From 26bd6fcae2bd37c770567776a31d9457f5f78ebf Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Thu, 5 Dec 2024 16:59:08 +0330 Subject: [PATCH 01/12] update: remove `debug banner` in top of all screens --- lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/main.dart b/lib/main.dart index f1cebdc..fe2a75c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,6 +32,7 @@ class MyApp extends StatelessWidget { return BlocBuilder( builder: (context, themeState) { return MaterialApp( + debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: themeState.themeData, locale: languageState.selectedLanguage.value, From 7644012fe4b9d9faa8d141c5945dfec352d78817 Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Thu, 5 Dec 2024 17:00:08 +0330 Subject: [PATCH 02/12] chore: add `flutter_svg` in project dependencies --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5a9c5f3..e80f0e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: sdk: flutter freezed: ^2.5.7 freezed_annotation: ^2.4.4 - + flutter_svg: ^2.0.16 get_it: ^8.0.2 intl: ^0.19.0 json_annotation: ^4.9.0 From 2f281c371e774846ed8d60fb49e2d389d5883aab Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Thu, 5 Dec 2024 17:01:29 +0330 Subject: [PATCH 03/12] chore: changing `flutter_gen` configs in project dependencies --- pubspec.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index e80f0e1..7020059 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 From 2e8740175d71cf89c67e32d3910f27740e8a6424 Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Thu, 5 Dec 2024 17:04:20 +0330 Subject: [PATCH 04/12] feat: add `ToolbarLogo` widget to app screen bar --- assets/icons/ic_logo_dark.svg | 3 +++ assets/icons/ic_logo_light.svg | 18 ++++++++++++++++++ lib/src/core/common/widgets/toolbar_logo.dart | 17 +++++++++++++++++ .../core/utils/assets}/assets.gen.dart | 9 ++++++++- .../core/utils/assets}/fonts.gen.dart | 0 .../main/theme/theme_data/app_theme_data.dart | 2 +- .../presentation/screen/home_page.dart | 2 ++ 7 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 assets/icons/ic_logo_dark.svg create mode 100644 assets/icons/ic_logo_light.svg create mode 100644 lib/src/core/common/widgets/toolbar_logo.dart rename lib/{gen => src/core/utils/assets}/assets.gen.dart (92%) rename lib/{gen => src/core/utils/assets}/fonts.gen.dart (100%) diff --git a/assets/icons/ic_logo_dark.svg b/assets/icons/ic_logo_dark.svg new file mode 100644 index 0000000..c0f8611 --- /dev/null +++ b/assets/icons/ic_logo_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_logo_light.svg b/assets/icons/ic_logo_light.svg new file mode 100644 index 0000000..59503f3 --- /dev/null +++ b/assets/icons/ic_logo_light.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/lib/src/core/common/widgets/toolbar_logo.dart b/lib/src/core/common/widgets/toolbar_logo.dart new file mode 100644 index 0000000..1271170 --- /dev/null +++ b/lib/src/core/common/widgets/toolbar_logo.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:gui/src/core/utils/assets/assets.gen.dart'; + +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, + ); + } +} diff --git a/lib/gen/assets.gen.dart b/lib/src/core/utils/assets/assets.gen.dart similarity index 92% rename from lib/gen/assets.gen.dart rename to lib/src/core/utils/assets/assets.gen.dart index 97ba016..4a429a3 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/src/core/utils/assets/assets.gen.dart @@ -16,6 +16,12 @@ class $AssetsIconsGen { AssetGenImage get clipboard => const AssetGenImage('assets/icons/clipboard.png'); + /// File path: assets/icons/ic_logo_dark.svg + String get icLogoDark => 'assets/icons/ic_logo_dark.svg'; + + /// File path: assets/icons/ic_logo_light.svg + String get icLogoLight => 'assets/icons/ic_logo_light.svg'; + /// File path: assets/icons/lock.png AssetGenImage get lock => const AssetGenImage('assets/icons/lock.png'); @@ -29,7 +35,8 @@ class $AssetsIconsGen { AssetGenImage get sun => const AssetGenImage('assets/icons/sun.png'); /// List of all assets - List get values => [clipboard, lock, logo, moon, sun]; + List get values => + [clipboard, icLogoDark, icLogoLight, lock, logo, moon, sun]; } class $AssetsImagesGen { diff --git a/lib/gen/fonts.gen.dart b/lib/src/core/utils/assets/fonts.gen.dart similarity index 100% rename from lib/gen/fonts.gen.dart rename to lib/src/core/utils/assets/fonts.gen.dart diff --git a/lib/src/features/main/theme/theme_data/app_theme_data.dart b/lib/src/features/main/theme/theme_data/app_theme_data.dart index 9e630b9..f704767 100644 --- a/lib/src/features/main/theme/theme_data/app_theme_data.dart +++ b/lib/src/features/main/theme/theme_data/app_theme_data.dart @@ -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'; diff --git a/lib/src/features/splash_screen/presentation/screen/home_page.dart b/lib/src/features/splash_screen/presentation/screen/home_page.dart index 61cfec0..bc9aa04 100644 --- a/lib/src/features/splash_screen/presentation/screen/home_page.dart +++ b/lib/src/features/splash_screen/presentation/screen/home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.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'; @@ -23,6 +24,7 @@ class _MyHomePageState extends State { final theme = Theme.of(context); return Scaffold( appBar: AppBar( + actions: [ToolbarLogo()], backgroundColor: theme.colorScheme.inversePrimary, title: Text(AppLocalizations.of(context)!.title), ), From e00c73b6d3c8687a3ba741e6e568338aa36bce0c Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 08:20:15 +0330 Subject: [PATCH 05/12] test: add `ToolbarLogo` widget tests --- .../widgets/test_toolbar_logo_widget.dart | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/src/core/common/widgets/test_toolbar_logo_widget.dart diff --git a/test/src/core/common/widgets/test_toolbar_logo_widget.dart b/test/src/core/common/widgets/test_toolbar_logo_widget.dart new file mode 100644 index 0000000..15775c5 --- /dev/null +++ b/test/src/core/common/widgets/test_toolbar_logo_widget.dart @@ -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/toolbar_logo.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 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 logo asset exists', () async { + final assetPath = Assets.icons.icLogoLight; + expect(await isAssetValid(assetPath), isTrue, + reason: 'The light logo asset should exist in the project assets.',); + }); + + test('Dark logo asset exists', () async { + final assetPath = Assets.icons.icLogoDark; + expect(await isAssetValid(assetPath), isTrue, + reason: 'The dark logo asset should exist in the project assets.',); + }); + }); + + group('Theme Switching Tests', () { + testWidgets('Displays light logo in light theme', (tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light(), + home: const Scaffold( + body: ToolbarLogo(), + ), + ), + ); + + // Find the SvgPicture widget + final finder = find.byType(SvgPicture); + expect(finder, findsOneWidget, reason: 'SvgPicture should be present.'); + + // Verify the asset path indirectly + final svgWidget = tester.widget(finder); + expect( + svgWidget.toString().contains(Assets.icons.icLogoLight), + isTrue, + reason: 'The light logo asset should be used in a light theme', + ); + }); + + testWidgets('Displays dark logo in dark theme', (tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.dark(), + home: const Scaffold( + body: ToolbarLogo(), + ), + ), + ); + + // Find the SvgPicture widget + final finder = find.byType(SvgPicture); + expect(finder, findsOneWidget, reason: 'SvgPicture should be present.'); + + // Verify the asset path indirectly + final svgWidget = tester.widget(finder); + expect( + svgWidget.toString().contains(Assets.icons.icLogoDark), + isTrue, + reason: 'The dark logo asset should be used in a dark theme', + ); + }); + }); +} From c01c941d958e011f593816af77f96915a80f6962 Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 08:20:26 +0330 Subject: [PATCH 06/12] docs: add `ToolbarLogo` widget docs --- lib/src/core/common/widgets/toolbar_logo.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/core/common/widgets/toolbar_logo.dart b/lib/src/core/common/widgets/toolbar_logo.dart index 1271170..e61c255 100644 --- a/lib/src/core/common/widgets/toolbar_logo.dart +++ b/lib/src/core/common/widgets/toolbar_logo.dart @@ -2,6 +2,11 @@ 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}); From 2cc0570363cb3e98906fe39722ee414e8dccf523 Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 13:26:18 +0330 Subject: [PATCH 07/12] feature: add `ThemeSwitcher` widget --- assets/icons/ic_dark_mode.svg | 7 ++ assets/icons/ic_light_mode.svg | 4 + lib/src/core/common/colors/app_colors.dart | 6 ++ .../core/common/widgets/theme_switcher.dart | 79 +++++++++++++++++++ lib/src/core/utils/assets/assets.gen.dart | 14 ++-- .../presentation/screen/home_page.dart | 3 +- 6 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 assets/icons/ic_dark_mode.svg create mode 100644 assets/icons/ic_light_mode.svg create mode 100644 lib/src/core/common/colors/app_colors.dart create mode 100644 lib/src/core/common/widgets/theme_switcher.dart diff --git a/assets/icons/ic_dark_mode.svg b/assets/icons/ic_dark_mode.svg new file mode 100644 index 0000000..42e03cb --- /dev/null +++ b/assets/icons/ic_dark_mode.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/assets/icons/ic_light_mode.svg b/assets/icons/ic_light_mode.svg new file mode 100644 index 0000000..bb49a19 --- /dev/null +++ b/assets/icons/ic_light_mode.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/src/core/common/colors/app_colors.dart b/lib/src/core/common/colors/app_colors.dart new file mode 100644 index 0000000..98f70b3 --- /dev/null +++ b/lib/src/core/common/colors/app_colors.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart' show Color; + +class AppColors { + AppColors._(); + static const primaryDark = Color(0xFF242424); +} diff --git a/lib/src/core/common/widgets/theme_switcher.dart b/lib/src/core/common/widgets/theme_switcher.dart new file mode 100644 index 0000000..5820a80 --- /dev/null +++ b/lib/src/core/common/widgets/theme_switcher.dart @@ -0,0 +1,79 @@ +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'; + +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().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, + ), + ), + ], + ); + } +} diff --git a/lib/src/core/utils/assets/assets.gen.dart b/lib/src/core/utils/assets/assets.gen.dart index 4a429a3..c0636b9 100644 --- a/lib/src/core/utils/assets/assets.gen.dart +++ b/lib/src/core/utils/assets/assets.gen.dart @@ -16,6 +16,12 @@ class $AssetsIconsGen { AssetGenImage get clipboard => const AssetGenImage('assets/icons/clipboard.png'); + /// File path: assets/icons/ic_dark_mode.svg + String get icDarkMode => 'assets/icons/ic_dark_mode.svg'; + + /// File path: assets/icons/ic_light_mode.svg + String get icLightMode => 'assets/icons/ic_light_mode.svg'; + /// File path: assets/icons/ic_logo_dark.svg String get icLogoDark => 'assets/icons/ic_logo_dark.svg'; @@ -28,15 +34,9 @@ class $AssetsIconsGen { /// File path: assets/icons/logo.png AssetGenImage get logo => const AssetGenImage('assets/icons/logo.png'); - /// File path: assets/icons/moon.png - AssetGenImage get moon => const AssetGenImage('assets/icons/moon.png'); - - /// File path: assets/icons/sun.png - AssetGenImage get sun => const AssetGenImage('assets/icons/sun.png'); - /// List of all assets List get values => - [clipboard, icLogoDark, icLogoLight, lock, logo, moon, sun]; + [clipboard, icDarkMode, icLightMode, icLogoDark, icLogoLight, lock, logo]; } class $AssetsImagesGen { diff --git a/lib/src/features/splash_screen/presentation/screen/home_page.dart b/lib/src/features/splash_screen/presentation/screen/home_page.dart index bc9aa04..a25fc5d 100644 --- a/lib/src/features/splash_screen/presentation/screen/home_page.dart +++ b/lib/src/features/splash_screen/presentation/screen/home_page.dart @@ -1,5 +1,6 @@ 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'; @@ -24,7 +25,7 @@ class _MyHomePageState extends State { final theme = Theme.of(context); return Scaffold( appBar: AppBar( - actions: [ToolbarLogo()], + actions: [ToolbarLogo(),ThemeSwitcher()], backgroundColor: theme.colorScheme.inversePrimary, title: Text(AppLocalizations.of(context)!.title), ), From 385a942540160cfe0794f8f9cd6bc9fcfc43ec0b Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 13:27:17 +0330 Subject: [PATCH 08/12] update: remove unused png assets for theme switcher widget --- assets/icons/moon.png | Bin 14879 -> 0 bytes assets/icons/sun.png | Bin 3052 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/icons/moon.png delete mode 100644 assets/icons/sun.png diff --git a/assets/icons/moon.png b/assets/icons/moon.png deleted file mode 100644 index 871abcb8d3b69115b280c98bfc23e35f72d7b5cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14879 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� zRw%4((PCE+K_8Gvjs@(BajncSp5jQlQ@+{Bn|%w^ODr6^=A zOuc`0=1L!3`tzTlO6^9=exGb1g&LON(6Ep1Yg% zf$N#=L=3rM6|p&byS7Gg{$H<#i^LTaY&2c9*^xQyVBhJux6_|;LW3OooNg}{brT9K zkf?FWWFHz?@F2gPOqwE5NDBQtJ^(x4yW1`9GVpTx+$%VDZh%2=#Ilj3y6>fwC10ZP~)|ywlIJCRrc;#VEJ3!xWY@}H5kN6!whUz$e(*S3ZTBl4_ zF>+Y>x3|~*m*asB3Og@w_-2=(idkn~wPj2D=F(qZX7Z;uu4s2&y+ZF-|LS-IK;9Ax zUB?OVjjwOxZ@+Ox7(Yc|#G@#Ky5;tld7b!d0iooTK-9j)Y7|sw6*I7JMJ9-$uZOZin=7Qa4US-0Der{GA0SW#9BatqyGC3&!bh1}VY2s)+2qzL?89Xsl53yV66` zX9xFG^O^u!t}|>U4qusJZU(*zXs<+erFj1&UCe~@40(6o_iEH`Ga1JtFsIfUfj7b|;MZyStLG9yH{OBckP^}g|z$ZWMerp1;|164*M1C$v(b3tWQB+sY7qm6dWY=mr0R-D4%wz^9c z;pT6lxW2dY&(C}bAN^v6QNoF02fdTtCkT)U*Lex18!%%8|7ICcr>SwAwmxv-+h?Y7 zs7QBFH&H=AJOS#s;P-uU^Fo69e_Gwj+WsG664|)v8_Vf-f>>6mH>Q5lZ(Z7NXE;Dr zw;C>EH1EBw7dr;01FQ!6Sns~D!ZjYvaLEDvU1oszL7BS#X>alG2+dD!aH-y_)=vxd zpuem0Q&;`M)y)1S`_; zSZ&Zw+&kx_@N|5y^}XU*R&Ya9MC0ON&pYx_nyU&YPx`$v^+|c8>9L#jXKC+#p!D_f zD_`Zei(7s+KfZoc`RyX9MazN=oKSG7+p@7qz#`@xAYZa!WjU2kDE>ubURRj7oy^Kq zPr)f{w;2BrnXK8f?QF`N_dcTF>+ASq1U8!I^}dD#Yq%L*MS=xe@PY?qKh9)iq&Es8 zZ#_}QWZk!BKF}?^GNMkB8Eo~G;X zljzJgY|L_RYNP?Vg8*HWjADk<9ibDpcMNMiRVpuXkPeqKN54xwsuN$D>JX@YseYD;goyzB)%sc5iG%(Nc_97g; zYY_=D0Ej8|El(Nc&zu-JN=>$F@%xi68TvceO~#J_kp1b)f7We%2G)HeLne@9YF@8V z)($cJa}YOdKW?h?g9JfgAr8x{ft_ChgWtco0!n*)gTq^AZ@Ev7o4PA-02IuzE6Wg6$1w2hxbQ@ap@R>O`fg&M zw$f%M@NqP*S7w4eY7GU~Eml5II(M6MOy*-@$`4i6Ej~Wy;$IGGBmn&Ut$`3Vx%5@- z0^RX6Ol2IxPX9_qo_am?{_}m^;d(zXt@=9x=?o(9zbxc%cNm_>46q4$7#&hqJJjso7~y=jzG6k_ zAm~8#rO~@JrQNQk#X6nNHT>%*m^+g(m8|^jLbV_gRLicN-zUYH>ut9k>{`j%7Wtw6 z6cG~To{#ij6pM#xnt|J_q~0;P9S$gwz4|B2>`~-2k)gg;hDmYuufn^AEc>7Dl6VnR z6Fs$q*k87MY+x{KYVhB~R1KQ;gyKu}s{XgM5>XIxx4;~Oa zJB^e}gLG6X(3r_6bNQ405O*qJ`Jrb%C39Ccsf7XXh#jIU-`FcrQib2QaPna>RH;!z zW8_7s;-g&pJEKMvV)(1Uw?`QdCYCh}k}jrZBpr6)?2yua7vMHiviW##{x(_H_|X4h zS`HU}j^hw?UdWQ*G9RZX{C(H`?65eP{zQVKz3qk()C)Ex+3W6$gSsn&C}`*iApgEj znINqX4N7ui93a+!3T>}F`^Q{_FqZMs2yt0^}0Os z$gJFp>tHnlvfypiXGgJ=b;tOZ8}(@mLH0X$*r>b%b=&&=f-^Ty8<5J5U$xiJdup^Sp=WIjOT_>yLc)E@_m)3!|H9k-7y+`+gSR41SFbJ~ zR6&3uT;pd_)uQebHr0LNi1kOKB!dbOr-LT}`R8km)GC{e-I8R9jS$5eTK(FPWKU9^ z2*G)d35Nm1OC!re^?RQa@$e<84?aMl^N8sv7zx#EE(+c3dgZH=5Hj!INLyI`J4E|I z+lAvNfRB2zG4zzSRzhFZOo}-2UTFzh- zu~)-6_G$i?`*4~|Pqc2fW~a10GCD4vm36g(q{tu!rnyDXl^X{{Li$M>jf}_Pu8M?@ zg?gV#uRqlKS!Yh5>k>aTpK^ijhS{0@p9$&3yUyo2c{vp(ziBamOXD3qWoN&>U**dE zolnV`rO$Pud)(BU9vNy1ls22qSI;C8UAO_!>4x{7Y4LrEBCDc@)*ea)p!UPVIJ37~ zsb?KVe_fw3htf9SqzmEXfw;J=)vVlC66e|^MHJy&9G4^pFiObxUpESpY!3Y$WA5BF zRwsdR>R9>R(94lb^;Tgt^>uwtNDT$vfL5G)IYvj{M+i#TfX_lV957QB5WQJom)_N*+&$tXxDB~odjEgC| za52eA=zRB%&U^mA3dd~sZs;l=!7~jfTU2+$Wd}(bMZvGL%6^b`?RSvC``d?5(m;E> zq5l@s6~NN%uIBmJCsgvUPt@G=B$MFuD~^Edt5@IaSO4GuM27hi{tSqDR#2*SxdHl* zH3xIz_2c8ISyA`m%amsUA73A=955aE&su_`e3N2bo9^GXUMfy`1jZQfbf*7 zm@V?&^M8_2U2qMg7^c@YzAt>Bk*jlX?msz}4LHKFLKE{gxjqtmHvi3LU6_}~%{OQ8 z%Q1R;nIi>Bocx$^+$7B4p029%C%!Xlon^(v*vu=&prCr6z2mWgzp+iiOJa9NM4ljs zMR%T)$rl)+SYl`gDQ<})%N#JBMa~0@jr@~w>D`MtYk5$gthiwt>2GVQm6~?D{16?1 zAp2iUJFvxY97R@#BwbSI z=ljh9XXykI{&i9m#krMuS9Y1 z{puM|P~rQQ!ebJV>0;V1LMK^vf7Zo;2MHWVJIMpcTayRe=s|E6t`?5lR!gzjdhRRz zHNjgsfr}z9y{qW2Oy@4A(pn*(nx!+*7RUz!(=ZhmdKhA1-16XgHM|8Nfq^{w>p~ap z@{6-Gse6%M9qdwS)nyJ601_ z|1l7IWePDcH5k9pO`QtldRn0|<@$<&;FuVgPZGC2zOMtWLLwG>$|yqh_oA49q6fxDB$u(3R99Wv~02!^9YtzAICbNQ6_s z0TJP>%4d$ev*$SJ!)eSKx}oZmd%-Q?e=*IQHhw8ExAQjmcxfU}`smb*19)g%m-;b8Fx9 zCH?e^HYr(0XbQLAhyl3l#P_6UtvukZ=s{j&l*G~G$$rOb^n;5%1Iaz#q>Cb#?}tL= zs?t#Vzy;M?eDDqLVv^C3mrajfUqs5?@eqrJn z^_Vb(@c%1$TqpMgXpJmfN;|8$bzwUJ`Yd`}zn&BT6Y6s%mQOUJI@~9!6|?xu;uGI4 zm3)3M)Qtth7x2v9cv6&-NpSUdmTb(xhU2XSD2WpcYwhEq)GwD_N|bV{iWgqAbMLx% zht66Pk@lXO-jOw2O+kFvtGNq+P;(8Ft7|jFt0mfVrooc>vWX#6!a#B2enXXrb>y9; ztDjEmsB68~JbTmkI0h`A%Y_s0tIt3$IeA9HK;cSLn}9L8C#LG3_LONn5A@^Tle0_k zxDwBoBWCnt$+6G!;Xfg8`aA6&0r80*&6ga@N8#Sq%UgdR0DITxHrCS9QA-#I`^ z>+2fWVS=}+Xn*`m*0F2FZrxb1HtJGmzk9pl{O90q^>AZI9P;WIzc<9xvd#xPUu}K$ zZ0LL#CLB9V*_J;kW^_N>X}4QsqwRU{`#}y2#}>~$xYl|y-ig!n#~lyU*xNJ-4@f_< zD?fa>F1(~+&xWr_P~GJ3x-`)t*6Ugihh0icllhQ$4f7?|Qvl8#$*@XH?FCDt?`}!j z<4i=Au0?yia!zVKe5hD><-E6c*;`)+|0lOEF{SRU;Q(RCf|n@or@8hqs{PQ`=YQuh z{NB#TM9a{9!EEFEV}YT0XEtBMy+yr{$10DZ z&>}o{oGwqC{hW1w>76}g4-0K_&CqWT8ZTU6s@82;)h*sTN-Ee*3!|5WPJR;=YgzNs zgKu`W0|nR*yVl}>AZjCqPT&iqYr@-?h_D@*;5-oNHSsA|G%WKNB@zyFq6px@A@;~l zQ8>6F@eAoDJ_t9)fiE9L#Jbe0+&q>aav;DjW*VYA-85Ct9E$;V&yxZs<=G?u!Ar^x zjU*c!+Jhk&SjV6NyXMvpSjU|>EMQbD`Byv6#YEc10^G56gWPI46R8wIup^!|R^#8q z-n*){naBt4c52+hbsTT`cYzfFj72%nE9;-UdC?z|DC;=B!%{)1w<*V9WGP#B6x_Jo z*dPl(k<3ICHU7$}b4e_SIqf$~CMuslI{Lr>QM(yMaI$kAaC^C31`i_fU*!p>7Q-tH zM23Nle=2BDAW@FQYxwSl97O^v!5?>tyaOAZN07kj_cUDQZM+iSm~u8fJ{2_IJ?)tO zysWLXki$V70QGD88-t*)*FQz4VF|)r#$OzEl&e%# zy)P~v!>DxTrrFSQb&ZHM_^C?uUzp;YrTOLbN{ao)!_+DVCOZsu_vr>tG_X5PREbIP zsf(w{W~_4|gVs-9>Whmo!W(|3%fc&_Oj2K14SQ9aNwJjvwVP_-3{L!|6Qc){7J5fM zi>P_09;Mx3rZNPdnWU6I!bV?Y(gM;Z%@``*C)h7u=b1#i>(y-&% z^2)&dIy8bu45rLgCy6Sxg^zQ@F|yyiuH0Ce5qN=Hd||$+jaMW53%s0&Qn5mdZ9gZC zgQ($pNd!)D;fiN%ltu5K`IaKeuwukYSDsz&Q@VMNS~o}WpEDI&JDb_a*Tt+6X7rN# z+_vsw$j^~dLl_zY&L2ORqTg`ydq0Ik;H;WQ5N`gCeZ*EnSb46QpoldAfCM6jO=ZmU zX2pwxBDj>><}c4z`JZaAF}%|BYN208=|kesWCmrhzp^#%u(croRvmWaCxZ6DqFzp? zua)vjoN0}IUFd7Trm^X?u(3E0EXB-Jf1@}h@7hCB~0bA&4Na5+;)c$YK&8D2O5=B8#}7 z#1)Dxw$ z2m2Wt+8Sao7-Rp{%uw{s(VSR4^g2J@tO>mt$W})xFqq|mnp5ipqo@jl(QOfN)+*Ps zf*@EbCUAMuScISw%TP21vtp%6#)ab%B`y{bh$IZ)d0hj56Y&^81eFD{WZsBSw0ff) z3ELRVfj7p(bRMwM1Ghp2p$^1|l8aM`6C?^q#Q-M!Lg-ranh4+~Aj)_K;He3STgzhO zyrpskM<Rab-jj0o2Hv0P^4&oGekEFcF*w6OjZ& zj7XFU6ib@H%7jv-R3Vi9iR+oqe+Yo4m&KZi@lh^f@k|JX(kBU(F_n;yq7|GJ8A1$2 z6w*XFjQAv>G#xc+WRSNU;VPwaj#Qd3T`cxAWgMAGaKSBMabb}}Bf|Lu6NJfCA`Aen z8wC%N@MIE)3_>IjqJYal5(I*iP?nS@;-`ER>IzZle}JNm!{aKs{};@IA-+^D=Ax2C zVy*xo$|M2+HL#l4pB2&CJRIS-B&(Yej42=neSsZo@?x>EYcTmx*)jl@b-As$?>?P zQ>RaFn9aBtQKuuF_c|^arw2ff$2PThq(?l-#kk``Qh*B>S@(t1rjBErNBP*!;1tth z4CqkWP;& zc*pIb0;|q^Hosrzd!-dMa8b_-?9)}u?w;Fa2U9|;-M{3S$8{W<6ZIxG6<4~gzd}2sdaej5X`qYne{yTQ5xnn=Kj!~x0u1K0jDi>RM)npnd*POv)%kNV@@50 zu(Q&(dYh}$*(5x0EjU$|f8|1^M{%06C5yINYHv&k-MoAL*~_)wN5SDRzqHHzHHKZC z+q3hvQlD2IOBi>mJK72tM;=JyEht$~+}J5()DYG6w;U6T;6UqSh7K*>c;UREP%9@& zVTaR}hnS<+W?gfy?f&`}cFy4wI*s>eeV$jYQ$oeD`t6G2^atzx+Ru7RoWXhBk!4ZeK98*o zO1XSBcJRnh7I_u3+Ip-J88+E|9h*1S9Cji}4(9D^=oK<|^&GDaZSJ3E=xNUh4m)cw zC%a|{6Tk}c-fpXXZiu=sd)#y2*V5OwM2=^Q_`8-+%2OX1T{`iuGLL=C>p74yjBQyk zY7;*&-10UcHmgMk+PNYPZr^m#?jZ-{p0nQSG5`Aa3~#enH?8}Ucix~~{Pw)Fz0B@S zXOzhC2bUV(@}`usRL52`wzdT8z9!Q!x-G4(=? z%yg6A!hH+6cW@+Y?4dPAPL#Pk)w~yiW=2Jy{W7mS(K+m$b4qOEX5;g*WibU9B5?{O*>@gNyb!2{r!B}%aTu>T(>FzF0n9Y&K@2vEN;W;|lmg|VR z&(_n*$jYajc8hy;?imm%DIs$vOFuVOPCn#L|XPwi);u8( Date: Fri, 6 Dec 2024 13:49:04 +0330 Subject: [PATCH 09/12] docs: add docs for `ThemeSwitcher` class --- lib/src/core/common/widgets/theme_switcher.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/core/common/widgets/theme_switcher.dart b/lib/src/core/common/widgets/theme_switcher.dart index 5820a80..1fad04c 100644 --- a/lib/src/core/common/widgets/theme_switcher.dart +++ b/lib/src/core/common/widgets/theme_switcher.dart @@ -6,6 +6,13 @@ 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 an 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 via `ThemeBloc` when tapped. +/// class ThemeSwitcher extends StatelessWidget { const ThemeSwitcher({super.key}); From dfed5e2635dea0167c5c000a8a39c92b3033a24c Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 13:50:17 +0330 Subject: [PATCH 10/12] test: add test for `ThemeSwitcher` class --- .../common/widgets/theme_switcher_test.dart | 75 +++++++++++++++++++ ...get.dart => toolbar_logo_widget_test.dart} | 0 2 files changed, 75 insertions(+) create mode 100644 test/src/core/common/widgets/theme_switcher_test.dart rename test/src/core/common/widgets/{test_toolbar_logo_widget.dart => toolbar_logo_widget_test.dart} (100%) diff --git a/test/src/core/common/widgets/theme_switcher_test.dart b/test/src/core/common/widgets/theme_switcher_test.dart new file mode 100644 index 0000000..e34b84e --- /dev/null +++ b/test/src/core/common/widgets/theme_switcher_test.dart @@ -0,0 +1,75 @@ +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 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( + 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( + find.byType(AnimatedOpacity).first, + ); + expect(moonIconOpacity.opacity, equals(1.0)); + }); + }); +} diff --git a/test/src/core/common/widgets/test_toolbar_logo_widget.dart b/test/src/core/common/widgets/toolbar_logo_widget_test.dart similarity index 100% rename from test/src/core/common/widgets/test_toolbar_logo_widget.dart rename to test/src/core/common/widgets/toolbar_logo_widget_test.dart From 17ebf70d41e7ea98670a92324502ad6035fb8cf0 Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 14:10:47 +0330 Subject: [PATCH 11/12] style: re-arrange code and fix `code styles` --- .../core/common/widgets/theme_switcher.dart | 4 ++-- .../presentation/screen/home_page.dart | 2 +- pubspec.yaml | 2 +- .../common/widgets/theme_switcher_test.dart | 17 ++++++++++++----- .../widgets/toolbar_logo_widget_test.dart | 19 +++++++++++++------ 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/lib/src/core/common/widgets/theme_switcher.dart b/lib/src/core/common/widgets/theme_switcher.dart index 1fad04c..98c452e 100644 --- a/lib/src/core/common/widgets/theme_switcher.dart +++ b/lib/src/core/common/widgets/theme_switcher.dart @@ -7,11 +7,11 @@ 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 an animated switch. +/// 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 via `ThemeBloc` when tapped. +/// - Provides a switch that triggers a theme change by `ThemeBloc` when tapped. /// class ThemeSwitcher extends StatelessWidget { const ThemeSwitcher({super.key}); diff --git a/lib/src/features/splash_screen/presentation/screen/home_page.dart b/lib/src/features/splash_screen/presentation/screen/home_page.dart index a25fc5d..acde13e 100644 --- a/lib/src/features/splash_screen/presentation/screen/home_page.dart +++ b/lib/src/features/splash_screen/presentation/screen/home_page.dart @@ -25,7 +25,7 @@ class _MyHomePageState extends State { final theme = Theme.of(context); return Scaffold( appBar: AppBar( - actions: [ToolbarLogo(),ThemeSwitcher()], + actions: [ToolbarLogo(), ThemeSwitcher()], backgroundColor: theme.colorScheme.inversePrimary, title: Text(AppLocalizations.of(context)!.title), ), diff --git a/pubspec.yaml b/pubspec.yaml index 7020059..0c53aec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 - flutter_svg: ^2.0.16 get_it: ^8.0.2 intl: ^0.19.0 json_annotation: ^4.9.0 diff --git a/test/src/core/common/widgets/theme_switcher_test.dart b/test/src/core/common/widgets/theme_switcher_test.dart index e34b84e..845a4c2 100644 --- a/test/src/core/common/widgets/theme_switcher_test.dart +++ b/test/src/core/common/widgets/theme_switcher_test.dart @@ -5,9 +5,9 @@ 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 isAssetValid(String assetPath) async { try { @@ -18,17 +18,24 @@ void main() { 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.',); + 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.',); + expect( + await isAssetValid(assetPath), + isTrue, + reason: 'The dark mode asset should exist in the project assets.', + ); }); }); group('ThemeSwitcher Tests', () { diff --git a/test/src/core/common/widgets/toolbar_logo_widget_test.dart b/test/src/core/common/widgets/toolbar_logo_widget_test.dart index 15775c5..10c6b60 100644 --- a/test/src/core/common/widgets/toolbar_logo_widget_test.dart +++ b/test/src/core/common/widgets/toolbar_logo_widget_test.dart @@ -7,6 +7,7 @@ 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 isAssetValid(String assetPath) async { try { @@ -21,14 +22,20 @@ void main() { group('Asset Validation Tests', () { test('Light logo asset exists', () async { final assetPath = Assets.icons.icLogoLight; - expect(await isAssetValid(assetPath), isTrue, - reason: 'The light logo asset should exist in the project assets.',); + expect( + await isAssetValid(assetPath), + isTrue, + reason: 'The light logo asset should exist in the project assets.', + ); }); test('Dark logo asset exists', () async { final assetPath = Assets.icons.icLogoDark; - expect(await isAssetValid(assetPath), isTrue, - reason: 'The dark logo asset should exist in the project assets.',); + expect( + await isAssetValid(assetPath), + isTrue, + reason: 'The dark logo asset should exist in the project assets.', + ); }); }); @@ -48,7 +55,7 @@ void main() { expect(finder, findsOneWidget, reason: 'SvgPicture should be present.'); // Verify the asset path indirectly - final svgWidget = tester.widget(finder); + final svgWidget = tester.widget(finder); expect( svgWidget.toString().contains(Assets.icons.icLogoLight), isTrue, @@ -71,7 +78,7 @@ void main() { expect(finder, findsOneWidget, reason: 'SvgPicture should be present.'); // Verify the asset path indirectly - final svgWidget = tester.widget(finder); + final svgWidget = tester.widget(finder); expect( svgWidget.toString().contains(Assets.icons.icLogoDark), isTrue, From 14afd54ef852a3aef5ea4020b310fdd24f593783 Mon Sep 17 00:00:00 2001 From: Esmaeil Ahmadipour Date: Fri, 6 Dec 2024 14:24:46 +0330 Subject: [PATCH 12/12] update: update `CHANGELOG.md` & `pubspec.yaml` files --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f37f00a..db50c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pubspec.yaml b/pubspec.yaml index 0c53aec..8fb1a08 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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