Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core:common:widgets): add reusable components ToolbarLogo and ThemeSwitcher #36

Merged
merged 12 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 1.4.0+5

- Implement `ToolbarLogo` and `ThemeSwitcher`
- feat: added `ToolbarLogo` and `ThemeSwitcher` as reusable components with UI implementation, tests, and documentation [#35](https://github.com/pactus-project/pactus-gui/pull/36)

# 1.3.0+4

- Implement Theme Management
Expand Down
7 changes: 7 additions & 0 deletions assets/icons/ic_dark_mode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/icons/ic_light_mode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/icons/ic_logo_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions assets/icons/ic_logo_light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/icons/moon.png
Binary file not shown.
Binary file removed assets/icons/sun.png
Binary file not shown.
1 change: 1 addition & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class MyApp extends StatelessWidget {
return BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, themeState) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: themeState.themeData,
locale: languageState.selectedLanguage.value,
Expand Down
6 changes: 6 additions & 0 deletions lib/src/core/common/colors/app_colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:flutter/material.dart' show Color;

class AppColors {
AppColors._();
static const primaryDark = Color(0xFF242424);
}
86 changes: 86 additions & 0 deletions lib/src/core/common/widgets/theme_switcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gui/src/core/common/colors/app_colors.dart';
import 'package:gui/src/core/enums/theme_modes.dart';
import 'package:gui/src/core/utils/assets/assets.gen.dart';
import 'package:gui/src/features/main/theme/bloc/theme_bloc.dart';

/// ### [ThemeSwitcher] Documentation
/// A widget that toggles between light and dark themes using a animated switch.
///
/// - Uses `Theme.of(context)` to determine the current theme.
/// - Displays different icons for light and dark modes with animations.
/// - Provides a switch that triggers a theme change by `ThemeBloc` when tapped.
///
class ThemeSwitcher extends StatelessWidget {
const ThemeSwitcher({super.key});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isLightTheme = theme.brightness == Brightness.light;
const duration = Duration(milliseconds: 200);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedOpacity(
curve: Curves.easeIn,
opacity: isLightTheme ? 0.0 : 1.0,
duration: duration,
child: SvgPicture.asset(
Assets.icons.icLightMode,
),
),
// Switch
GestureDetector(
onTap: () {
context.read<ThemeBloc>().add(
ThemeChanged(
theme: isLightTheme ? ThemeModes.dark : ThemeModes.light,
),
);
},
child: AnimatedContainer(
margin: EdgeInsets.symmetric(horizontal: 4),
width: 40, // Total width of the switch
height: 20, // Total height of the switch
duration: duration * 2,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.primaryDark,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: AnimatedAlign(
duration: Duration(milliseconds: 100),
alignment:
isLightTheme ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
width: 14, // Thumb width
height: 14, // Thumb height
decoration: BoxDecoration(
color: AppColors.primaryDark,
borderRadius: BorderRadius.circular(7),
),
),
),
),
),
),
// Sun icon (right side)
AnimatedOpacity(
curve: Curves.easeIn,
opacity: isLightTheme ? 1.0 : 0.0,
duration: duration,
child: SvgPicture.asset(
Assets.icons.icDarkMode,
),
),
],
);
}
}
22 changes: 22 additions & 0 deletions lib/src/core/common/widgets/toolbar_logo.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gui/src/core/utils/assets/assets.gen.dart';

