import 'dart:async'; import 'package:copy_with/copy_with.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/android_info.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_platform_util/np_platform_util.dart'; import 'package:to_string/to_string.dart'; part 'theme/bloc.dart'; part 'theme/state_event.dart'; part 'theme_settings.g.dart'; // typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; typedef _BlocListener = BlocListener<_Bloc, _State>; typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; class ThemeSettings extends StatelessWidget { const ThemeSettings({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => _Bloc( prefController: context.read(), ), child: const _WrappedThemeSettings(), ); } } class _WrappedThemeSettings extends StatefulWidget { const _WrappedThemeSettings(); @override State createState() => _WrappedThemeSettingsState(); } @npLog class _WrappedThemeSettingsState extends State<_WrappedThemeSettings> with RouteAware, PageVisibilityMixin { @override void initState() { super.initState(); _bloc.add(const _Init()); } @override Widget build(BuildContext context) { return Scaffold( body: MultiBlocListener( listeners: [ _BlocListener( listenWhen: (previous, current) => previous.error != current.error, listener: (context, state) { if (state.error != null && isPageVisible()) { SnackBarManager().showSnackBar(SnackBar( content: Text(exception_util.toUserString(state.error!.error)), duration: k.snackBarDurationNormal, )); } }, ), ], child: CustomScrollView( slivers: [ SliverAppBar( pinned: true, title: Text(L10n.global().settingsThemeTitle), ), SliverList( delegate: SliverChildListDelegate( [ const _SeedColorOption(), if (getRawPlatform() == NpPlatform.android && AndroidInfo().sdkInt >= AndroidVersion.Q) _BlocSelector( selector: (state) => state.isFollowSystemTheme, builder: (_, isFollowSystemTheme) { return SwitchListTile( title: Text( L10n.global().settingsFollowSystemThemeTitle), value: isFollowSystemTheme, onChanged: (value) { _bloc.add(_SetFollowSystemTheme(value)); }, ); }, ), _BlocSelector( selector: (state) => state.isUseBlackInDarkTheme, builder: (context, isUseBlackInDarkTheme) { return SwitchListTile( title: Text( L10n.global().settingsUseBlackInDarkThemeTitle), subtitle: Text(isUseBlackInDarkTheme ? L10n.global() .settingsUseBlackInDarkThemeTrueDescription : L10n.global() .settingsUseBlackInDarkThemeFalseDescription), value: isUseBlackInDarkTheme, onChanged: (value) { _bloc.add(_SetUseBlackInDarkTheme( value, Theme.of(context))); }, ); }, ), ], ), ), ], ), ), ); } late final _bloc = context.read<_Bloc>(); } class _SeedColorOption extends StatelessWidget { const _SeedColorOption(); @override Widget build(BuildContext context) { return _BlocSelector( selector: (state) => state.seedColor, builder: (context, seedColor) { if (SessionStorage().isSupportDynamicColor) { return ListTile( title: Text(L10n.global().settingsSeedColorTitle), subtitle: Text(seedColor == null ? L10n.global().settingsSeedColorSystemColorDescription : L10n.global().settingsSeedColorDescription), trailing: seedColor == null ? null : Icon( Icons.circle, size: 32, color: Color(seedColor), ), onTap: () => _onSeedColorPressed(context), ); } else { return ListTile( title: Text(L10n.global().settingsSeedColorTitle), subtitle: Text(L10n.global().settingsSeedColorDescription), trailing: Icon( Icons.circle, size: 32, color: seedColor?.run(Color.new) ?? defaultSeedColor, ), onTap: () => _onSeedColorPressed(context), ); } }, ); } Future _onSeedColorPressed(BuildContext context) async { final result = await showDialog( context: context, builder: (context) => const _SeedColorPicker(), ); if (result == null) { return; } if (context.mounted) { context .read<_Bloc>() .add(_SetSeedColor(result == -1 ? null : Color(result))); } } } 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?.value), )) .toList(), ), actions: SessionStorage().isSupportDynamicColor ? [ TextButton( onPressed: () => _onItemSelected(context, -1), child: Text(L10n.global() .settingsSeedColorPickerSystemColorButtonLabel), ), ] : null, ), ); } Future _onItemSelected(BuildContext context, int? seedColor) async { if (seedColor != null) { Navigator.of(context).pop(seedColor); return; } setState(() { _isVisible = false; }); final color = await showDialog( context: context, builder: (_) => const _SeedColorCustomPicker(), barrierColor: Colors.transparent, ); Navigator.of(context).pop(color?.value); } 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 var _customColor = getSeedColor(context) ?? defaultSeedColor; } 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, ), ), ], ), ), ), ], ); } }