Files
MWClash/lib/views/theme.dart
chen08209 db49cd81ce Add sqlite store
Optimize android quick action

Optimize backup and restore

Optimize more details
2026-02-02 10:15:11 +08:00

690 lines
21 KiB
Dart

// 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/models.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) {
return BaseScaffold(
title: appLocalizations.theme,
body: CustomScrollView(
slivers: [
_ThemeModeItem(),
SliverToBoxAdapter(child: SizedBox(height: 16)),
_PrimaryColorItem(),
SliverToBoxAdapter(child: SizedBox(height: 16)),
_PrueBlackItem(),
SliverToBoxAdapter(child: SizedBox(height: 16)),
_TextScaleFactorItem(),
SliverToBoxAdapter(child: SizedBox(height: 32)),
],
),
);
}
}
class ItemCard extends StatelessWidget {
final Widget child;
final Info info;
final List<Widget> 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<ThemeModeItem> 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 SliverToBoxAdapter(
child: 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)
.update(
(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<void> _handleReset() async {
final res = await globalState.showMessage(
message: TextSpan(text: appLocalizations.resetTip),
);
if (res != true) {
return;
}
ref.read(themeSettingProvider.notifier).update((state) {
return state.copyWith(
primaryColors: defaultPrimaryColors,
primaryColor: defaultPrimaryColor,
schemeVariant: DynamicSchemeVariant.content,
);
});
}
Future<void> _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).update((state) {
final newPrimaryColors = List<int>.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<void> _handleAdd() async {
final res = await globalState.showCommonDialog<int>(
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).update((state) {
return state.copyWith(
primaryColors: List.from(state.primaryColors)..add(res),
);
});
}
Future<void> _handleChangeSchemeVariant() async {
final schemeVariant = ref.read(
themeSettingProvider.select((state) => state.schemeVariant),
);
final value = await globalState.showCommonDialog<DynamicSchemeVariant>(
child: OptionsDialog<DynamicSchemeVariant>(
title: appLocalizations.colorSchemes,
options: DynamicSchemeVariant.values,
textBuilder: (item) => Intl.message('${item.name}Scheme'),
value: schemeVariant,
),
);
if (value == null) {
return;
}
ref.read(themeSettingProvider.notifier).update((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,
) &&
state.schemeVariant == DynamicSchemeVariant.content,
),
),
);
final primaryColor = vm4.a;
final primaryColors = [null, ...vm4.b];
final schemeVariant = vm4.c;
final isEquals = vm4.d;
return SliverToBoxAdapter(
child: 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)
.update(
(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 SliverToBoxAdapter(
child: 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)
.update((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 SliverToBoxAdapter(
child: 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)
.update(
(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)
.update(
(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<ui.Color>(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<WidgetState> 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<Size?>? get thumbSize {
return WidgetStateProperty.resolveWith((Set<WidgetState> 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;
}