// ignore_for_file: deprecated_member_use import 'dart:math'; import 'dart:ui' as ui; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/selector.dart'; import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; class ThemeModeItem { final ThemeMode themeMode; final IconData iconData; final String label; const ThemeModeItem({ required this.themeMode, required this.iconData, required this.label, }); } class FontFamilyItem { final FontFamily fontFamily; final String label; const FontFamilyItem({required this.fontFamily, required this.label}); } class ThemeView extends StatelessWidget { const ThemeView({super.key}); @override Widget build(BuildContext context) { final items = [ _ThemeModeItem(), _PrimaryColorItem(), _PrueBlackItem(), _TextScaleFactorItem(), const SizedBox(height: 64), ]; return ListView.separated( itemCount: items.length, itemBuilder: (_, index) { return items[index]; }, separatorBuilder: (_, _) { return SizedBox(height: 24); }, ); } } class ItemCard extends StatelessWidget { final Widget child; final Info info; final List actions; const ItemCard({ super.key, required this.info, required this.child, this.actions = const [], }); @override Widget build(BuildContext context) { return Wrap( runSpacing: 16, children: [ InfoHeader(info: info, actions: actions), child, ], ); } } class _ThemeModeItem extends ConsumerWidget { const _ThemeModeItem(); @override Widget build(BuildContext context, WidgetRef ref) { final themeMode = ref.watch( themeSettingProvider.select((state) => state.themeMode), ); List themeModeItems = [ ThemeModeItem( iconData: Icons.auto_mode, label: appLocalizations.auto, themeMode: ThemeMode.system, ), ThemeModeItem( iconData: Icons.light_mode, label: appLocalizations.light, themeMode: ThemeMode.light, ), ThemeModeItem( iconData: Icons.dark_mode, label: appLocalizations.dark, themeMode: ThemeMode.dark, ), ]; return ItemCard( info: Info( label: appLocalizations.themeMode, iconData: Icons.brightness_high, ), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), height: 56, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: themeModeItems.length, itemBuilder: (_, index) { final themeModeItem = themeModeItems[index]; return CommonCard( isSelected: themeModeItem.themeMode == themeMode, onPressed: () { ref .read(themeSettingProvider.notifier) .updateState( (state) => state.copyWith(themeMode: themeModeItem.themeMode), ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ Flexible(child: Icon(themeModeItem.iconData)), const SizedBox(width: 8), Flexible(child: Text(themeModeItem.label)), ], ), ), ); }, separatorBuilder: (_, _) { return const SizedBox(width: 16); }, ), ), ); } } class _PrimaryColorItem extends ConsumerStatefulWidget { const _PrimaryColorItem(); @override ConsumerState<_PrimaryColorItem> createState() => _PrimaryColorItemState(); } class _PrimaryColorItemState extends ConsumerState<_PrimaryColorItem> { int? _removablePrimaryColor; int _calcColumns(double maxWidth) { return max((maxWidth / 96).ceil(), 3); } Future _handleReset() async { final res = await globalState.showMessage( message: TextSpan(text: appLocalizations.resetTip), ); if (res != true) { return; } ref.read(themeSettingProvider.notifier).updateState((state) { return state.copyWith( primaryColors: defaultPrimaryColors, primaryColor: defaultPrimaryColor, schemeVariant: DynamicSchemeVariant.tonalSpot, ); }); } Future _handleDel() async { if (_removablePrimaryColor == null) { return; } final res = await globalState.showMessage( message: TextSpan( text: appLocalizations.deleteTip(appLocalizations.colorSchemes), ), ); if (res != true) { return; } ref.read(themeSettingProvider.notifier).updateState((state) { final newPrimaryColors = List.from(state.primaryColors) ..remove(_removablePrimaryColor); int? newPrimaryColor = state.primaryColor; if (state.primaryColor == _removablePrimaryColor) { if (newPrimaryColors.contains(defaultPrimaryColor)) { newPrimaryColor = defaultPrimaryColor; } else { newPrimaryColor = null; } } return state.copyWith( primaryColors: newPrimaryColors, primaryColor: newPrimaryColor, ); }); setState(() { _removablePrimaryColor = null; }); } Future _handleAdd() async { final res = await globalState.showCommonDialog( child: _PaletteDialog(), ); if (res == null) { return; } final isExists = ref.read( themeSettingProvider.select((state) => state.primaryColors.contains(res)), ); if (isExists && mounted) { context.showNotifier( appLocalizations.existsTip(appLocalizations.colorSchemes), ); return; } ref.read(themeSettingProvider.notifier).updateState((state) { return state.copyWith( primaryColors: List.from(state.primaryColors)..add(res), ); }); } Future _handleChangeSchemeVariant() async { final schemeVariant = ref.read( themeSettingProvider.select((state) => state.schemeVariant), ); final value = await globalState.showCommonDialog( child: OptionsDialog( title: appLocalizations.colorSchemes, options: DynamicSchemeVariant.values, textBuilder: (item) => Intl.message('${item.name}Scheme'), value: schemeVariant, ), ); if (value == null) { return; } ref.read(themeSettingProvider.notifier).updateState((state) { return state.copyWith(schemeVariant: value); }); } @override Widget build(BuildContext context) { final vm4 = ref.watch( themeSettingProvider.select( (state) => VM4( state.primaryColor, state.primaryColors, state.schemeVariant, state.primaryColor == defaultPrimaryColor && intListEquality.equals(state.primaryColors, defaultPrimaryColors), ), ), ); final primaryColor = vm4.a; final primaryColors = [null, ...vm4.b]; final schemeVariant = vm4.c; final isEquals = vm4.d; return CommonPopScope( onPop: (context) { if (_removablePrimaryColor != null) { setState(() { _removablePrimaryColor = null; }); return false; } return true; }, child: ItemCard( info: Info(label: appLocalizations.themeColor, iconData: Icons.palette), actions: genActions([ if (_removablePrimaryColor == null) FilledButton( style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), onPressed: _handleChangeSchemeVariant, child: Text(Intl.message('${schemeVariant.name}Scheme')), ), if (_removablePrimaryColor != null) FilledButton( style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), onPressed: () { setState(() { _removablePrimaryColor = null; }); }, child: Text(appLocalizations.cancel), ), if (_removablePrimaryColor == null && !isEquals) IconButton.filledTonal( iconSize: 20, padding: EdgeInsets.all(4), visualDensity: VisualDensity.compact, onPressed: _handleReset, icon: Icon(Icons.replay), ), ], space: 8), child: Container( margin: const EdgeInsets.symmetric(horizontal: 16), child: LayoutBuilder( builder: (_, constraints) { final columns = _calcColumns(constraints.maxWidth); final itemWidth = (constraints.maxWidth - (columns - 1) * 16) / columns; return Wrap( spacing: 16, runSpacing: 16, children: [ for (final color in primaryColors) Container( clipBehavior: Clip.none, width: itemWidth, height: itemWidth, child: Stack( alignment: Alignment.center, clipBehavior: Clip.none, children: [ EffectGestureDetector( child: ColorSchemeBox( isSelected: color == primaryColor, primaryColor: color != null ? Color(color) : null, onPressed: () { setState(() { _removablePrimaryColor = null; }); ref .read(themeSettingProvider.notifier) .updateState( (state) => state.copyWith(primaryColor: color), ); }, ), onLongPress: () { setState(() { _removablePrimaryColor = color; }); }, ), if (_removablePrimaryColor != null && _removablePrimaryColor == color) Container( color: Colors.white.opacity0, padding: EdgeInsets.all(8), child: IconButton.filledTonal( onPressed: _handleDel, padding: EdgeInsets.all(12), iconSize: 30, icon: Icon( color: context.colorScheme.primary, Icons.delete, ), ), ), ], ), ), if (_removablePrimaryColor == null) Container( width: itemWidth, height: itemWidth, padding: EdgeInsets.all(4), child: IconButton.filledTonal( onPressed: _handleAdd, iconSize: 32, icon: Icon( color: context.colorScheme.primary, Icons.add, ), ), ), ], ); }, ), ), ), ); } } class _PrueBlackItem extends ConsumerWidget { const _PrueBlackItem(); @override Widget build(BuildContext context, WidgetRef ref) { final prueBlack = ref.watch( themeSettingProvider.select((state) => state.pureBlack), ); return ListItem.switchItem( leading: Icon(Icons.contrast), horizontalTitleGap: 12, title: Text( appLocalizations.pureBlackMode, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurfaceVariant, ), ), delegate: SwitchDelegate( value: prueBlack, onChanged: (value) { ref .read(themeSettingProvider.notifier) .updateState((state) => state.copyWith(pureBlack: value)); }, ), ); } } class _TextScaleFactorItem extends ConsumerWidget { const _TextScaleFactorItem(); @override Widget build(BuildContext context, WidgetRef ref) { final textScale = ref.watch( themeSettingProvider.select((state) => state.textScale), ); final String process = '${(textScale.scale * 100).round()}%'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only(bottom: 8), child: ListItem.switchItem( leading: Icon(Icons.text_fields), horizontalTitleGap: 12, title: Text( appLocalizations.textScale, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurfaceVariant, ), ), delegate: SwitchDelegate( value: textScale.enable, onChanged: (value) { ref .read(themeSettingProvider.notifier) .updateState( (state) => state.copyWith.textScale(enable: value), ); }, ), ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, spacing: 32, children: [ Expanded( child: DisabledMask( status: !textScale.enable, child: ActivateBox( active: textScale.enable, child: SliderTheme( data: _SliderDefaultsM3(context), child: Slider( padding: EdgeInsets.zero, min: minTextScale, max: maxTextScale, value: textScale.scale, onChanged: (value) { ref .read(themeSettingProvider.notifier) .updateState( (state) => state.copyWith.textScale(scale: value), ); }, ), ), ), ), ), Padding( padding: EdgeInsets.only(right: 4), child: Text(process, style: context.textTheme.titleMedium), ), ], ), ), ], ); } } class _PaletteDialog extends StatefulWidget { const _PaletteDialog(); @override State<_PaletteDialog> createState() => _PaletteDialogState(); } class _PaletteDialogState extends State<_PaletteDialog> { final _controller = ValueNotifier(Colors.transparent); @override Widget build(BuildContext context) { return CommonDialog( title: appLocalizations.palette, actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(appLocalizations.cancel), ), TextButton( onPressed: () { Navigator.of(context).pop(_controller.value.toARGB32()); }, child: Text(appLocalizations.confirm), ), ], child: Column( children: [ SizedBox(height: 8), SizedBox( width: 250, height: 250, child: Palette(controller: _controller), ), SizedBox(height: 24), ValueListenableBuilder( valueListenable: _controller, builder: (_, color, _) { return PrimaryColorBox( primaryColor: color, child: FilledButton( onPressed: () {}, child: Text(_controller.value.hex), ), ); }, ), ], ), ); } } class _SliderDefaultsM3 extends SliderThemeData { _SliderDefaultsM3(this.context) : super(trackHeight: 16.0); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override Color? get activeTrackColor => _colors.primary; @override Color? get inactiveTrackColor => _colors.secondaryContainer; @override Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54); @override Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38); @override Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12); @override Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.38); @override Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0); @override Color? get inactiveTickMarkColor => _colors.onSecondaryContainer.withOpacity(1.0); @override Color? get disabledActiveTickMarkColor => _colors.onInverseSurface; @override Color? get disabledInactiveTickMarkColor => _colors.onSurface; @override Color? get thumbColor => _colors.primary; @override Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38); @override Color? get overlayColor => WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.dragged)) { return _colors.primary.withOpacity(0.1); } if (states.contains(WidgetState.hovered)) { return _colors.primary.withOpacity(0.08); } if (states.contains(WidgetState.focused)) { return _colors.primary.withOpacity(0.1); } return Colors.transparent; }); @override TextStyle? get valueIndicatorTextStyle => Theme.of( context, ).textTheme.labelLarge!.copyWith(color: _colors.onInverseSurface); @override Color? get valueIndicatorColor => _colors.inverseSurface; @override SliderComponentShape? get valueIndicatorShape => const RoundedRectSliderValueIndicatorShape(); @override SliderComponentShape? get thumbShape => const HandleThumbShape(); @override SliderTrackShape? get trackShape => const GappedSliderTrackShape(); @override SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); @override SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2); @override WidgetStateProperty? get thumbSize { return WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.disabled)) { return const Size(4.0, 44.0); } if (states.contains(WidgetState.hovered)) { return const Size(4.0, 44.0); } if (states.contains(WidgetState.focused)) { return const Size(2.0, 44.0); } if (states.contains(WidgetState.pressed)) { return const Size(2.0, 44.0); } return const Size(4.0, 44.0); }); } @override double? get trackGap => 6.0; }