mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
* fix: Add SafeArea to the page to prevent content from being obscured Add the SafeArea component to multiple pages to ensure that content is not obscured by the device status bar or navigation bar, thereby enhancing the user experience * fix(ssh page): Fix the issue of the virtual keyboard area being displayed within the security zone Wrap the virtual keyboard area within the SafeArea to prevent it from being obscured by the system UI, and remove any unnecessary bottom padding
669 lines
22 KiB
Dart
669 lines
22 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:computer/computer.dart';
|
|
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:icons_plus/icons_plus.dart';
|
|
import 'package:server_box/core/extension/context/locale.dart';
|
|
import 'package:server_box/core/sync.dart';
|
|
import 'package:server_box/data/model/app/bak/backup2.dart';
|
|
import 'package:server_box/data/model/app/bak/backup_service.dart';
|
|
import 'package:server_box/data/model/app/bak/backup_source.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
import 'package:server_box/data/model/server/snippet.dart';
|
|
import 'package:server_box/data/provider/snippet.dart';
|
|
import 'package:server_box/data/res/misc.dart';
|
|
import 'package:server_box/data/res/store.dart';
|
|
import 'package:webdav_client_plus/webdav_client_plus.dart';
|
|
|
|
class BackupPage extends ConsumerStatefulWidget {
|
|
const BackupPage({super.key});
|
|
|
|
@override
|
|
ConsumerState<BackupPage> createState() => _BackupPageState();
|
|
|
|
static const route = AppRouteNoArg(page: BackupPage.new, path: '/backup');
|
|
}
|
|
|
|
final class _BackupPageState extends ConsumerState<BackupPage> with AutomaticKeepAliveClientMixin {
|
|
final webdavLoading = false.vn;
|
|
final gistLoading = false.vn;
|
|
|
|
@override
|
|
void dispose() {
|
|
webdavLoading.dispose();
|
|
gistLoading.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Scaffold(body: SafeArea(child: _buildBody));
|
|
}
|
|
|
|
Widget get _buildBody {
|
|
return MultiList(
|
|
widthDivider: 2,
|
|
children: [
|
|
[
|
|
CenterGreyTitle(libL10n.sync),
|
|
_buildTip,
|
|
_buildBakPwd,
|
|
if (isMacOS || isIOS) _buildIcloud,
|
|
_buildWebdav,
|
|
_buildGist,
|
|
_buildFile,
|
|
_buildClipboard,
|
|
],
|
|
[CenterGreyTitle(libL10n.import), _buildBulkImportServers, _buildImportSnippet],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget get _buildBakPwd {
|
|
return FutureBuilder<String?>(
|
|
future: SecureStoreProps.bakPwd.read(),
|
|
builder: (context, snapshot) {
|
|
final hasPwd = snapshot.data?.isNotEmpty == true;
|
|
return CardX(
|
|
child: ListTile(
|
|
leading: const Icon(Icons.lock),
|
|
title: Text(l10n.backupPassword),
|
|
subtitle: Text(hasPwd ? l10n.backupEncrypted : l10n.backupNotEncrypted, style: UIs.textGrey),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextButton(onPressed: () async => _onTapSetBakPwd(context), child: Text(libL10n.setting)),
|
|
if (hasPwd) ...[
|
|
UIs.width7,
|
|
TextButton(
|
|
onPressed: () async {
|
|
await SecureStoreProps.bakPwd.write(null);
|
|
context.showSnackBar(l10n.backupPasswordRemoved);
|
|
setState(() {});
|
|
},
|
|
child: Text(libL10n.delete),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
onTap: () async => _onTapSetBakPwd(context),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _onTapSetBakPwd(BuildContext context) async {
|
|
final currentPwd = await SecureStoreProps.bakPwd.read();
|
|
final controller = TextEditingController(text: currentPwd ?? '');
|
|
final node = FocusNode();
|
|
final result = await context.showRoundDialog<bool>(
|
|
title: l10n.backupPassword,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(l10n.backupPasswordTip, style: UIs.textGrey),
|
|
UIs.height13,
|
|
Input(
|
|
label: l10n.backupPassword,
|
|
controller: controller,
|
|
node: node,
|
|
obscureText: true,
|
|
onSubmitted: (_) => context.pop(true),
|
|
),
|
|
],
|
|
),
|
|
actions: Btnx.oks,
|
|
);
|
|
if (result == true) {
|
|
final pwd = controller.text.trim();
|
|
if (pwd.isEmpty) {
|
|
context.showSnackBar(libL10n.empty);
|
|
return;
|
|
}
|
|
await SecureStoreProps.bakPwd.write(pwd);
|
|
context.showSnackBar(l10n.backupPasswordSet);
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
Widget get _buildTip {
|
|
return CardX(
|
|
child: ListTile(
|
|
leading: const Icon(Icons.warning),
|
|
title: Text(libL10n.attention),
|
|
subtitle: Text(l10n.backupTip, style: UIs.textGrey),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildFile {
|
|
return CardX(
|
|
child: ExpandTile(
|
|
leading: const Icon(Icons.file_open),
|
|
title: Text(libL10n.file),
|
|
initiallyExpanded: false,
|
|
children: [
|
|
ListTile(
|
|
title: Text(libL10n.backup),
|
|
trailing: const Icon(Icons.save),
|
|
onTap: () => BackupService.backup(context, FileBackupSource()),
|
|
),
|
|
ListTile(
|
|
trailing: const Icon(Icons.restore),
|
|
title: Text(libL10n.restore),
|
|
onTap: () => BackupService.restore(context, FileBackupSource()),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildIcloud {
|
|
return CardX(
|
|
child: ListTile(
|
|
leading: const Icon(Icons.cloud),
|
|
title: const Text('iCloud'),
|
|
trailing: StoreSwitch(
|
|
prop: PrefProps.icloudSync,
|
|
validator: (p0) async {
|
|
if (p0 && PrefProps.webdavSync.get()) {
|
|
context.showSnackBar(l10n.autoBackupConflict);
|
|
return false;
|
|
}
|
|
if (p0) {
|
|
final ok = await _ensureBakPwd(context);
|
|
if (!ok) return false;
|
|
}
|
|
if (p0) {
|
|
await bakSync.sync(rs: icloud);
|
|
}
|
|
return true;
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildWebdav {
|
|
return CardX(
|
|
child: ExpandTile(
|
|
leading: const Icon(Icons.storage),
|
|
title: const Text('WebDAV'),
|
|
initiallyExpanded: false,
|
|
children: [
|
|
ListTile(
|
|
title: Text(libL10n.setting),
|
|
trailing: const Icon(Icons.settings),
|
|
onTap: () async => _onTapWebdavSetting(context),
|
|
),
|
|
ListTile(
|
|
title: Text(libL10n.auto),
|
|
trailing: StoreSwitch(
|
|
prop: PrefProps.webdavSync,
|
|
validator: (p0) async {
|
|
if (p0 && PrefProps.icloudSync.get()) {
|
|
context.showSnackBar(l10n.autoBackupConflict);
|
|
return false;
|
|
}
|
|
if (p0) {
|
|
final ok = await _ensureBakPwd(context);
|
|
if (!ok) return false;
|
|
}
|
|
if (p0) {
|
|
final url = PrefProps.webdavUrl.get();
|
|
final user = PrefProps.webdavUser.get();
|
|
final pwd = PrefProps.webdavPwd.get();
|
|
|
|
final anyNull = url == null || user == null || pwd == null;
|
|
if (anyNull) {
|
|
context.showSnackBar(l10n.webdavSettingEmpty);
|
|
return false;
|
|
}
|
|
|
|
final anyEmpty = url.isEmpty || user.isEmpty || pwd.isEmpty;
|
|
if (anyEmpty) {
|
|
context.showSnackBar(l10n.webdavSettingEmpty);
|
|
return false;
|
|
}
|
|
|
|
webdavLoading.value = true;
|
|
await bakSync.sync(rs: Webdav.shared);
|
|
webdavLoading.value = false;
|
|
}
|
|
return true;
|
|
},
|
|
),
|
|
),
|
|
ListTile(
|
|
title: Text(libL10n.manual),
|
|
trailing: webdavLoading.listenVal((loading) {
|
|
if (loading) return SizedLoading.small;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextButton(onPressed: () async => _onTapWebdavDl(context), child: Text(libL10n.restore)),
|
|
UIs.width7,
|
|
TextButton(onPressed: () async => _onTapWebdavUp(context), child: Text(libL10n.backup)),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildGist {
|
|
return CardX(
|
|
child: ExpandTile(
|
|
leading: const Icon(Icons.code),
|
|
title: const Text('GitHub Gist'),
|
|
initiallyExpanded: false,
|
|
children: [
|
|
ListTile(
|
|
title: Text(libL10n.setting),
|
|
trailing: const Icon(Icons.settings),
|
|
onTap: () async => _onTapGistSetting(context),
|
|
),
|
|
ListTile(
|
|
title: Text(libL10n.auto),
|
|
trailing: StoreSwitch(
|
|
prop: PrefProps.gistSync,
|
|
validator: (p0) async {
|
|
if (p0 && (PrefProps.icloudSync.get() || PrefProps.webdavSync.get())) {
|
|
context.showSnackBar(l10n.autoBackupConflict);
|
|
return false;
|
|
}
|
|
if (p0) {
|
|
final ok = await _ensureBakPwd(context);
|
|
if (!ok) return false;
|
|
}
|
|
if (p0) {
|
|
final token = PrefProps.githubToken.get();
|
|
// Allow empty gistId (will create one on first upload)
|
|
final hasToken = token != null && token.isNotEmpty;
|
|
if (!hasToken) {
|
|
context.showSnackBar('Token or Gist ID is empty');
|
|
return false;
|
|
}
|
|
gistLoading.value = true;
|
|
await bakSync.sync(rs: GistRs.shared);
|
|
gistLoading.value = false;
|
|
}
|
|
return true;
|
|
},
|
|
),
|
|
),
|
|
ListTile(
|
|
title: Text(libL10n.manual),
|
|
trailing: gistLoading.listenVal((loading) {
|
|
if (loading) return SizedLoading.small;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextButton(onPressed: () async => _onTapGistDl(context), child: Text(libL10n.restore)),
|
|
UIs.width7,
|
|
TextButton(onPressed: () async => _onTapGistUp(context), child: Text(libL10n.backup)),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildClipboard {
|
|
return CardX(
|
|
child: ExpandTile(
|
|
leading: const Icon(Icons.content_paste),
|
|
title: Text(libL10n.clipboard),
|
|
children: [
|
|
ListTile(
|
|
title: Text(libL10n.backup),
|
|
trailing: const Icon(Icons.save),
|
|
onTap: () => BackupService.backup(context, ClipboardBackupSource()),
|
|
),
|
|
ListTile(
|
|
trailing: const Icon(Icons.restore),
|
|
title: Text(libL10n.restore),
|
|
onTap: () => BackupService.restore(context, ClipboardBackupSource()),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildBulkImportServers {
|
|
return CardX(
|
|
child: ListTile(
|
|
title: Text(l10n.server),
|
|
leading: const Icon(BoxIcons.bx_server),
|
|
onTap: () => _onBulkImportServers(context),
|
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget get _buildImportSnippet {
|
|
return ListTile(
|
|
title: Text(l10n.snippet),
|
|
leading: const Icon(MingCute.code_line),
|
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
onTap: () async {
|
|
final data = await context.showImportDialog(title: l10n.snippet, modelDef: Snippet.example.toJson());
|
|
if (data == null) return;
|
|
String str;
|
|
try {
|
|
str = utf8.decode(data);
|
|
} on FormatException catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.error);
|
|
return;
|
|
}
|
|
final (list, _) = await context.showLoadingDialog(
|
|
fn: () => Computer.shared.start((s) {
|
|
return json.decode(s) as List;
|
|
}, str),
|
|
);
|
|
if (list == null || list.isEmpty) return;
|
|
final snippets = <Snippet>[];
|
|
final errs = <String>[];
|
|
for (final item in list) {
|
|
try {
|
|
final snippet = Snippet.fromJson(item);
|
|
snippets.add(snippet);
|
|
} catch (e) {
|
|
errs.add(e.toString());
|
|
}
|
|
}
|
|
if (snippets.isEmpty) {
|
|
context.showSnackBar(libL10n.empty);
|
|
return;
|
|
}
|
|
if (errs.isNotEmpty) {
|
|
context.showRoundDialog(
|
|
title: libL10n.error,
|
|
child: SingleChildScrollView(child: Text(errs.join('\n'))),
|
|
);
|
|
return;
|
|
}
|
|
final snippetNames = snippets.map((e) => e.name).join(', ');
|
|
context.showRoundDialog(
|
|
title: libL10n.attention,
|
|
child: SingleChildScrollView(child: Text(libL10n.askContinue('${libL10n.import} [$snippetNames]'))),
|
|
actions: Btn.ok(
|
|
onTap: () {
|
|
final notifier = ref.read(snippetProvider.notifier);
|
|
for (final snippet in snippets) {
|
|
notifier.add(snippet);
|
|
}
|
|
context.pop();
|
|
context.pop();
|
|
},
|
|
).toList,
|
|
);
|
|
},
|
|
).cardx;
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
extension on _BackupPageState {
|
|
Future<void> _onTapWebdavDl(BuildContext context) async {
|
|
webdavLoading.value = true;
|
|
try {
|
|
final files = await Webdav.shared.list();
|
|
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
|
|
|
|
final fileName = await context.showPickSingleDialog(title: libL10n.restore, items: files);
|
|
if (fileName == null) return;
|
|
|
|
await Webdav.shared.download(relativePath: fileName);
|
|
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
|
await BackupService.restoreFromText(context, dlFile);
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.restore);
|
|
Loggers.app.warning('Download webdav backup failed', e, s);
|
|
} finally {
|
|
webdavLoading.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _onTapWebdavUp(BuildContext context) async {
|
|
webdavLoading.value = true;
|
|
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
|
|
final bakName = '$date-${Miscs.bakFileName}';
|
|
try {
|
|
final ok = await _ensureBakPwd(context);
|
|
if (!ok) return;
|
|
final savedPassword = await SecureStoreProps.bakPwd.read();
|
|
await BackupV2.backup(bakName, savedPassword?.isEmpty == true ? null : savedPassword);
|
|
await Webdav.shared.upload(relativePath: bakName);
|
|
Loggers.app.info('Upload webdav backup success');
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.upload);
|
|
Loggers.app.warning('Upload webdav backup failed', e, s);
|
|
} finally {
|
|
webdavLoading.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _onTapGistDl(BuildContext context) async {
|
|
gistLoading.value = true;
|
|
try {
|
|
final files = await GistRs.shared.list();
|
|
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
|
|
|
|
final fileName = await context.showPickSingleDialog(title: libL10n.restore, items: files);
|
|
if (fileName == null) return;
|
|
|
|
await GistRs.shared.download(relativePath: fileName);
|
|
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
|
await BackupService.restoreFromText(context, dlFile);
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.restore);
|
|
Loggers.app.warning('Download gist backup failed', e, s);
|
|
} finally {
|
|
gistLoading.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _onTapGistUp(BuildContext context) async {
|
|
gistLoading.value = true;
|
|
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
|
|
final bakName = '$date-${Miscs.bakFileName}';
|
|
try {
|
|
final ok = await _ensureBakPwd(context);
|
|
if (!ok) return;
|
|
final savedPassword = await SecureStoreProps.bakPwd.read();
|
|
await BackupV2.backup(bakName, savedPassword?.isEmpty == true ? null : savedPassword);
|
|
await GistRs.shared.upload(relativePath: bakName);
|
|
Loggers.app.info('Upload gist backup success');
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.upload);
|
|
Loggers.app.warning('Upload gist backup failed', e, s);
|
|
} finally {
|
|
gistLoading.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _onTapGistSetting(BuildContext context) async {
|
|
final tokenCtrl = TextEditingController(text: PrefProps.githubToken.get());
|
|
final gistIdCtrl = TextEditingController(text: PrefProps.gistId.get());
|
|
final nodeToken = FocusNode();
|
|
final result = await context.showRoundDialog<bool>(
|
|
title: 'GitHub Gist',
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Input(label: 'Token', controller: tokenCtrl, suggestion: false, node: nodeToken),
|
|
Input(
|
|
label: 'Gist ID (optional)',
|
|
controller: gistIdCtrl,
|
|
suggestion: false,
|
|
onSubmitted: (_) => context.pop(true),
|
|
),
|
|
],
|
|
),
|
|
actions: Btnx.oks,
|
|
);
|
|
if (result == true) {
|
|
try {
|
|
final token_ = tokenCtrl.text.trim();
|
|
final gistId_ = gistIdCtrl.text.trim();
|
|
|
|
await GistRs.test(token: token_, gistId: gistId_.isEmpty ? null : gistId_);
|
|
context.showSnackBar(libL10n.success);
|
|
|
|
await PrefProps.githubToken.set(token_);
|
|
if (gistId_.isEmpty) {
|
|
await PrefProps.gistId.remove();
|
|
} else {
|
|
await PrefProps.gistId.set(gistId_);
|
|
}
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, 'Gist');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _onTapWebdavSetting(BuildContext context) async {
|
|
final url = TextEditingController(text: PrefProps.webdavUrl.get());
|
|
final user = TextEditingController(text: PrefProps.webdavUser.get());
|
|
final pwd = TextEditingController(text: PrefProps.webdavPwd.get());
|
|
final nodeUser = FocusNode();
|
|
final nodePwd = FocusNode();
|
|
final result = await context.showRoundDialog<bool>(
|
|
title: 'WebDAV',
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Input(
|
|
label: 'URL',
|
|
hint: 'https://example.com/sub/',
|
|
controller: url,
|
|
suggestion: false,
|
|
onSubmitted: (p0) => FocusScope.of(context).requestFocus(nodeUser),
|
|
),
|
|
Input(
|
|
label: libL10n.user,
|
|
controller: user,
|
|
node: nodeUser,
|
|
suggestion: false,
|
|
onSubmitted: (p0) => FocusScope.of(context).requestFocus(nodePwd),
|
|
),
|
|
Input(
|
|
label: libL10n.pwd,
|
|
controller: pwd,
|
|
node: nodePwd,
|
|
suggestion: false,
|
|
onSubmitted: (_) => context.pop(true),
|
|
),
|
|
],
|
|
),
|
|
actions: Btnx.oks,
|
|
);
|
|
if (result == true) {
|
|
try {
|
|
final url_ = url.text;
|
|
final user_ = user.text;
|
|
final pwd_ = pwd.text;
|
|
|
|
await Webdav.test(url_, user_, pwd_);
|
|
context.showSnackBar(libL10n.success);
|
|
|
|
Webdav.shared.client = WebdavClient.basicAuth(url: url_, user: user_, pwd: pwd_);
|
|
PrefProps.webdavUrl.set(url_);
|
|
PrefProps.webdavUser.set(user_);
|
|
PrefProps.webdavPwd.set(pwd_);
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, 'Webdav');
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onBulkImportServers(BuildContext context) async {
|
|
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
|
|
if (data == null) return;
|
|
String text;
|
|
try {
|
|
text = utf8.decode(data);
|
|
} on FormatException catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.error);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final (spis, err) = await context.showLoadingDialog(
|
|
fn: () => Computer.shared.start((val) {
|
|
final list = json.decode(val) as List;
|
|
return list.map((e) => Spi.fromJson(e)).toList();
|
|
}, text.trim()),
|
|
);
|
|
if (err != null || spis == null) return;
|
|
final sure = await context.showRoundDialog<bool>(
|
|
title: libL10n.import,
|
|
child: Text(libL10n.askContinue('${spis.length} ${l10n.server}')),
|
|
actions: Btnx.oks,
|
|
);
|
|
if (sure == true) {
|
|
final (suc, err) = await context.showLoadingDialog(
|
|
fn: () async {
|
|
final usedIds = <String>{};
|
|
for (var spi in spis) {
|
|
// Ensure each server has a unique ID
|
|
|
|
// Only generate a new ID if the imported one is empty or already used in importing stage
|
|
final isIdUsed = spi.id.isNotEmpty || usedIds.contains(spi.id);
|
|
final spiWithId = isIdUsed ? spi.copyWith(id: ShortId.generate()) : spi;
|
|
Stores.server.put(spiWithId);
|
|
usedIds.add(spiWithId.id);
|
|
}
|
|
return true;
|
|
},
|
|
);
|
|
if (err != null || suc != true) return;
|
|
context.showSnackBar(libL10n.success);
|
|
}
|
|
} catch (e, s) {
|
|
context.showErrDialog(e, s, libL10n.import);
|
|
Loggers.app.warning('Import servers failed', e, s);
|
|
}
|
|
}
|
|
|
|
Future<bool> _ensureBakPwd(BuildContext context) async {
|
|
final saved = await SecureStoreProps.bakPwd.read();
|
|
if (saved != null && saved.isNotEmpty) return true;
|
|
|
|
// Show dialog asking if user wants to set password or continue without
|
|
final result = await context.showRoundDialog<bool>(
|
|
title: l10n.backupPassword,
|
|
child: Text(l10n.backupPasswordTip, style: UIs.textGrey),
|
|
actions: [
|
|
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.cancel)),
|
|
TextButton(onPressed: () => context.pop(false), child: Text(libL10n.setting)),
|
|
],
|
|
);
|
|
|
|
if (result == true) {
|
|
// Continue without password
|
|
return true;
|
|
} else if (result == false) {
|
|
// User wants to set password
|
|
await _onTapSetBakPwd(context);
|
|
return true; // Allow continuing even if password setting was cancelled
|
|
}
|
|
|
|
return false; // User cancelled the dialog
|
|
}
|
|
}
|