From 71b32c8f5d6e889667eb0c3688a07a34e440f9c7 Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Sun, 27 Oct 2024 15:14:51 +0100 Subject: [PATCH] fix: HighContrast YaruMasterTile, move Example theme buttons to side pane - rework example to use watch_it instead of ubuntu service and provider --- example/analysis_options.yaml | 2 +- example/lib/code_snippet_button.dart | 26 +-- example/lib/common/space.dart | 20 --- example/lib/example.dart | 147 ++++++++--------- .../lib/example_dark_light_toggle_button.dart | 30 ++++ example/lib/example_high_contrast_button.dart | 24 +++ example/lib/example_home.dart | 88 ++++++++++ example/lib/example_model.dart | 29 +++- example/lib/example_page_items.dart | 17 +- example/lib/example_theme_button.dart | 41 +++++ example/lib/main.dart | 54 ++---- example/lib/pages/color_disk_page.dart | 15 +- example/lib/pages/icons_page/icon_view.dart | 13 +- example/lib/pages/icons_page/icons_page.dart | 154 +++++++++--------- .../pages/theme_page/src/controls/fabs.dart | 2 +- .../pages/theme_page/src/home/home_page.dart | 97 +---------- example/lib/pages/theme_page/theme_page.dart | 45 ----- example/lib/theme.dart | 24 --- .../lib/{pages/theme_page/src => }/utils.dart | 15 ++ example/pubspec.yaml | 3 +- lib/src/themes/common_themes.dart | 2 +- .../master_detail/yaru_master_tile.dart | 63 ++++--- 22 files changed, 461 insertions(+), 450 deletions(-) delete mode 100644 example/lib/common/space.dart create mode 100644 example/lib/example_dark_light_toggle_button.dart create mode 100644 example/lib/example_high_contrast_button.dart create mode 100644 example/lib/example_home.dart create mode 100644 example/lib/example_theme_button.dart delete mode 100644 example/lib/pages/theme_page/theme_page.dart delete mode 100644 example/lib/theme.dart rename example/lib/{pages/theme_page/src => }/utils.dart (54%) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 66b00e2da..7e9b67980 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -7,7 +7,6 @@ linter: always_declare_return_types: true avoid_catches_without_on_clauses: true avoid_equals_and_hash_code_on_mutable_classes: true - avoid_types_on_closure_parameters: true cancel_subscriptions: true directives_ordering: true eol_at_end_of_file: true @@ -27,3 +26,4 @@ linter: unnecessary_lambdas: true unnecessary_parenthesis: true use_named_constants: true + unnecessary_await_in_return: true diff --git a/example/lib/code_snippet_button.dart b/example/lib/code_snippet_button.dart index 5f45fd45c..efcddd3c3 100644 --- a/example/lib/code_snippet_button.dart +++ b/example/lib/code_snippet_button.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:flutter_highlight/themes/vs.dart'; import 'package:flutter_highlight/themes/vs2015.dart'; -import 'package:provider/provider.dart'; +import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import 'example_model.dart'; @@ -18,19 +18,11 @@ class CodeSnippedButton extends StatelessWidget { @override Widget build(BuildContext context) { - final model = context.watch(); return FloatingActionButton( onPressed: () => showDialog( barrierDismissible: true, context: context, - builder: (context) { - return ChangeNotifierProvider.value( - value: model, - child: _CodeDialog( - snippetUrl: snippetUrl, - ), - ); - }, + builder: (context) => _CodeDialog(snippetUrl: snippetUrl), ), tooltip: 'Example snippet', foregroundColor: Theme.of(context).colorScheme.onSurface, @@ -58,20 +50,20 @@ class _CodeDialogState extends State<_CodeDialog> { @override void initState() { super.initState(); - _snippet = context.read().getCodeSnippet( - widget.snippetUrl, - ); + _snippet = di().getCodeSnippet( + widget.snippetUrl, + ); } @override Widget build(BuildContext context) { - final model = context.watch(); + final appIsOnline = watchPropertyValue((ExampleModel m) => m.appIsOnline); return AlertDialog( titlePadding: EdgeInsets.zero, title: YaruDialogTitleBar( - title: Text(!model.appIsOnline ? 'Offline' : 'Source code'), - leading: !model.appIsOnline + title: Text(!appIsOnline ? 'Offline' : 'Source code'), + leading: !appIsOnline ? null : Center( child: YaruIconButton( @@ -88,7 +80,7 @@ class _CodeDialogState extends State<_CodeDialog> { ), ), contentPadding: EdgeInsets.zero, - content: !model.appIsOnline + content: !appIsOnline ? Center( child: Column( mainAxisSize: MainAxisSize.min, diff --git a/example/lib/common/space.dart b/example/lib/common/space.dart deleted file mode 100644 index 58983e391..000000000 --- a/example/lib/common/space.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/widgets.dart'; - -List space({ - required Iterable children, - double? widthGap, - double? heightGap, - int skip = 1, -}) => - children - .expand( - (item) sync* { - yield SizedBox( - width: widthGap, - height: heightGap, - ); - yield item; - }, - ) - .skip(skip) - .toList(); diff --git a/example/lib/example.dart b/example/lib/example.dart index a66f87d9e..1228eea4c 100644 --- a/example/lib/example.dart +++ b/example/lib/example.dart @@ -1,49 +1,24 @@ -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import 'example_dark_light_toggle_button.dart'; +import 'example_high_contrast_button.dart'; import 'example_model.dart'; import 'example_page_items.dart'; -import 'pages/icons_page/provider/icon_view_model.dart'; +import 'example_theme_button.dart'; -class Example extends StatefulWidget { - // ignore: unused_element +class Example extends StatelessWidget with WatchItMixin { const Example({super.key}); - static Widget create(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => ExampleModel(getService()), - ), - ChangeNotifierProvider( - create: (_) => IconViewModel(), - ), - ], - child: const Example(), - ); - } - - @override - State createState() => _ExampleState(); -} - -class _ExampleState extends State { - @override - void initState() { - super.initState(); - context.read().init(); - } - @override Widget build(BuildContext context) { - final model = context.watch(); + final compactMode = watchPropertyValue((ExampleModel m) => m.compactMode); + final rtl = watchPropertyValue((ExampleModel m) => m.rtl); return Directionality( - textDirection: !model.rtl ? TextDirection.ltr : TextDirection.rtl, - child: model.compactMode + textDirection: !rtl ? TextDirection.ltr : TextDirection.rtl, + child: compactMode ? _CompactPage(pageItems: examplePageItems) : _MasterDetailPage(pageItems: examplePageItems), ); @@ -72,6 +47,7 @@ class _MasterDetailPage extends StatelessWidget { tileBuilder: (context, index, selected, availableWidth) => YaruMasterTile( leading: pageItems[index].iconBuilder(context, selected), title: Text(pageItems[index].title), + subtitle: index == 0 ? const Text('Subtitle') : null, ), pageBuilder: (context, index) => YaruDetailPage( appBar: YaruWindowTitleBar( @@ -90,6 +66,23 @@ class _MasterDetailPage extends StatelessWidget { title: const Text('Yaru'), border: BorderSide.none, backgroundColor: YaruMasterDetailTheme.of(context).sideBarColor, + actions: const [ + SizedBox( + width: 5, + ), + ExampleDarkLightToggleButton(), + SizedBox( + width: 5, + ), + ExampleYaruVariantPicker(), + SizedBox( + width: 5, + ), + ExampleHighContrastButton(), + SizedBox( + width: 5, + ), + ], ), bottomBar: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -170,6 +163,13 @@ class _CompactPageState extends State<_CompactPage> { } } +void showSettingsDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => const SettingsDialog(), + ); +} + Widget? buildLeading(BuildContext context, PageItem item) { return item.leadingBuilder?.call(context); } @@ -186,49 +186,44 @@ Widget? buildFloatingActionButton(BuildContext context, PageItem item) { return item.floatingActionButtonBuilder?.call(context); } -Future showSettingsDialog(BuildContext context) { - final model = context.read(); +class SettingsDialog extends StatelessWidget with WatchItMixin { + const SettingsDialog({super.key}); - return showDialog( - context: context, - builder: (context) { - return AnimatedBuilder( - animation: model, - builder: (context, child) { - return AlertDialog( - title: const YaruDialogTitleBar( - title: Text('Settings'), + @override + Widget build(BuildContext context) { + final model = di(); + + return AlertDialog( + title: const YaruDialogTitleBar( + title: Text('Settings'), + ), + titlePadding: EdgeInsets.zero, + contentPadding: const EdgeInsets.all(kYaruPagePadding), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + YaruTile( + title: const Text('Compact mode'), + trailing: YaruSwitch( + value: watchPropertyValue((ExampleModel m) => m.compactMode), + onChanged: (v) => model.compactMode = v, ), - titlePadding: EdgeInsets.zero, - contentPadding: const EdgeInsets.all(kYaruPagePadding), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - YaruTile( - title: const Text('Compact mode'), - trailing: YaruSwitch( - value: model.compactMode, - onChanged: (v) => model.compactMode = v, - ), - ), - YaruTile( - title: const Text('RTL mode'), - trailing: YaruSwitch( - value: model.rtl, - onChanged: (v) => model.rtl = v, - ), - ), - ], + ), + YaruTile( + title: const Text('RTL mode'), + trailing: YaruSwitch( + value: watchPropertyValue((ExampleModel m) => m.rtl), + onChanged: (v) => model.rtl = v, ), - actions: [ - OutlinedButton( - onPressed: Navigator.of(context).pop, - child: const Text('Close'), - ), - ], - ); - }, - ); - }, - ); + ), + ], + ), + actions: [ + OutlinedButton( + onPressed: Navigator.of(context).pop, + child: const Text('Close'), + ), + ], + ); + } } diff --git a/example/lib/example_dark_light_toggle_button.dart b/example/lib/example_dark_light_toggle_button.dart new file mode 100644 index 000000000..bb81c678b --- /dev/null +++ b/example/lib/example_dark_light_toggle_button.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import 'example_model.dart'; + +class ExampleDarkLightToggleButton extends StatelessWidget with WatchItMixin { + const ExampleDarkLightToggleButton({super.key}); + + @override + Widget build(BuildContext context) { + final themeMode = watchPropertyValue((ExampleModel m) => m.themeMode); + + return IconButton( + tooltip: 'ThemeMode (System, Light, Dark)', + onPressed: () => di().setThemeMode( + switch (themeMode) { + ThemeMode.system => ThemeMode.light, + ThemeMode.light => ThemeMode.dark, + ThemeMode.dark => ThemeMode.system, + }, + ), + icon: switch (themeMode) { + ThemeMode.system => const Icon(YaruIcons.private_mask), + ThemeMode.light => const Icon(YaruIcons.sun), + ThemeMode.dark => const Icon(YaruIcons.clear_night), + }, + ); + } +} diff --git a/example/lib/example_high_contrast_button.dart b/example/lib/example_high_contrast_button.dart new file mode 100644 index 000000000..82250adec --- /dev/null +++ b/example/lib/example_high_contrast_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import 'example_model.dart'; + +class ExampleHighContrastButton extends StatelessWidget with WatchItMixin { + const ExampleHighContrastButton({super.key}); + + @override + Widget build(BuildContext context) { + final highContrast = + watchPropertyValue((ExampleModel m) => m.forceHighContrast) || + Theme.of(context).colorScheme.isHighContrast == true; + + return IconButton( + tooltip: 'Force HighContrast mode', + onPressed: di().toggleForceHighContrast, + icon: Icon( + highContrast ? YaruIcons.eye_filled : YaruIcons.eye, + ), + ); + } +} diff --git a/example/lib/example_home.dart b/example/lib/example_home.dart new file mode 100644 index 000000000..d8798a5dc --- /dev/null +++ b/example/lib/example_home.dart @@ -0,0 +1,88 @@ +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import 'example.dart'; +import 'example_model.dart'; + +class ExampleHome extends StatelessWidget with WatchItMixin { + const ExampleHome({super.key}); + + @override + Widget build(BuildContext context) { + final themeMode = watchPropertyValue((ExampleModel m) => m.themeMode); + final yaruVariant = watchPropertyValue((ExampleModel m) => m.yaruVariant); + final forceHighContrast = + watchPropertyValue((ExampleModel m) => m.forceHighContrast); + + if (Platform.isLinux) { + return YaruTheme( + builder: (context, yaru, child) => _ExampleHome( + themeMode: themeMode, + lightTheme: forceHighContrast + ? yaruHighContrastLight + : yaruVariant?.theme ?? yaru.theme, + darkTheme: forceHighContrast + ? yaruHighContrastDark + : yaruVariant?.darkTheme ?? yaru.darkTheme, + highContrastTheme: yaruHighContrastLight, + highContrastDarkTheme: yaruHighContrastDark, + ), + ); + } + + return _ExampleHome( + themeMode: themeMode, + lightTheme: forceHighContrast + ? yaruHighContrastLight + : yaruVariant?.theme ?? yaruLight, + darkTheme: forceHighContrast + ? yaruHighContrastDark + : yaruVariant?.darkTheme ?? yaruDark, + highContrastTheme: yaruHighContrastLight, + highContrastDarkTheme: yaruHighContrastDark, + ); + } +} + +class _ExampleHome extends StatelessWidget { + const _ExampleHome({ + required this.themeMode, + required this.lightTheme, + required this.darkTheme, + required this.highContrastTheme, + required this.highContrastDarkTheme, + }); + + final ThemeData? lightTheme; + final ThemeData? darkTheme; + final ThemeData? highContrastTheme; + final ThemeData? highContrastDarkTheme; + final ThemeMode themeMode; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Yaru', + debugShowCheckedModeBanner: false, + theme: lightTheme, + themeMode: themeMode, + darkTheme: darkTheme, + highContrastTheme: highContrastDarkTheme, + highContrastDarkTheme: highContrastDarkTheme, + home: const Example(), + scrollBehavior: const MaterialScrollBehavior().copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.unknown, + PointerDeviceKind.trackpad, + }, + ), + ); + } +} diff --git a/example/lib/example_model.dart b/example/lib/example_model.dart index 786d545a1..37b70f42d 100644 --- a/example/lib/example_model.dart +++ b/example/lib/example_model.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:safe_change_notifier/safe_change_notifier.dart'; +import 'package:yaru/yaru.dart'; class ExampleModel extends SafeChangeNotifier { ExampleModel( @@ -14,6 +16,29 @@ class ExampleModel extends SafeChangeNotifier { List _connectivityResult = [ConnectivityResult.wifi]; List get state => _connectivityResult; + ThemeMode _themeMode = ThemeMode.system; + ThemeMode get themeMode => _themeMode; + void setThemeMode(ThemeMode value) { + if (value == _themeMode) return; + _themeMode = value; + notifyListeners(); + } + + bool _forceHighContrast = false; + bool get forceHighContrast => _forceHighContrast; + void toggleForceHighContrast() { + _forceHighContrast = !_forceHighContrast; + notifyListeners(); + } + + YaruVariant? _yaruVariant; + YaruVariant? get yaruVariant => _yaruVariant; + void setYaruVariant(YaruVariant value) { + if (value == _yaruVariant) return; + _yaruVariant = value; + notifyListeners(); + } + bool _compactMode = false; bool get compactMode => _compactMode; set compactMode(bool value) { @@ -30,9 +55,7 @@ class ExampleModel extends SafeChangeNotifier { notifyListeners(); } - Future init() async { - await initConnectivity(); - } + Future init() async => initConnectivity(); @override void dispose() { diff --git a/example/lib/example_page_items.dart b/example/lib/example_page_items.dart index dd439b190..b4f3212af 100644 --- a/example/lib/example_page_items.dart +++ b/example/lib/example_page_items.dart @@ -33,7 +33,7 @@ import 'pages/selectable_container_page.dart'; import 'pages/split_button_page.dart'; import 'pages/switch_page.dart'; import 'pages/tab_bar_page.dart'; -import 'pages/theme_page/theme_page.dart'; +import 'pages/theme_page/home.dart'; import 'pages/tile_page.dart'; import 'pages/window_controls_page.dart'; @@ -327,12 +327,11 @@ final examplePageItems = [ ), PageItem( title: 'YaruIcons', - titleBuilder: createIconsPageAppBarTitle, - actionsBuilder: createIconsPageAppBarActions, - floatingActionButtonBuilder: createIconsPageFloatingActionButton, - pageBuilder: (context) { - return const IconsPage(); - }, + titleBuilder: (context) => const IconsPageAppBarTitle(), + actionsBuilder: (context) => [const IconsSearchIcon()], + floatingActionButtonBuilder: (context) => + const IconsPageFloatingActionButton(), + pageBuilder: (context) => const IconsPage(), iconBuilder: (context, selected) => selected ? const Icon(YaruIcons.placeholder_icon_filled) : const Icon(YaruIcons.placeholder_icon), @@ -349,9 +348,7 @@ final examplePageItems = [ ), PageItem( title: 'Material Components, using Yaru Material Themes', - pageBuilder: (context) { - return const ThemePage(); - }, + pageBuilder: (context) => const MaterialThemeHomePage(), iconBuilder: (context, selected) => selected ? const Icon(YaruIcons.colors_filled) : const Icon(YaruIcons.colors), diff --git a/example/lib/example_theme_button.dart b/example/lib/example_theme_button.dart new file mode 100644 index 000000000..f6d9ca990 --- /dev/null +++ b/example/lib/example_theme_button.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; +import 'example_model.dart'; +import 'pages/theme_page/src/home/color_disk.dart'; + +class ExampleYaruVariantPicker extends StatelessWidget with WatchItMixin { + const ExampleYaruVariantPicker({super.key}); + + @override + Widget build(BuildContext context) { + final model = di(); + final yaruVariant = watchPropertyValue((ExampleModel m) => m.yaruVariant); + + return PopupMenuButton( + tooltip: 'Pick a YaruVariant', + padding: EdgeInsets.zero, + icon: Icon( + YaruIcons.color_select, + color: Theme.of(context).primaryColor, + ), + itemBuilder: (context) { + return [ + for (final variant in YaruVariant.values) // skip flavors + PopupMenuItem( + onTap: () => model.setYaruVariant(variant), + child: Row( + children: [ + ColorDisk( + color: variant.color, + selected: variant == yaruVariant, + ), + Text(variant.name), + ], + ), + ), + ]; + }, + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 093be2dc2..3f2c5d229 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,12 +1,12 @@ import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; -import 'example.dart'; -import 'theme.dart'; +import 'example_home.dart'; +import 'example_model.dart'; +import 'pages/icons_page/provider/icon_view_model.dart'; Future main() async { await YaruWindowTitleBar.ensureInitialized(); @@ -14,43 +14,15 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); SemanticsBinding.instance.ensureSemantics(); - registerService(Connectivity.new); - runApp( - InheritedYaruVariant( - child: const Home(), - ), - ); -} + di + ..registerLazySingleton(Connectivity.new) + ..registerLazySingleton(IconViewModel.new) + ..registerLazySingleton( + () => ExampleModel(di()), + dispose: (m) => m.dispose(), + ); -class Home extends StatelessWidget { - const Home({super.key}); + await di().init(); - @override - Widget build(BuildContext context) { - return YaruTheme( - data: YaruThemeData( - variant: InheritedYaruVariant.of(context), - ), - builder: (context, yaru, child) { - return MaterialApp( - title: 'Yaru', - debugShowCheckedModeBanner: false, - theme: yaru.theme, - darkTheme: yaru.darkTheme, - highContrastTheme: yaruHighContrastLight, - highContrastDarkTheme: yaruHighContrastDark, - home: Example.create(context), - scrollBehavior: const MaterialScrollBehavior().copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.stylus, - PointerDeviceKind.unknown, - PointerDeviceKind.trackpad, - }, - ), - ); - }, - ); - } + runApp(const ExampleHome()); } diff --git a/example/lib/pages/color_disk_page.dart b/example/lib/pages/color_disk_page.dart index 9b1f1c33a..66e64c7b5 100644 --- a/example/lib/pages/color_disk_page.dart +++ b/example/lib/pages/color_disk_page.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; -import '../theme.dart'; +import '../example_model.dart'; -class ColorDiskPage extends StatefulWidget { +class ColorDiskPage extends StatelessWidget with WatchItMixin { const ColorDiskPage({super.key}); - @override - State createState() => _ColorDiskPageState(); -} - -class _ColorDiskPageState extends State { @override Widget build(BuildContext context) { return SingleChildScrollView( @@ -21,9 +17,10 @@ class _ColorDiskPageState extends State { children: [ for (final variant in YaruVariant.accents) YaruColorDisk( - onPressed: () => InheritedYaruVariant.apply(context, variant), + onPressed: () => di().setYaruVariant(variant), color: variant.color, - selected: YaruTheme.of(context).variant == variant, + selected: watchPropertyValue((ExampleModel m) => m.yaruVariant) == + variant, ), ], ), diff --git a/example/lib/pages/icons_page/icon_view.dart b/example/lib/pages/icons_page/icon_view.dart index 4a0812ab6..d977bfeaa 100644 --- a/example/lib/pages/icons_page/icon_view.dart +++ b/example/lib/pages/icons_page/icon_view.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:watch_it/watch_it.dart'; import 'common/icon_grid.dart'; import 'common/icon_table.dart'; import 'icon_items.dart'; import 'provider/icon_view_model.dart'; -class IconView extends StatelessWidget { +class IconView extends StatelessWidget with WatchItMixin { const IconView({ super.key, required this.iconItems, @@ -17,11 +17,10 @@ class IconView extends StatelessWidget { @override Widget build(BuildContext context) { final searchActive = - context.select((m) => m.searchActive); - final searchQuery = - context.select((m) => m.searchQuery); - final gridView = context.select((m) => m.gridView); - final iconSize = context.select((m) => m.iconSize); + watchPropertyValue((IconViewModel m) => m.searchActive); + final searchQuery = watchPropertyValue((IconViewModel m) => m.searchQuery); + final gridView = watchPropertyValue((IconViewModel m) => m.gridView); + final iconSize = watchPropertyValue((IconViewModel m) => m.iconSize); final localIconItems = searchActive ? iconItems.where((iconItem) { diff --git a/example/lib/pages/icons_page/icons_page.dart b/example/lib/pages/icons_page/icons_page.dart index 07c565634..48d9905b7 100644 --- a/example/lib/pages/icons_page/icons_page.dart +++ b/example/lib/pages/icons_page/icons_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import 'icon_items.dart'; @@ -9,22 +9,13 @@ import 'provider/icon_view_model.dart'; class IconsPage extends StatefulWidget { const IconsPage({super.key}); - static Widget create({ - required BuildContext context, - }) { - return ChangeNotifierProvider( - create: (_) => IconViewModel(), - child: const IconsPage(), - ); - } - @override State createState() => _IconsPageState(); } class _IconsPageState extends State with SingleTickerProviderStateMixin { - int index = 0; + int _index = 0; @override Widget build(BuildContext context) { @@ -41,7 +32,7 @@ class _IconsPageState extends State child: SizedBox( width: 700, child: YaruTabBar( - onTap: (value) => setState(() => index = value), + onTap: (value) => setState(() => _index = value), tabs: const [ Tab( text: 'Static', @@ -58,7 +49,7 @@ class _IconsPageState extends State ), Expanded( child: IconView( - iconItems: switch (index) { + iconItems: switch (_index) { 0 => IconItems.static, 1 => IconItems.animated, 2 => IconItems.widget, @@ -72,79 +63,88 @@ class _IconsPageState extends State } } -List createIconsPageAppBarActions(BuildContext context) { - final model = context.read(); - final searchActive = - context.select((m) => m.searchActive); +class IconsSearchIcon extends StatelessWidget with WatchItMixin { + const IconsSearchIcon({super.key}); - return [ - YaruSearchButton( - searchActive: searchActive, - onPressed: model.toggleSearch, - ), - ]; + @override + Widget build(BuildContext context) { + return YaruSearchButton( + searchActive: watchPropertyValue((IconViewModel m) => m.searchActive), + onPressed: di().toggleSearch, + ); + } } -Widget createIconsPageAppBarTitle(BuildContext context) { - final model = context.read(); - final searchActive = - context.select((m) => m.searchActive); +class IconsPageAppBarTitle extends StatelessWidget with WatchItMixin { + const IconsPageAppBarTitle({super.key}); - return searchActive - ? SizedBox( - width: 250, - child: YaruSearchField( - onClear: model.toggleSearch, - onChanged: model.onSearchChanged, - ), - ) - : const Text('YaruIcons'); + @override + Widget build(BuildContext context) { + final model = di(); + final searchActive = + watchPropertyValue((IconViewModel m) => m.searchActive); + + return searchActive + ? SizedBox( + width: 250, + child: YaruSearchField( + onClear: model.toggleSearch, + onChanged: model.onSearchChanged, + ), + ) + : const Text('YaruIcons'); + } } -Widget createIconsPageFloatingActionButton(BuildContext context) { - final model = context.read(); - final iconSize = context.select((m) => m.iconSize); - final isMinIconSize = - context.select((m) => m.isMinIconSize); - final isMaxIconSize = - context.select((m) => m.isMaxIconSize); - final gridView = context.select((m) => m.gridView); +class IconsPageFloatingActionButton extends StatelessWidget with WatchItMixin { + const IconsPageFloatingActionButton({super.key}); - return Material( - elevation: 2, - borderRadius: BorderRadius.circular(kYaruContainerRadius), - child: YaruBorderContainer( - padding: const EdgeInsets.all(10), - color: Theme.of(context).colorScheme.surface, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Tooltip( - message: gridView ? 'Toggle list view' : 'Toggle grid view', - child: IconButton( - onPressed: model.toggleGridView, - icon: gridView - ? const Icon(YaruIcons.unordered_list) - : const Icon(YaruIcons.app_grid), + @override + Widget build(BuildContext context) { + final model = di(); + final iconSize = watchPropertyValue((IconViewModel m) => m.iconSize); + final isMinIconSize = + watchPropertyValue((IconViewModel m) => m.isMinIconSize); + final isMaxIconSize = + watchPropertyValue((IconViewModel m) => m.isMaxIconSize); + final gridView = watchPropertyValue((IconViewModel m) => m.gridView); + + return Material( + elevation: 2, + borderRadius: BorderRadius.circular(kYaruContainerRadius), + child: YaruBorderContainer( + padding: const EdgeInsets.all(10), + color: Theme.of(context).colorScheme.surface, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Tooltip( + message: gridView ? 'Toggle list view' : 'Toggle grid view', + child: IconButton( + onPressed: model.toggleGridView, + icon: gridView + ? const Icon(YaruIcons.unordered_list) + : const Icon(YaruIcons.app_grid), + ), ), - ), - Tooltip( - message: 'Decrease icon size', - child: IconButton( - onPressed: isMinIconSize ? null : model.decreaseIconSize, - icon: const Icon(YaruIcons.minus), + Tooltip( + message: 'Decrease icon size', + child: IconButton( + onPressed: isMinIconSize ? null : model.decreaseIconSize, + icon: const Icon(YaruIcons.minus), + ), ), - ), - Text('${iconSize.truncate()}px'), - Tooltip( - message: 'Increase icon size', - child: IconButton( - onPressed: isMaxIconSize ? null : model.increaseIconSize, - icon: const Icon(YaruIcons.plus), + Text('${iconSize.truncate()}px'), + Tooltip( + message: 'Increase icon size', + child: IconButton( + onPressed: isMaxIconSize ? null : model.increaseIconSize, + icon: const Icon(YaruIcons.plus), + ), ), - ), - ], + ], + ), ), - ), - ); + ); + } } diff --git a/example/lib/pages/theme_page/src/controls/fabs.dart b/example/lib/pages/theme_page/src/controls/fabs.dart index c5ab127f6..91676cf41 100644 --- a/example/lib/pages/theme_page/src/controls/fabs.dart +++ b/example/lib/pages/theme_page/src/controls/fabs.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; +import '../../../../utils.dart'; import '../constants.dart'; -import '../utils.dart'; class Fabs extends StatelessWidget { const Fabs({super.key}); diff --git a/example/lib/pages/theme_page/src/home/home_page.dart b/example/lib/pages/theme_page/src/home/home_page.dart index 40db2fb03..de9a49612 100644 --- a/example/lib/pages/theme_page/src/home/home_page.dart +++ b/example/lib/pages/theme_page/src/home/home_page.dart @@ -1,27 +1,24 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; +import '../../../../utils.dart'; import '../../colors.dart'; import '../../containers.dart'; import '../../controls.dart'; import '../../fonts.dart'; import '../../textfields.dart'; -import '../../theme_page.dart'; -import '../constants.dart'; -import '../utils.dart'; -import 'color_disk.dart'; final GlobalKey themePageScaffoldKey = GlobalKey(); -class HomePage extends StatefulWidget { - const HomePage({super.key}); +class MaterialThemeHomePage extends StatefulWidget { + const MaterialThemeHomePage({super.key}); @override - HomePageState createState() => HomePageState(); + MaterialThemeHomePageState createState() => MaterialThemeHomePageState(); } -class HomePageState extends State { - HomePageState(); +class MaterialThemeHomePageState extends State { + MaterialThemeHomePageState(); int _selectedIndex = 0; @@ -79,16 +76,12 @@ class HomePageState extends State { icon: const Icon(YaruIcons.menu), ), ), - title: const _Title(), + title: const Text(''), actions: [ IconButton( onPressed: () => showSnack(context), icon: const Icon(YaruIcons.plus), ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: kWrapSpacing), - child: _ThemeButton(), - ), ], ), body: LayoutBuilder( @@ -154,80 +147,6 @@ class HomePageState extends State { } } -class _ThemeButton extends StatelessWidget { - const _ThemeButton(); - - @override - Widget build(BuildContext context) { - final theme = YaruTheme.of(context); - final light = theme.themeMode == ThemeMode.light; - - return PopupMenuButton( - padding: EdgeInsets.zero, - icon: Icon( - YaruIcons.color_select, - color: Theme.of(context).primaryColor, - ), - itemBuilder: (context) { - return [ - PopupMenuItem( - onTap: () => AppTheme.apply(context, highContrast: true), - child: Row( - children: [ - ColorDisk( - color: light ? Colors.black : Colors.white, - selected: theme.highContrast == true, - ), - const Text('highContrast'), - ], - ), - ), - for (final variant in YaruVariant.values) // skip flavors - PopupMenuItem( - onTap: () => AppTheme.apply( - context, - variant: variant, - highContrast: false, - ), - child: Row( - children: [ - ColorDisk( - color: variant.color, - selected: - variant == theme.variant && theme.highContrast != true, - ), - Text(variant.name), - ], - ), - ), - ]; - }, - ); - } -} - -class _Title extends StatelessWidget { - const _Title(); - - @override - Widget build(BuildContext context) { - final theme = YaruTheme.of(context); - final light = theme.themeMode == ThemeMode.light; - - return IconButton( - onPressed: () { - return AppTheme.apply( - context, - themeMode: light ? ThemeMode.dark : ThemeMode.light, - ); - }, - icon: Icon( - light ? YaruIcons.sun_filled : YaruIcons.clear_night_filled, - ), - ); - } -} - class _Drawer extends StatelessWidget { const _Drawer({ required this.items, @@ -243,7 +162,7 @@ class _Drawer extends StatelessWidget { Widget build(BuildContext context) { return Drawer( child: ListView( - padding: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(horizontal: 8), children: [ const DrawerHeader( child: Text('Drawer Header'), diff --git a/example/lib/pages/theme_page/theme_page.dart b/example/lib/pages/theme_page/theme_page.dart deleted file mode 100644 index 1f34c97d1..000000000 --- a/example/lib/pages/theme_page/theme_page.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:yaru/yaru.dart'; - -import 'home.dart'; - -class ThemePage extends StatelessWidget { - const ThemePage({super.key}); - - @override - Widget build(BuildContext context) { - return Builder( - builder: (context) => YaruTheme( - data: AppTheme.of(context), - child: const HomePage(), - ), - ); - } -} - -class AppTheme { - static YaruThemeData of(BuildContext context) { - return SharedAppData.getValue( - context, - 'theme', - () => const YaruThemeData(), - ); - } - - static void apply( - BuildContext context, { - YaruVariant? variant, - bool? highContrast, - ThemeMode? themeMode, - }) { - SharedAppData.setValue( - context, - 'theme', - AppTheme.of(context).copyWith( - themeMode: themeMode, - variant: variant, - highContrast: highContrast, - ), - ); - } -} diff --git a/example/lib/theme.dart b/example/lib/theme.dart deleted file mode 100644 index ce2aa89a0..000000000 --- a/example/lib/theme.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:yaru/yaru.dart'; - -class InheritedYaruVariant - extends InheritedNotifier> { - InheritedYaruVariant({ - super.key, - required super.child, - }) : super(notifier: ValueNotifier(null)); - - static YaruVariant? of(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType()! - .notifier! - .value; - } - - static void apply(BuildContext context, YaruVariant variant) { - context - .findAncestorWidgetOfExactType()! - .notifier! - .value = variant; - } -} diff --git a/example/lib/pages/theme_page/src/utils.dart b/example/lib/utils.dart similarity index 54% rename from example/lib/pages/theme_page/src/utils.dart rename to example/lib/utils.dart index 6919bc6e9..94c2968bc 100644 --- a/example/lib/pages/theme_page/src/utils.dart +++ b/example/lib/utils.dart @@ -13,3 +13,18 @@ ScaffoldFeatureController showSnack( ), ); } + +List space({ + double widthGap = 5, + double heightGap = 5, + required Iterable children, +}) => + children + .expand( + (item) sync* { + yield SizedBox(width: widthGap); + yield item; + }, + ) + .skip(1) + .toList(); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0f9b844ed..73ca9eb33 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,11 +16,10 @@ dependencies: handy_window: ^0.4.0 http: ^1.2.2 path: ^1.9.0 - provider: ^6.1.2 safe_change_notifier: ^0.3.2 - ubuntu_service: ^0.3.2 universal_html: ^2.2.4 url_launcher: ^6.3.0 + watch_it: ^1.5.1 yaru: path: ../ diff --git a/lib/src/themes/common_themes.dart b/lib/src/themes/common_themes.dart index c31b1db48..7cc5d8ab8 100644 --- a/lib/src/themes/common_themes.dart +++ b/lib/src/themes/common_themes.dart @@ -797,7 +797,7 @@ ListTileThemeData _createListTileTheme(ColorScheme colorScheme) { iconColor: colorScheme.onSurface.withOpacity(0.8), selectedTileColor: isHighContrast ? colorScheme.inverseSurface - : colorScheme.onSurface.withOpacity(0.08), + : colorScheme.onSurface.withOpacity(colorScheme.isDark ? 0.035 : 0.04), minVerticalPadding: 6, visualDensity: const VisualDensity(horizontal: -4, vertical: -4), shape: const RoundedRectangleBorder( diff --git a/lib/src/widgets/master_detail/yaru_master_tile.dart b/lib/src/widgets/master_detail/yaru_master_tile.dart index 6f2b470f2..c5ff4c21f 100644 --- a/lib/src/widgets/master_detail/yaru_master_tile.dart +++ b/lib/src/widgets/master_detail/yaru_master_tile.dart @@ -41,41 +41,50 @@ class YaruMasterTile extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final listTileTheme = theme.listTileTheme; final scope = YaruMasterTileScope.maybeOf(context); final isSelected = selected ?? scope?.selected ?? false; final scrollbarThicknessWithTrack = _calcScrollbarThicknessWithTrack(context); - return Padding( - padding: EdgeInsets.symmetric(horizontal: scrollbarThicknessWithTrack), - child: AnimatedContainer( - duration: _kSelectedTileAnimationDuration, - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(kYaruButtonRadius)), - color: - isSelected ? theme.colorScheme.onSurface.withOpacity(0.07) : null, - ), - child: ListTile( - leading: leading, - title: _titleStyle(context, title), - subtitle: _subTitleStyle(context, subtitle), - trailing: trailing, - selected: isSelected, - onTap: () { - if (onTap != null) { - onTap!.call(); - } else { - scope?.onTap(); - } - }, + final backgroundColor = + isSelected ? listTileTheme.selectedTileColor : listTileTheme.tileColor; + + final foregroundColor = + isSelected ? listTileTheme.selectedColor : listTileTheme.textColor; + + return Material( + color: Colors.transparent, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: scrollbarThicknessWithTrack), + child: AnimatedContainer( + duration: _kSelectedTileAnimationDuration, + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(kYaruButtonRadius)), + color: backgroundColor, + ), + child: ListTile( + leading: leading, + title: _titleStyle(title, foregroundColor), + subtitle: _subTitleStyle(subtitle, foregroundColor), + trailing: trailing, + selected: isSelected, + onTap: () { + if (onTap != null) { + onTap!.call(); + } else { + scope?.onTap(); + } + }, + ), ), ), ); } - Widget? _titleStyle(BuildContext context, Widget? child) { + Widget? _titleStyle(Widget? child, Color? color) { if (child == null) { return child; } @@ -84,11 +93,11 @@ class YaruMasterTile extends StatelessWidget { child: child, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(color: color), ); } - Widget? _subTitleStyle(BuildContext context, Widget? child) { + Widget? _subTitleStyle(Widget? child, Color? color) { if (child == null) { return child; } @@ -97,7 +106,7 @@ class YaruMasterTile extends StatelessWidget { child: child, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color), + style: TextStyle(color: color), ); }