mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat: GitHub Gist sync (#854)
This commit is contained in:
@@ -28,10 +28,12 @@ class BackupPage extends StatefulWidget {
|
||||
|
||||
final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveClientMixin {
|
||||
final webdavLoading = false.vn;
|
||||
final gistLoading = false.vn;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
webdavLoading.dispose();
|
||||
gistLoading.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -48,8 +50,10 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
[
|
||||
CenterGreyTitle(libL10n.sync),
|
||||
_buildTip,
|
||||
_buildBakPwd,
|
||||
if (isMacOS || isIOS) _buildIcloud,
|
||||
_buildWebdav,
|
||||
_buildGist,
|
||||
_buildFile,
|
||||
_buildClipboard,
|
||||
],
|
||||
@@ -58,6 +62,82 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
);
|
||||
}
|
||||
|
||||
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(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _ensureBakPwd(BuildContext context) async {
|
||||
final saved = await SecureStoreProps.bakPwd.read();
|
||||
if (saved != null && saved.isNotEmpty) return true;
|
||||
await _onTapSetBakPwd(context);
|
||||
final after = await SecureStoreProps.bakPwd.read();
|
||||
return after != null && after.isNotEmpty;
|
||||
}
|
||||
|
||||
Widget get _buildTip {
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
@@ -102,6 +182,10 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
context.showSnackBar(l10n.autoBackupConflict);
|
||||
return false;
|
||||
}
|
||||
if (p0) {
|
||||
final ok = await _ensureBakPwd(context);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (p0) {
|
||||
await bakSync.sync(rs: icloud);
|
||||
}
|
||||
@@ -133,6 +217,10 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
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();
|
||||
@@ -178,6 +266,67 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
);
|
||||
}
|
||||
|
||||
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(l10n.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(
|
||||
@@ -289,7 +438,9 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
|
||||
final bakName = '$date-${Miscs.bakFileName}';
|
||||
try {
|
||||
final savedPassword = await Stores.setting.backupasswd.read();
|
||||
final ok = await _ensureBakPwd(context);
|
||||
if (!ok) return;
|
||||
final savedPassword = await SecureStoreProps.bakPwd.read();
|
||||
await BackupV2.backup(bakName, savedPassword);
|
||||
await Webdav.shared.upload(relativePath: bakName);
|
||||
Loggers.app.info('Upload webdav backup success');
|
||||
@@ -301,6 +452,85 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
await GistRs.shared.upload(relativePath: bakName);
|
||||
Loggers.app.info('Upload gist backup success');
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s, l10n.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());
|
||||
|
||||
Reference in New Issue
Block a user