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
481 lines
15 KiB
Dart
481 lines
15 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/common/dav_client.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/dialog.dart';
|
|
import 'package:fl_clash/widgets/fade_box.dart';
|
|
import 'package:fl_clash/widgets/input.dart';
|
|
import 'package:fl_clash/widgets/list.dart';
|
|
import 'package:fl_clash/widgets/text.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class BackupAndRecovery extends ConsumerWidget {
|
|
const BackupAndRecovery({super.key});
|
|
|
|
_showAddWebDAV(DAV? dav) async {
|
|
await globalState.showCommonDialog<String>(
|
|
child: WebDAVFormDialog(
|
|
dav: dav?.copyWith(),
|
|
),
|
|
);
|
|
}
|
|
|
|
_backupOnWebDAV(BuildContext context, DAVClient client) async {
|
|
final commonScaffoldState = context.commonScaffoldState;
|
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
|
() async {
|
|
final backupData = await globalState.appController.backupData();
|
|
return await client.backup(Uint8List.fromList(backupData));
|
|
},
|
|
title: appLocalizations.backup,
|
|
);
|
|
if (res != true) return;
|
|
globalState.showMessage(
|
|
title: appLocalizations.backup,
|
|
message: TextSpan(text: appLocalizations.backupSuccess),
|
|
);
|
|
}
|
|
|
|
_recoveryOnWebDAV(
|
|
BuildContext context,
|
|
DAVClient client,
|
|
RecoveryOption recoveryOption,
|
|
) async {
|
|
final commonScaffoldState = context.commonScaffoldState;
|
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
|
() async {
|
|
final data = await client.recovery();
|
|
await globalState.appController.recoveryData(data, recoveryOption);
|
|
return true;
|
|
},
|
|
title: appLocalizations.recovery,
|
|
);
|
|
if (res != true) return;
|
|
globalState.showMessage(
|
|
title: appLocalizations.recovery,
|
|
message: TextSpan(text: appLocalizations.recoverySuccess),
|
|
);
|
|
}
|
|
|
|
_handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async {
|
|
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
|
|
child: const RecoveryOptionsDialog(),
|
|
);
|
|
if (recoveryOption == null || !context.mounted) return;
|
|
_recoveryOnWebDAV(context, client, recoveryOption);
|
|
}
|
|
|
|
_backupOnLocal(BuildContext context) async {
|
|
final commonScaffoldState = context.commonScaffoldState;
|
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
|
() async {
|
|
final backupData = await globalState.appController.backupData();
|
|
final value = await picker.saveFile(
|
|
utils.getBackupFileName(),
|
|
Uint8List.fromList(backupData),
|
|
);
|
|
if (value == null) return false;
|
|
return true;
|
|
},
|
|
title: appLocalizations.backup,
|
|
);
|
|
if (res != true) return;
|
|
globalState.showMessage(
|
|
title: appLocalizations.backup,
|
|
message: TextSpan(text: appLocalizations.backupSuccess),
|
|
);
|
|
}
|
|
|
|
_recoveryOnLocal(
|
|
BuildContext context,
|
|
RecoveryOption recoveryOption,
|
|
) async {
|
|
final file = await picker.pickerFile();
|
|
final data = file?.bytes;
|
|
if (data == null || !context.mounted) return;
|
|
final commonScaffoldState = context.commonScaffoldState;
|
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
|
() async {
|
|
await globalState.appController.recoveryData(
|
|
List<int>.from(data),
|
|
recoveryOption,
|
|
);
|
|
return true;
|
|
},
|
|
title: appLocalizations.recovery,
|
|
);
|
|
if (res != true) return;
|
|
globalState.showMessage(
|
|
title: appLocalizations.recovery,
|
|
message: TextSpan(text: appLocalizations.recoverySuccess),
|
|
);
|
|
}
|
|
|
|
_handleRecoveryOnLocal(BuildContext context) async {
|
|
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
|
|
child: const RecoveryOptionsDialog(),
|
|
);
|
|
if (recoveryOption == null || !context.mounted) return;
|
|
_recoveryOnLocal(context, recoveryOption);
|
|
}
|
|
|
|
_handleChange(String? value, WidgetRef ref) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
ref.read(appDAVSettingProvider.notifier).updateState(
|
|
(state) => state?.copyWith(
|
|
fileName: value,
|
|
),
|
|
);
|
|
}
|
|
|
|
_handleUpdateRecoveryStrategy(WidgetRef ref) async {
|
|
final recoveryStrategy = ref.read(appSettingProvider.select(
|
|
(state) => state.recoveryStrategy,
|
|
));
|
|
final res = await globalState.showCommonDialog(
|
|
child: OptionsDialog<RecoveryStrategy>(
|
|
title: appLocalizations.recoveryStrategy,
|
|
options: RecoveryStrategy.values,
|
|
textBuilder: (mode) => Intl.message(
|
|
"recoveryStrategy_${mode.name}",
|
|
),
|
|
value: recoveryStrategy,
|
|
),
|
|
);
|
|
if (res == null) {
|
|
return;
|
|
}
|
|
ref.read(appSettingProvider.notifier).updateState(
|
|
(state) => state.copyWith(
|
|
recoveryStrategy: res,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final dav = ref.watch(appDAVSettingProvider);
|
|
final client = dav != null ? DAVClient(dav) : null;
|
|
return ListView(
|
|
children: [
|
|
ListHeader(title: appLocalizations.remote),
|
|
if (dav == null)
|
|
ListItem(
|
|
leading: const Icon(Icons.account_box),
|
|
title: Text(appLocalizations.noInfo),
|
|
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
|
trailing: FilledButton.tonal(
|
|
onPressed: () {
|
|
_showAddWebDAV(dav);
|
|
},
|
|
child: Text(
|
|
appLocalizations.bind,
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
ListItem(
|
|
leading: const Icon(Icons.account_box),
|
|
title: TooltipText(
|
|
text: Text(
|
|
dav.user,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
subtitle: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(appLocalizations.connectivity),
|
|
FutureBuilder<bool>(
|
|
future: client!.pingCompleter.future,
|
|
builder: (_, snapshot) {
|
|
return Center(
|
|
child: FadeThroughBox(
|
|
child: snapshot.connectionState ==
|
|
ConnectionState.waiting
|
|
? const SizedBox(
|
|
width: 12,
|
|
height: 12,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 1,
|
|
),
|
|
)
|
|
: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: snapshot.data == true
|
|
? Colors.green
|
|
: Colors.red,
|
|
),
|
|
width: 12,
|
|
height: 12,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
trailing: FilledButton.tonal(
|
|
onPressed: () {
|
|
_showAddWebDAV(dav);
|
|
},
|
|
child: Text(
|
|
appLocalizations.edit,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 4,
|
|
),
|
|
ListItem.input(
|
|
title: Text(appLocalizations.file),
|
|
subtitle: Text(dav.fileName),
|
|
delegate: InputDelegate(
|
|
title: appLocalizations.file,
|
|
value: dav.fileName,
|
|
resetValue: defaultDavFileName,
|
|
onChanged: (value) {
|
|
_handleChange(value, ref);
|
|
},
|
|
),
|
|
),
|
|
ListItem(
|
|
onTap: () {
|
|
_backupOnWebDAV(context, client);
|
|
},
|
|
title: Text(appLocalizations.backup),
|
|
subtitle: Text(appLocalizations.remoteBackupDesc),
|
|
),
|
|
ListItem(
|
|
onTap: () {
|
|
_handleRecoveryOnWebDAV(context, client);
|
|
},
|
|
title: Text(appLocalizations.recovery),
|
|
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
|
),
|
|
],
|
|
ListHeader(title: appLocalizations.local),
|
|
ListItem(
|
|
onTap: () {
|
|
_backupOnLocal(context);
|
|
},
|
|
title: Text(appLocalizations.backup),
|
|
subtitle: Text(appLocalizations.localBackupDesc),
|
|
),
|
|
ListItem(
|
|
onTap: () {
|
|
_handleRecoveryOnLocal(context);
|
|
},
|
|
title: Text(appLocalizations.recovery),
|
|
subtitle: Text(appLocalizations.localRecoveryDesc),
|
|
),
|
|
ListHeader(title: appLocalizations.options),
|
|
Consumer(builder: (_, ref, __) {
|
|
final recoveryStrategy = ref.watch(appSettingProvider.select(
|
|
(state) => state.recoveryStrategy,
|
|
));
|
|
return ListItem(
|
|
onTap: () {
|
|
_handleUpdateRecoveryStrategy(ref);
|
|
},
|
|
title: Text(appLocalizations.recoveryStrategy),
|
|
trailing: FilledButton(
|
|
onPressed: () {
|
|
_handleUpdateRecoveryStrategy(ref);
|
|
},
|
|
child: Text(
|
|
Intl.message("recoveryStrategy_${recoveryStrategy.name}"),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class RecoveryOptionsDialog extends StatefulWidget {
|
|
const RecoveryOptionsDialog({super.key});
|
|
|
|
@override
|
|
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
|
|
}
|
|
|
|
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
|
|
_handleOnTab(RecoveryOption? value) {
|
|
if (value == null) return;
|
|
Navigator.of(context).pop(value);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CommonDialog(
|
|
title: appLocalizations.recovery,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 16,
|
|
),
|
|
child: Wrap(
|
|
children: [
|
|
ListItem(
|
|
onTap: () {
|
|
_handleOnTab(RecoveryOption.onlyProfiles);
|
|
},
|
|
title: Text(appLocalizations.recoveryProfiles),
|
|
),
|
|
ListItem(
|
|
onTap: () {
|
|
_handleOnTab(RecoveryOption.all);
|
|
},
|
|
title: Text(appLocalizations.recoveryAll),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class WebDAVFormDialog extends ConsumerStatefulWidget {
|
|
final DAV? dav;
|
|
|
|
const WebDAVFormDialog({super.key, this.dav});
|
|
|
|
@override
|
|
ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
|
}
|
|
|
|
class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
|
|
late TextEditingController uriController;
|
|
late TextEditingController userController;
|
|
late TextEditingController passwordController;
|
|
final _obscureController = ValueNotifier<bool>(true);
|
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
uriController = TextEditingController(text: widget.dav?.uri);
|
|
userController = TextEditingController(text: widget.dav?.user);
|
|
passwordController = TextEditingController(text: widget.dav?.password);
|
|
}
|
|
|
|
_submit() {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
ref.read(appDAVSettingProvider.notifier).value = DAV(
|
|
uri: uriController.text,
|
|
user: userController.text,
|
|
password: passwordController.text,
|
|
);
|
|
Navigator.pop(context);
|
|
}
|
|
|
|
_delete() {
|
|
ref.read(appDAVSettingProvider.notifier).value = null;
|
|
Navigator.pop(context);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_obscureController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CommonDialog(
|
|
title: appLocalizations.webDAVConfiguration,
|
|
actions: [
|
|
if (widget.dav != null)
|
|
TextButton(
|
|
onPressed: _delete,
|
|
child: Text(appLocalizations.delete),
|
|
),
|
|
TextButton(
|
|
onPressed: _submit,
|
|
child: Text(appLocalizations.save),
|
|
)
|
|
],
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Wrap(
|
|
runSpacing: 16,
|
|
children: [
|
|
TextFormField(
|
|
controller: uriController,
|
|
maxLines: 5,
|
|
minLines: 1,
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.link),
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.address,
|
|
helperText: appLocalizations.addressHelp,
|
|
),
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty || !value.isUrl) {
|
|
return appLocalizations.addressTip;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
TextFormField(
|
|
controller: userController,
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.account_circle),
|
|
border: const OutlineInputBorder(),
|
|
labelText: appLocalizations.account,
|
|
),
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.accountTip;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
ValueListenableBuilder(
|
|
valueListenable: _obscureController,
|
|
builder: (_, obscure, __) {
|
|
return TextFormField(
|
|
controller: passwordController,
|
|
obscureText: obscure,
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.password),
|
|
border: const OutlineInputBorder(),
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
obscure ? Icons.visibility : Icons.visibility_off,
|
|
),
|
|
onPressed: () {
|
|
_obscureController.value = !obscure;
|
|
},
|
|
),
|
|
labelText: appLocalizations.password,
|
|
),
|
|
validator: (String? value) {
|
|
if (value == null || value.isEmpty) {
|
|
return appLocalizations.passwordTip;
|
|
}
|
|
return null;
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|