716 lines
20 KiB
Dart
716 lines
20 KiB
Dart
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 ThemeFragment extends StatelessWidget {
|
|
const ThemeFragment({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
_ThemeModeItem(),
|
|
_PrimaryColorItem(),
|
|
_PrueBlackItem(),
|
|
_TextScaleFactorItem(),
|
|
const SizedBox(
|
|
height: 64,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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 Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 16,
|
|
),
|
|
child: 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);
|
|
}
|
|
|
|
_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,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
_handleDel() async {
|
|
if (_removablePrimaryColor == null) {
|
|
return;
|
|
}
|
|
final res = await globalState.showMessage(
|
|
message: TextSpan(text: appLocalizations.deleteColorTip));
|
|
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;
|
|
});
|
|
}
|
|
|
|
_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.colorExists);
|
|
return;
|
|
}
|
|
ref.read(themeSettingProvider.notifier).updateState(
|
|
(state) {
|
|
return state.copyWith(
|
|
primaryColors: List.from(
|
|
state.primaryColors,
|
|
)..add(res),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
_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 vm3 = ref.watch(
|
|
themeSettingProvider.select(
|
|
(state) => VM3(
|
|
a: state.primaryColor,
|
|
b: state.primaryColors,
|
|
c: state.schemeVariant,
|
|
),
|
|
),
|
|
);
|
|
final primaryColor = vm3.a;
|
|
final primaryColors = [null, ...vm3.b];
|
|
final schemeVariant = vm3.c;
|
|
|
|
return CommonPopScope(
|
|
onPop: () {
|
|
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: ButtonStyle(
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
onPressed: _handleChangeSchemeVariant,
|
|
child: Text(Intl.message("${schemeVariant.name}Scheme")),
|
|
),
|
|
_removablePrimaryColor != null
|
|
? FilledButton(
|
|
style: ButtonStyle(
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
_removablePrimaryColor = null;
|
|
});
|
|
},
|
|
child: Text(appLocalizations.cancel),
|
|
)
|
|
: IconButton.filledTonal(
|
|
iconSize: 20,
|
|
padding: EdgeInsets.all(4),
|
|
visualDensity: VisualDensity.compact,
|
|
onPressed: _handleReset,
|
|
icon: Icon(Icons.replay),
|
|
)
|
|
],
|
|
space: 8,
|
|
),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(
|
|
left: 16,
|
|
right: 16,
|
|
bottom: 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 Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
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).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) as double).round()}%";
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
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,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
DisabledMask(
|
|
status: !textScale.enable,
|
|
child: ActivateBox(
|
|
active: textScale.enable,
|
|
child: SliderTheme(
|
|
data: _SliderDefaultsM3(context),
|
|
child: Slider(
|
|
min: 0.8,
|
|
divisions: 8,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
),
|
|
max: 1.2,
|
|
label: process,
|
|
value: textScale.scale,
|
|
onChanged: (value) {
|
|
ref.read(themeSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith.textScale(
|
|
scale: value,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|