/// # [ToolbarLogo] Documentation
/// A stateless widget that displays the app's toolbar logo.
/// Automatically adapts to the current theme (light or dark) and selects the
/// appropriate logo asset.
///
class ToolbarLogo extends StatelessWidget {
const ToolbarLogo({super.key});

@override
Widget build(BuildContext context) {
final isLightTheme = Theme.of(context).brightness == Brightness.light;
return SvgPicture.asset(
width: 25,
height: 25,
isLightTheme ? Assets.icons.icLogoLight : Assets.icons.icLogoDark,
);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
2 changes: 1 addition & 1 deletion lib/src/features/main/theme/theme_data/app_theme_data.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:gui/gen/fonts.gen.dart';
import 'package:gui/src/core/enums/theme_modes.dart';
import 'package:gui/src/core/utils/assets/fonts.gen.dart';
import 'package:gui/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart';
import 'package:gui/src/features/main/theme/theme_data/pallets/surface_pallet.dart';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:gui/src/core/common/widgets/theme_switcher.dart';
import 'package:gui/src/core/common/widgets/toolbar_logo.dart';
import 'package:gui/src/features/main/language/presentation/widget/language_widget.dart';
import 'package:gui/src/features/main/theme/presentation/widgets/theme_selector.dart';
import 'package:gui/src/features/main/theme/theme_data/pallets/on_surface_pallet.dart';
Expand All @@ -23,6 +25,7 @@ class _MyHomePageState extends State<MyHomePage> {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
actions: [ToolbarLogo(), ThemeSwitcher()],
backgroundColor: theme.colorScheme.inversePrimary,
title: Text(AppLocalizations.of(context)!.title),
),
Expand Down
8 changes: 6 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: gui
description: "Pactus Flutter GUI"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.3.0+4
version: 1.4.0+5

environment:
sdk: ^3.5.3
Expand All @@ -17,9 +17,9 @@ dependencies:
flutter_gen_runner: ^5.3.1
flutter_localizations:
sdk: flutter
flutter_svg: ^2.0.16
freezed: ^2.5.7
freezed_annotation: ^2.4.4

get_it: ^8.0.2
intl: ^0.19.0
json_annotation: ^4.9.0
Expand All @@ -31,6 +31,10 @@ dev_dependencies:
sdk: flutter
json_serializable: ^6.5.0

flutter_gen:
output: lib/src/core/utils/assets # Optional (default: libf/gen/)
line_length: 80 # Optional (default: 80)

flutter:
generate: true
uses-material-design: true
Expand Down
82 changes: 82 additions & 0 deletions test/src/core/common/widgets/theme_switcher_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gui/src/core/common/widgets/theme_switcher.dart';
import 'package:gui/src/core/utils/assets/assets.gen.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

/// Checks if the asset exists by attempting to load it.
Future<bool> isAssetValid(String assetPath) async {
try {
// Attempt to load the asset from the root bundle
final assetData = await rootBundle.load(assetPath);
return assetData.lengthInBytes > 0;
} on Exception catch (_) {
return false;
}
}

group('Asset Validation Tests', () {
test('Light mode asset exists', () async {
final assetPath = Assets.icons.icLightMode;
expect(
await isAssetValid(assetPath),
isTrue,
reason: 'The light mode asset should exist in the project assets.',
);
});

test('Dark mode asset exists', () async {
final assetPath = Assets.icons.icDarkMode;
expect(
await isAssetValid(assetPath),
isTrue,
reason: 'The dark mode asset should exist in the project assets.',
);
});
});
group('ThemeSwitcher Tests', () {
testWidgets('should detect light theme in UI', (WidgetTester tester) async {
// Create a light theme context
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.light(),
home: const ThemeSwitcher(),
),
);

// Verify light theme-specific icon (e.g., dark mode icon) is visible
final lightModeIcon = find.byType(SvgPicture).first;
expect(lightModeIcon, findsOneWidget);

// Verify the opacity for the sun icon is visible (light theme icon)
final sunIconOpacity = tester.widget<AnimatedOpacity>(
find.byType(AnimatedOpacity).last,
);
expect(sunIconOpacity.opacity, equals(1.0));
});

testWidgets('should detect dark theme in UI', (WidgetTester tester) async {
// Create a dark theme context
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.dark(),
home: const ThemeSwitcher(),
),
);

// Verify dark theme-specific icon (e.g., light mode icon) is visible
final darkModeIcon = find.byType(SvgPicture).last;
expect(darkModeIcon, findsOneWidget);

// Verify the opacity for the moon icon is visible (dark theme icon)
final moonIconOpacity = tester.widget<AnimatedOpacity>(
find.byType(AnimatedOpacity).first,
);
expect(moonIconOpacity.opacity, equals(1.0));
});
});
}
Loading
Loading