diff --git a/app/assets/2.0x/ic_custom_color_56dp.png b/app/assets/2.0x/ic_custom_color_56dp.png new file mode 100644 index 00000000..9e98aad3 Binary files /dev/null and b/app/assets/2.0x/ic_custom_color_56dp.png differ diff --git a/app/assets/3.0x/ic_custom_color_56dp.png b/app/assets/3.0x/ic_custom_color_56dp.png new file mode 100644 index 00000000..6b38c827 Binary files /dev/null and b/app/assets/3.0x/ic_custom_color_56dp.png differ diff --git a/app/assets/ic_custom_color_56dp.png b/app/assets/ic_custom_color_56dp.png new file mode 100644 index 00000000..8ee8bc48 Binary files /dev/null and b/app/assets/ic_custom_color_56dp.png differ diff --git a/app/lib/widget/settings.dart b/app/lib/widget/settings.dart index 18b95591..0fe93277 100644 --- a/app/lib/widget/settings.dart +++ b/app/lib/widget/settings.dart @@ -13,7 +13,6 @@ import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; -import 'package:nc_photos/mobile/android/android_info.dart'; import 'package:nc_photos/mobile/platform.dart' if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; import 'package:nc_photos/platform/features.dart' as features; @@ -22,13 +21,13 @@ import 'package:nc_photos/platform/notification.dart'; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/service.dart'; import 'package:nc_photos/snack_bar_manager.dart'; -import 'package:nc_photos/theme.dart'; import 'package:nc_photos/url_launcher_util.dart'; import 'package:nc_photos/widget/fancy_option_picker.dart'; import 'package:nc_photos/widget/gps_map.dart'; import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/list_tile_center_leading.dart'; import 'package:nc_photos/widget/root_picker.dart'; +import 'package:nc_photos/widget/settings/theme_settings.dart'; import 'package:nc_photos/widget/share_folder_picker.dart'; import 'package:nc_photos/widget/simple_input_dialog.dart'; import 'package:nc_photos/widget/stateful_slider.dart'; @@ -166,7 +165,7 @@ class _SettingsState extends State { leading: const Icon(Icons.palette_outlined), label: L10n.global().settingsThemeTitle, description: L10n.global().settingsThemeDescription, - builder: () => _ThemeSettings(), + builder: () => const ThemeSettings(), ), _buildSubSettings( context, @@ -1463,216 +1462,6 @@ class _EnhanceResolutionSliderState extends State<_EnhanceResolutionSlider> { late int _height; } -class _ThemeSettings extends StatefulWidget { - @override - createState() => _ThemeSettingsState(); -} - -class _ThemeSettingsState extends State<_ThemeSettings> { - @override - initState() { - super.initState(); - _isFollowSystemTheme = Pref().isFollowSystemThemeOr(false); - _isUseBlackInDarkTheme = Pref().isUseBlackInDarkThemeOr(false); - _seedColor = getSeedColor(); - } - - @override - build(BuildContext context) { - return Scaffold( - body: Builder( - builder: (context) => _buildContent(context), - ), - ); - } - - Widget _buildContent(BuildContext context) { - return CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - title: Text(L10n.global().settingsThemeTitle), - ), - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text(L10n.global().settingsSeedColorTitle), - trailing: Icon( - Icons.circle, - size: 32, - color: _seedColor, - ), - onTap: () => _onSeedColorPressed(context), - ), - if (platform_k.isAndroid && - AndroidInfo().sdkInt >= AndroidVersion.Q) - SwitchListTile( - title: Text(L10n.global().settingsFollowSystemThemeTitle), - value: _isFollowSystemTheme, - onChanged: (value) => _onFollowSystemThemeChanged(value), - ), - SwitchListTile( - title: Text(L10n.global().settingsUseBlackInDarkThemeTitle), - subtitle: Text(_isUseBlackInDarkTheme - ? L10n.global().settingsUseBlackInDarkThemeTrueDescription - : L10n.global() - .settingsUseBlackInDarkThemeFalseDescription), - value: _isUseBlackInDarkTheme, - onChanged: (value) => - _onUseBlackInDarkThemeChanged(context, value), - ), - ], - ), - ), - ], - ); - } - - Future _onFollowSystemThemeChanged(bool value) async { - final oldValue = _isFollowSystemTheme; - setState(() { - _isFollowSystemTheme = value; - }); - if (await Pref().setFollowSystemTheme(value)) { - KiwiContainer().resolve().fire(ThemeChangedEvent()); - } else { - _log.severe("[_onFollowSystemThemeChanged] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _isFollowSystemTheme = oldValue; - }); - } - } - - Future _onUseBlackInDarkThemeChanged( - BuildContext context, bool value) async { - final oldValue = _isUseBlackInDarkTheme; - setState(() { - _isUseBlackInDarkTheme = value; - }); - if (await Pref().setUseBlackInDarkTheme(value)) { - if (Theme.of(context).brightness == Brightness.dark) { - KiwiContainer().resolve().fire(ThemeChangedEvent()); - } - } else { - _log.severe("[_onUseBlackInDarkThemeChanged] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _isUseBlackInDarkTheme = oldValue; - }); - } - } - - Future _onSeedColorPressed(BuildContext context) async { - final result = await showDialog( - context: context, - builder: (context) => const _SeedColorPicker(), - ); - if (result == null) { - return; - } - - final oldValue = _seedColor; - setState(() { - _seedColor = result; - }); - if (await Pref().setSeedColor(result.withAlpha(0xFF).value)) { - KiwiContainer().resolve().fire(ThemeChangedEvent()); - } else { - _log.severe("[_onSeedColorPressed] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _seedColor = oldValue; - }); - } - } - - late bool _isFollowSystemTheme; - late bool _isUseBlackInDarkTheme; - late Color _seedColor; - - static final _log = Logger("widget.settings._ThemeSettingsState"); -} - -class _SeedColorPicker extends StatefulWidget { - const _SeedColorPicker(); - - @override - State createState() => _SeedColorPickerState(); -} - -class _SeedColorPickerState extends State<_SeedColorPicker> { - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(L10n.global().settingsSeedColorPickerTitle), - content: Wrap( - children: const [ - Color(0xFFF44336), - Color(0xFF9C27B0), - Color(0xFF2196F3), - Color(0xFF4CAF50), - Color(0xFFFFC107), - ] - .map((c) => _SeedColorPickerItem( - seedColor: c, - onSelected: () => _onItemSelected(context, c), - )) - .toList(), - ), - ); - } - - void _onItemSelected(BuildContext context, Color seedColor) { - Navigator.of(context).pop(seedColor); - } -} - -class _SeedColorPickerItem extends StatelessWidget { - const _SeedColorPickerItem({ - required this.seedColor, - this.onSelected, - }); - - @override - Widget build(BuildContext context) { - final content = SizedBox.square( - dimension: _size, - child: Center( - child: Icon( - Icons.circle, - size: _size - _size * .1, - color: seedColor, - ), - ), - ); - if (onSelected != null) { - return InkWell( - customBorder: const CircleBorder(), - onTap: onSelected, - child: content, - ); - } else { - return content; - } - } - - final Color seedColor; - final VoidCallback? onSelected; - - static const _size = 56.0; -} - class _MiscSettings extends StatefulWidget { const _MiscSettings({Key? key}) : super(key: key); diff --git a/app/lib/widget/settings/theme_settings.dart b/app/lib/widget/settings/theme_settings.dart new file mode 100644 index 00000000..105cd01b --- /dev/null +++ b/app/lib/widget/settings/theme_settings.dart @@ -0,0 +1,377 @@ +import 'package:event_bus/event_bus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:kiwi/kiwi.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/event/event.dart'; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/mobile/android/android_info.dart'; +import 'package:nc_photos/platform/k.dart' as platform_k; +import 'package:nc_photos/pref.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/theme.dart'; + +class ThemeSettings extends StatefulWidget { + const ThemeSettings({super.key}); + + @override + createState() => _ThemeSettingsState(); +} + +class _ThemeSettingsState extends State { + @override + initState() { + super.initState(); + _isFollowSystemTheme = Pref().isFollowSystemThemeOr(false); + _isUseBlackInDarkTheme = Pref().isUseBlackInDarkThemeOr(false); + _seedColor = getSeedColor(); + } + + @override + build(BuildContext context) { + return Scaffold( + body: Builder( + builder: (context) => _buildContent(context), + ), + ); + } + + Widget _buildContent(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(L10n.global().settingsThemeTitle), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text(L10n.global().settingsSeedColorTitle), + trailing: Icon( + Icons.circle, + size: 32, + color: _seedColor, + ), + onTap: () => _onSeedColorPressed(context), + ), + if (platform_k.isAndroid && + AndroidInfo().sdkInt >= AndroidVersion.Q) + SwitchListTile( + title: Text(L10n.global().settingsFollowSystemThemeTitle), + value: _isFollowSystemTheme, + onChanged: (value) => _onFollowSystemThemeChanged(value), + ), + SwitchListTile( + title: Text(L10n.global().settingsUseBlackInDarkThemeTitle), + subtitle: Text(_isUseBlackInDarkTheme + ? L10n.global().settingsUseBlackInDarkThemeTrueDescription + : L10n.global() + .settingsUseBlackInDarkThemeFalseDescription), + value: _isUseBlackInDarkTheme, + onChanged: (value) => + _onUseBlackInDarkThemeChanged(context, value), + ), + ], + ), + ), + ], + ); + } + + Future _onFollowSystemThemeChanged(bool value) async { + final oldValue = _isFollowSystemTheme; + setState(() { + _isFollowSystemTheme = value; + }); + if (await Pref().setFollowSystemTheme(value)) { + KiwiContainer().resolve().fire(ThemeChangedEvent()); + } else { + _log.severe("[_onFollowSystemThemeChanged] Failed writing pref"); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _isFollowSystemTheme = oldValue; + }); + } + } + + Future _onUseBlackInDarkThemeChanged( + BuildContext context, bool value) async { + final oldValue = _isUseBlackInDarkTheme; + setState(() { + _isUseBlackInDarkTheme = value; + }); + if (await Pref().setUseBlackInDarkTheme(value)) { + if (Theme.of(context).brightness == Brightness.dark) { + KiwiContainer().resolve().fire(ThemeChangedEvent()); + } + } else { + _log.severe("[_onUseBlackInDarkThemeChanged] Failed writing pref"); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _isUseBlackInDarkTheme = oldValue; + }); + } + } + + Future _onSeedColorPressed(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (context) => const _SeedColorPicker(), + ); + if (result == null) { + return; + } + + final oldValue = _seedColor; + setState(() { + _seedColor = result; + }); + if (await Pref().setSeedColor(result.withAlpha(0xFF).value)) { + KiwiContainer().resolve().fire(ThemeChangedEvent()); + } else { + _log.severe("[_onSeedColorPressed] Failed writing pref"); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _seedColor = oldValue; + }); + } + } + + late bool _isFollowSystemTheme; + late bool _isUseBlackInDarkTheme; + late Color _seedColor; + + static final _log = + Logger("widget.settings.theme_settings._ThemeSettingsState"); +} + +class _SeedColorPicker extends StatefulWidget { + const _SeedColorPicker(); + + @override + State createState() => _SeedColorPickerState(); +} + +class _SeedColorPickerState extends State<_SeedColorPicker> { + @override + Widget build(BuildContext context) { + return Visibility( + visible: _isVisible, + child: AlertDialog( + title: Text(L10n.global().settingsSeedColorPickerTitle), + content: Wrap( + children: const [ + Color(0xFFF44336), + Color(0xFF9C27B0), + Color(0xFF2196F3), + Color(0xFF4CAF50), + Color(0xFFFFC107), + null, + ] + .map((c) => _SeedColorPickerItem( + seedColor: c, + onSelected: () => _onItemSelected(context, c), + )) + .toList(), + ), + ), + ); + } + + Future _onItemSelected(BuildContext context, Color? seedColor) async { + if (seedColor != null) { + Navigator.of(context).pop(seedColor); + return; + } + setState(() { + _isVisible = false; + }); + final color = await showDialog( + context: context, + builder: (context) => const _SeedColorCustomPicker(), + barrierColor: Colors.transparent, + ); + Navigator.of(context).pop(color); + } + + var _isVisible = true; +} + +class _SeedColorCustomPicker extends StatefulWidget { + const _SeedColorCustomPicker(); + + @override + State createState() => _SeedColorCustomPickerState(); +} + +class _SeedColorCustomPickerState extends State<_SeedColorCustomPicker> { + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(L10n.global().settingsSeedColorPickerTitle), + content: SingleChildScrollView( + child: _HueRingPicker( + pickerColor: _customColor, + onColorChanged: (value) { + setState(() { + _customColor = value; + }); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(_customColor); + }, + child: Text(L10n.global().applyButtonLabel), + ), + ], + ); + } + + late Color _customColor = getSeedColor(); +} + +class _SeedColorPickerItem extends StatelessWidget { + const _SeedColorPickerItem({ + required this.seedColor, + this.onSelected, + }); + + @override + Widget build(BuildContext context) { + final content = SizedBox.square( + dimension: _size, + child: Center( + child: seedColor != null + ? Icon( + Icons.circle, + size: _size * .9, + color: seedColor, + ) + : Transform.scale( + scale: .9, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset("assets/ic_custom_color_56dp.png"), + const Icon( + Icons.colorize_outlined, + size: _size * .5, + color: Colors.black87, + ), + ], + ), + ), + ), + ); + if (onSelected != null) { + return InkWell( + customBorder: const CircleBorder(), + onTap: onSelected, + child: content, + ); + } else { + return content; + } + } + + final Color? seedColor; + final VoidCallback? onSelected; + + static const _size = 56.0; +} + +/// Based on the original HueRingPicker +class _HueRingPicker extends StatefulWidget { + const _HueRingPicker({ + Key? key, + required this.pickerColor, + required this.onColorChanged, + // ignore: unused_element + this.colorPickerHeight = 250.0, + // ignore: unused_element + this.hueRingStrokeWidth = 20.0, + // ignore: unused_element + this.displayThumbColor = true, + // ignore: unused_element + this.pickerAreaBorderRadius = const BorderRadius.all(Radius.zero), + }) : super(key: key); + + final Color pickerColor; + final ValueChanged onColorChanged; + final double colorPickerHeight; + final double hueRingStrokeWidth; + final bool displayThumbColor; + final BorderRadius pickerAreaBorderRadius; + + @override + _HueRingPickerState createState() => _HueRingPickerState(); +} + +class _HueRingPickerState extends State<_HueRingPicker> { + HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0); + + @override + void initState() { + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + super.initState(); + } + + @override + void didUpdateWidget(_HueRingPicker oldWidget) { + super.didUpdateWidget(oldWidget); + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + } + + void onColorChanging(HSVColor color) { + setState(() => currentHsvColor = color.withSaturation(1).withValue(1)); + widget.onColorChanged(currentHsvColor.toColor()); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + ClipRRect( + borderRadius: widget.pickerAreaBorderRadius, + child: Padding( + padding: const EdgeInsets.all(18), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + ColorIndicator( + currentHsvColor, + width: 128, + height: 128, + ), + SizedBox( + width: widget.colorPickerHeight, + height: widget.colorPickerHeight, + child: ColorPickerHueRing( + currentHsvColor, + onColorChanging, + displayThumbColor: widget.displayThumbColor, + strokeWidth: 26, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/app/pubspec.lock b/app/pubspec.lock index 5efe07c9..1eb05b06 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -497,6 +497,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.3.0" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" flutter_isolate: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f8743d59..dd0e51dd 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: url: https://gitlab.com/nc-photos/flutter_background_service.git ref: v0.2.6-nc-photos-2 flutter_bloc: ^8.0.0 + flutter_colorpicker: ^1.0.3 flutter_isolate: git: url: https://gitlab.com/nc-photos/flutter_isolate.git