674 lines
20 KiB
Dart
674 lines
20 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/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<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 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<void> _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<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).updateState((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).updateState((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).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<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;
|
|
}
|