Skip to content

Commit

Permalink
feat(core:common:widgets): add reusable components ToolbarLogo and …
Browse files Browse the repository at this point in the history
…`ThemeSwitcher` (#36)

* update: remove `debug banner` in top of all screens

* chore: add `flutter_svg` in project dependencies

* chore: changing `flutter_gen` configs in project dependencies

* feat: add `ToolbarLogo` widget to app screen bar

* test: add `ToolbarLogo` widget tests

* docs: add `ToolbarLogo` widget docs

* feature: add `ThemeSwitcher` widget

* update: remove unused png assets for theme switcher widget

* docs: add docs for `ThemeSwitcher` class

* test: add test for `ThemeSwitcher` class

* style: re-arrange code and fix `code styles`

* update: update `CHANGELOG.md` & `pubspec.yaml` files
  • Loading branch information
esmaeil-ahmadipour authored Dec 7, 2024
1 parent 8fcfd70 commit f551393
Show file tree
Hide file tree
Showing 18 changed files with 347 additions and 10 deletions.
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

0 comments on commit f551393

Please sign in to comment.