Files
MWClash/lib/views/backup_and_restore.dart
chen08209 2fbb96f5c1 Add sqlite store
Optimize android quick action

Optimize backup and restore

Optimize more details
2026-02-02 10:15:42 +08:00

477 lines
15 KiB
Dart

import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/controller.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.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/scaffold.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 BackupAndRestore extends ConsumerWidget {
const BackupAndRestore({super.key});
Future<void> _showAddWebDAV(DAVProps? dav) async {
await globalState.showCommonDialog<String>(
child: WebDAVFormDialog(dav: dav?.copyWith()),
);
}
Future<void> _backupOnWebDAV(DAVClient client) async {
final res = await appController.loadingRun<bool>(
() async {
final path = await appController.backup();
if (path.isEmpty) {
return false;
}
return await client.backup(path);
},
tag: LoadingTag.backup_restore,
title: appLocalizations.backup,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess),
);
}
Future<void> _restoreOnWebDAV(
BuildContext context,
DAVClient client,
RestoreOption option,
) async {
final res = await appController.loadingRun<bool>(
() async {
await client.restore();
await appController.restore(option);
return true;
},
tag: LoadingTag.backup_restore,
title: appLocalizations.restore,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.restore,
message: TextSpan(text: appLocalizations.restoreSuccess),
);
}
Future<void> _handleRestoreOnWebDAV(
BuildContext context,
DAVClient client,
) async {
final restoreOption = await globalState.showCommonDialog<RestoreOption>(
child: const RestoreOptionsDialog(),
);
if (restoreOption == null || !context.mounted) return;
_restoreOnWebDAV(context, client, restoreOption);
}
Future<void> _backupOnLocal(BuildContext context) async {
final res = await appController.loadingRun<bool>(
() async {
final path = await appController.backup();
if (path.isEmpty) {
return false;
}
final value = await picker.saveFileWithPath(
utils.getBackupFileName(),
path,
);
if (value == null) return false;
return true;
},
title: appLocalizations.backup,
tag: LoadingTag.backup_restore,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess),
);
}
Future<void> _restoreOnLocal(RestoreOption option) async {
final file = await picker.pickerFile(withData: false);
final path = file?.path;
if (path == null) return;
await File(path).safeCopy(await appPath.backupFilePath);
final res = await appController.loadingRun<bool>(
() async {
await appController.restore(option);
return true;
},
tag: LoadingTag.backup_restore,
title: appLocalizations.restore,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.restore,
message: TextSpan(text: appLocalizations.restoreSuccess),
);
}
Future<void> _handleRestoreOnLocal(BuildContext context) async {
final option = await globalState.showCommonDialog<RestoreOption>(
child: const RestoreOptionsDialog(),
);
if (option == null || !context.mounted) return;
_restoreOnLocal(option);
}
void _handleChange(String? value, WidgetRef ref) {
if (value == null) {
return;
}
ref
.read(davSettingProvider.notifier)
.update((state) => state?.copyWith(fileName: value));
}
Future<void> _handleUpdateRestoreStrategy(WidgetRef ref) async {
final restoreStrategy = ref.read(
appSettingProvider.select((state) => state.restoreStrategy),
);
final res = await globalState.showCommonDialog(
child: OptionsDialog<RestoreStrategy>(
title: appLocalizations.restoreStrategy,
options: RestoreStrategy.values,
textBuilder: (mode) => Intl.message('restoreStrategy_${mode.name}'),
value: restoreStrategy,
),
);
if (res == null) {
return;
}
ref
.read(appSettingProvider.notifier)
.update((state) => state.copyWith(restoreStrategy: res));
}
@override
Widget build(BuildContext context, ref) {
final dav = ref.watch(davSettingProvider);
final isLoading = ref.watch(loadingProvider(LoadingTag.backup_restore));
final client = dav != null ? DAVClient(dav) : null;
return CommonScaffold(
isLoading: isLoading,
title: appLocalizations.backupAndRestore,
body: 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: () {
_handleRestoreOnWebDAV(context, client);
},
title: Text(appLocalizations.restore),
subtitle: Text(appLocalizations.restoreFromWebDAVDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRestoreOnLocal(context);
},
title: Text(appLocalizations.restore),
subtitle: Text(appLocalizations.restoreFromFileDesc),
),
ListHeader(title: appLocalizations.options),
Consumer(
builder: (_, ref, _) {
final restoreStrategy = ref.watch(
appSettingProvider.select((state) => state.restoreStrategy),
);
return ListItem(
onTap: () {
_handleUpdateRestoreStrategy(ref);
},
title: Text(appLocalizations.restoreStrategy),
trailing: FilledButton(
onPressed: () {
_handleUpdateRestoreStrategy(ref);
},
child: Text(
Intl.message('restoreStrategy_${restoreStrategy.name}'),
),
),
);
},
),
],
),
);
}
}
class RestoreOptionsDialog extends StatefulWidget {
const RestoreOptionsDialog({super.key});
@override
State<RestoreOptionsDialog> createState() => _RestoreOptionsDialogState();
}
class _RestoreOptionsDialogState extends State<RestoreOptionsDialog> {
void _handleOnTab(RestoreOption? option) {
if (option == null) return;
Navigator.of(context).pop(option);
}
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.restore,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(RestoreOption.onlyProfiles);
},
title: Text(appLocalizations.restoreOnlyConfig),
),
ListItem(
onTap: () {
_handleOnTab(RestoreOption.all);
},
title: Text(appLocalizations.restoreAllData),
),
],
),
);
}
}
class WebDAVFormDialog extends ConsumerStatefulWidget {
final DAVProps? 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(davSettingProvider.notifier).value = DAVProps(
uri: _uriController.text,
user: _userController.text,
password: _passwordController.text,
);
Navigator.pop(context);
}
void _delete() {
ref.read(davSettingProvider.notifier).value = null;
Navigator.pop(context);
}
@override
void dispose() {
_obscureController.dispose();
_uriController.dispose();
_userController.dispose();
_passwordController.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;
},
);
},
),
],
),
),
);
}
}