Files
MWClash/lib/fragments/backup_and_recovery.dart
chen08209 676f2d058a Add windows server mode start process verify
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
2025-05-01 00:02:29 +08:00

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;
},
);
},
),
],
),
),
);
}
}