Files
MWClash/lib/views/backup_and_recovery.dart
chen08209 ed7868282a Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-23 15:23:58 +08:00

462 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});
Future<void> _showAddWebDAV(DAV? dav) async {
await globalState.showCommonDialog<String>(
child: WebDAVFormDialog(dav: dav?.copyWith()),
);
}
Future<void> _backupOnWebDAV(DAVClient client) async {
final res = await globalState.appController.safeRun<bool>(
() async {
final backupData = await globalState.appController.backupData();
return await client.backup(Uint8List.fromList(backupData));
},
needLoading: true,
title: appLocalizations.backup,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess),
);
}
Future<void> _recoveryOnWebDAV(
BuildContext context,
DAVClient client,
RecoveryOption recoveryOption,
) async {
final res = await globalState.appController.safeRun<bool>(
() async {
final data = await client.recovery();
await globalState.appController.recoveryData(data, recoveryOption);
return true;
},
needLoading: true,
title: appLocalizations.recovery,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.recoverySuccess),
);
}
Future<void> _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);
}
Future<void> _backupOnLocal(BuildContext context) async {
final res = await globalState.appController.safeRun<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,
needLoading: true,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess),
);
}
Future<void> _recoveryOnLocal(RecoveryOption recoveryOption) async {
final file = await picker.pickerFile();
final data = file?.bytes;
if (data == null) return;
final res = await globalState.appController.safeRun<bool>(
() async {
await globalState.appController.recoveryData(
List<int>.from(data),
recoveryOption,
);
return true;
},
needLoading: true,
title: appLocalizations.recovery,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.recoverySuccess),
);
}
Future<void> _handleRecoveryOnLocal(BuildContext context) async {
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
child: const RecoveryOptionsDialog(),
);
if (recoveryOption == null || !context.mounted) return;
_recoveryOnLocal(recoveryOption);
}
void _handleChange(String? value, WidgetRef ref) {
if (value == null) {
return;
}
ref
.read(appDAVSettingProvider.notifier)
.updateState((state) => state?.copyWith(fileName: value));
}
Future<void> _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.done
? 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(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> {
void _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);
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
ref.read(appDAVSettingProvider.notifier).value = DAV(
uri: uriController.text,
user: userController.text,
password: passwordController.text,
);
Navigator.pop(context);
}
void _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.emptyTip(appLocalizations.account);
}
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.emptyTip(
appLocalizations.password,
);
}
return null;
},
);
},
),
],
),
),
);
}
}