Add linux deb dependencies Add backup recovery strategy select Support custom text scaling Optimize the display of different text scale Optimize windows setup experience Optimize startTun performance Optimize android tv experience Optimize default option Optimize computed text size Optimize hyperOS freeform window Add developer mode Update core Optimize more details
584 lines
14 KiB
Dart
584 lines
14 KiB
Dart
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/state.dart';
|
|
import 'package:fl_clash/widgets/open_container.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'card.dart';
|
|
import 'input.dart';
|
|
import 'scaffold.dart';
|
|
import 'sheet.dart';
|
|
|
|
class Delegate {
|
|
const Delegate();
|
|
}
|
|
|
|
class RadioDelegate<T> extends Delegate {
|
|
final T value;
|
|
final T groupValue;
|
|
final void Function(T?)? onChanged;
|
|
|
|
const RadioDelegate({
|
|
required this.value,
|
|
required this.groupValue,
|
|
this.onChanged,
|
|
});
|
|
}
|
|
|
|
class SwitchDelegate<T> extends Delegate {
|
|
final bool value;
|
|
final ValueChanged<bool>? onChanged;
|
|
|
|
const SwitchDelegate({
|
|
required this.value,
|
|
this.onChanged,
|
|
});
|
|
}
|
|
|
|
class CheckboxDelegate<T> extends Delegate {
|
|
final bool value;
|
|
final ValueChanged<bool?>? onChanged;
|
|
|
|
const CheckboxDelegate({
|
|
this.value = false,
|
|
this.onChanged,
|
|
});
|
|
}
|
|
|
|
class OpenDelegate extends Delegate {
|
|
final Widget widget;
|
|
final String title;
|
|
final double? maxWidth;
|
|
final Widget? action;
|
|
final bool blur;
|
|
|
|
const OpenDelegate({
|
|
required this.title,
|
|
required this.widget,
|
|
this.maxWidth,
|
|
this.action,
|
|
this.blur = true,
|
|
});
|
|
}
|
|
|
|
class NextDelegate extends Delegate {
|
|
final Widget widget;
|
|
final String title;
|
|
final double? maxWidth;
|
|
final Widget? action;
|
|
final bool blur;
|
|
|
|
const NextDelegate({
|
|
required this.title,
|
|
required this.widget,
|
|
this.maxWidth,
|
|
this.action,
|
|
this.blur = true,
|
|
});
|
|
}
|
|
|
|
class OptionsDelegate<T> extends Delegate {
|
|
final List<T> options;
|
|
final String title;
|
|
final T value;
|
|
final String Function(T value) textBuilder;
|
|
final Function(T? value) onChanged;
|
|
|
|
const OptionsDelegate({
|
|
required this.title,
|
|
required this.options,
|
|
required this.textBuilder,
|
|
required this.value,
|
|
required this.onChanged,
|
|
});
|
|
}
|
|
|
|
class InputDelegate extends Delegate {
|
|
final String title;
|
|
final String value;
|
|
final String? suffixText;
|
|
final Function(String? value) onChanged;
|
|
final String? resetValue;
|
|
|
|
const InputDelegate({
|
|
required this.title,
|
|
required this.value,
|
|
this.suffixText,
|
|
required this.onChanged,
|
|
this.resetValue,
|
|
});
|
|
}
|
|
|
|
class ListItem<T> extends StatelessWidget {
|
|
final Widget? leading;
|
|
final Widget title;
|
|
final Widget? subtitle;
|
|
final EdgeInsets padding;
|
|
final ListTileTitleAlignment tileTitleAlignment;
|
|
final bool? dense;
|
|
final Widget? trailing;
|
|
final Delegate delegate;
|
|
final double? horizontalTitleGap;
|
|
final TextStyle? titleTextStyle;
|
|
final TextStyle? subtitleTextStyle;
|
|
final void Function()? onTap;
|
|
|
|
const ListItem({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
|
this.trailing,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.onTap,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : delegate = const Delegate();
|
|
|
|
const ListItem.open({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
|
this.trailing,
|
|
required OpenDelegate this.delegate,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : onTap = null;
|
|
|
|
const ListItem.next({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
|
this.trailing,
|
|
required NextDelegate this.delegate,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : onTap = null;
|
|
|
|
const ListItem.options({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
|
this.trailing,
|
|
required OptionsDelegate<T> this.delegate,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : onTap = null;
|
|
|
|
const ListItem.input({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
|
this.trailing,
|
|
required InputDelegate this.delegate,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : onTap = null;
|
|
|
|
const ListItem.checkbox({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.only(left: 16, right: 8),
|
|
required CheckboxDelegate<T> this.delegate,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : trailing = null,
|
|
onTap = null;
|
|
|
|
const ListItem.switchItem({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.padding = const EdgeInsets.only(left: 16, right: 8),
|
|
required SwitchDelegate<T> this.delegate,
|
|
this.horizontalTitleGap,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : trailing = null,
|
|
onTap = null;
|
|
|
|
const ListItem.radio({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.trailing,
|
|
this.padding = const EdgeInsets.only(left: 12, right: 16),
|
|
required RadioDelegate<T> this.delegate,
|
|
this.horizontalTitleGap = 8,
|
|
this.dense,
|
|
this.titleTextStyle,
|
|
this.subtitleTextStyle,
|
|
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
|
}) : leading = null,
|
|
onTap = null;
|
|
|
|
_buildListTile({
|
|
void Function()? onTap,
|
|
Widget? trailing,
|
|
Widget? leading,
|
|
}) {
|
|
return ListTile(
|
|
key: key,
|
|
dense: dense,
|
|
titleTextStyle: titleTextStyle,
|
|
subtitleTextStyle: subtitleTextStyle,
|
|
leading: leading ?? this.leading,
|
|
horizontalTitleGap: horizontalTitleGap,
|
|
title: title,
|
|
minVerticalPadding: 12,
|
|
subtitle: subtitle,
|
|
titleAlignment: tileTitleAlignment,
|
|
onTap: onTap,
|
|
trailing: trailing ?? this.trailing,
|
|
contentPadding: padding,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (delegate is OpenDelegate) {
|
|
final openDelegate = delegate as OpenDelegate;
|
|
final child = SafeArea(
|
|
child: openDelegate.widget,
|
|
);
|
|
return OpenContainer(
|
|
closedBuilder: (_, action) {
|
|
openAction() {
|
|
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
|
if (!isMobile) {
|
|
showExtend(
|
|
context,
|
|
props: ExtendProps(
|
|
blur: openDelegate.blur,
|
|
maxWidth: openDelegate.maxWidth,
|
|
),
|
|
builder: (_, type) {
|
|
return AdaptiveSheetScaffold(
|
|
actions: [
|
|
if (openDelegate.action != null) openDelegate.action!,
|
|
],
|
|
type: type,
|
|
body: child,
|
|
title: openDelegate.title,
|
|
);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
action();
|
|
}
|
|
|
|
return _buildListTile(
|
|
onTap: openAction,
|
|
);
|
|
},
|
|
openBuilder: (_, action) {
|
|
return CommonScaffold.open(
|
|
key: Key(openDelegate.title),
|
|
onBack: action,
|
|
title: openDelegate.title,
|
|
body: child,
|
|
actions: [
|
|
if (openDelegate.action != null) openDelegate.action!,
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
if (delegate is NextDelegate) {
|
|
final nextDelegate = delegate as NextDelegate;
|
|
final child = SafeArea(
|
|
child: nextDelegate.widget,
|
|
);
|
|
|
|
return _buildListTile(
|
|
onTap: () {
|
|
showExtend(
|
|
context,
|
|
props: ExtendProps(
|
|
blur: nextDelegate.blur,
|
|
maxWidth: nextDelegate.maxWidth,
|
|
),
|
|
builder: (_, type) {
|
|
return AdaptiveSheetScaffold(
|
|
actions: [
|
|
if (nextDelegate.action != null) nextDelegate.action!,
|
|
],
|
|
type: type,
|
|
body: child,
|
|
title: nextDelegate.title,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
if (delegate is OptionsDelegate) {
|
|
final optionsDelegate = delegate as OptionsDelegate<T>;
|
|
return _buildListTile(
|
|
onTap: () async {
|
|
final value = await globalState.showCommonDialog<T>(
|
|
child: OptionsDialog<T>(
|
|
title: optionsDelegate.title,
|
|
options: optionsDelegate.options,
|
|
textBuilder: optionsDelegate.textBuilder,
|
|
value: optionsDelegate.value,
|
|
),
|
|
);
|
|
optionsDelegate.onChanged(value);
|
|
},
|
|
);
|
|
}
|
|
if (delegate is InputDelegate) {
|
|
final inputDelegate = delegate as InputDelegate;
|
|
return _buildListTile(
|
|
onTap: () async {
|
|
final value = await globalState.showCommonDialog<String>(
|
|
child: InputDialog(
|
|
title: inputDelegate.title,
|
|
value: inputDelegate.value,
|
|
suffixText: inputDelegate.suffixText,
|
|
resetValue: inputDelegate.resetValue,
|
|
),
|
|
);
|
|
inputDelegate.onChanged(value);
|
|
},
|
|
);
|
|
}
|
|
if (delegate is CheckboxDelegate) {
|
|
final checkboxDelegate = delegate as CheckboxDelegate;
|
|
return _buildListTile(
|
|
onTap: () {
|
|
if (checkboxDelegate.onChanged != null) {
|
|
checkboxDelegate.onChanged!(!checkboxDelegate.value);
|
|
}
|
|
},
|
|
trailing: CommonCheckBox(
|
|
value: checkboxDelegate.value,
|
|
onChanged: checkboxDelegate.onChanged,
|
|
),
|
|
);
|
|
}
|
|
if (delegate is SwitchDelegate) {
|
|
final switchDelegate = delegate as SwitchDelegate;
|
|
return _buildListTile(
|
|
onTap: () {
|
|
if (switchDelegate.onChanged != null) {
|
|
switchDelegate.onChanged!(!switchDelegate.value);
|
|
}
|
|
},
|
|
trailing: Switch(
|
|
value: switchDelegate.value,
|
|
onChanged: switchDelegate.onChanged,
|
|
),
|
|
);
|
|
}
|
|
if (delegate is RadioDelegate) {
|
|
final radioDelegate = delegate as RadioDelegate<T>;
|
|
return _buildListTile(
|
|
onTap: () {
|
|
if (radioDelegate.onChanged != null) {
|
|
radioDelegate.onChanged!(radioDelegate.value);
|
|
}
|
|
},
|
|
leading: Radio<T>(
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
value: radioDelegate.value,
|
|
groupValue: radioDelegate.groupValue,
|
|
onChanged: radioDelegate.onChanged,
|
|
),
|
|
trailing: trailing,
|
|
);
|
|
}
|
|
|
|
return _buildListTile(
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ListHeader extends StatelessWidget {
|
|
final String title;
|
|
final String? subTitle;
|
|
final List<Widget> actions;
|
|
final EdgeInsets? padding;
|
|
final double? space;
|
|
|
|
const ListHeader({
|
|
super.key,
|
|
required this.title,
|
|
this.subTitle,
|
|
this.padding,
|
|
List<Widget>? actions,
|
|
this.space,
|
|
}) : actions = actions ?? const [];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
alignment: Alignment.centerLeft,
|
|
padding: padding ??
|
|
const EdgeInsets.only(
|
|
left: 16,
|
|
right: 8,
|
|
top: 24,
|
|
bottom: 8,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSurfaceVariant
|
|
.opacity80,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
if (subTitle != null)
|
|
Text(
|
|
subTitle!,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.outline,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
...genActions(
|
|
actions,
|
|
space: space,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
List<Widget> generateSection({
|
|
String? title,
|
|
required Iterable<Widget> items,
|
|
List<Widget>? actions,
|
|
bool separated = true,
|
|
}) {
|
|
final genItems = separated
|
|
? items.separated(
|
|
const Divider(
|
|
height: 0,
|
|
),
|
|
)
|
|
: items;
|
|
return [
|
|
if (items.isNotEmpty && title != null)
|
|
ListHeader(
|
|
title: title,
|
|
actions: actions,
|
|
),
|
|
...genItems,
|
|
];
|
|
}
|
|
|
|
Widget generateSectionV2({
|
|
String? title,
|
|
required Iterable<Widget> items,
|
|
List<Widget>? actions,
|
|
bool separated = true,
|
|
}) {
|
|
return Column(
|
|
children: [
|
|
if (items.isNotEmpty && title != null)
|
|
ListHeader(
|
|
title: title,
|
|
actions: actions,
|
|
),
|
|
CommonCard(
|
|
radius: 18,
|
|
type: CommonCardType.filled,
|
|
child: Column(
|
|
children: [
|
|
...items,
|
|
],
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
List<Widget> generateInfoSection({
|
|
required Info info,
|
|
required Iterable<Widget> items,
|
|
List<Widget>? actions,
|
|
bool separated = true,
|
|
}) {
|
|
final genItems = separated
|
|
? items.separated(
|
|
const Divider(
|
|
height: 0,
|
|
),
|
|
)
|
|
: items;
|
|
return [
|
|
if (items.isNotEmpty)
|
|
InfoHeader(
|
|
info: info,
|
|
actions: actions,
|
|
),
|
|
...genItems,
|
|
];
|
|
}
|
|
|
|
Widget generateListView(List<Widget> items) {
|
|
return ListView.builder(
|
|
itemCount: items.length,
|
|
itemBuilder: (_, index) => items[index],
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16,
|
|
),
|
|
);
|
|
}
|