mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-21 15:44:30 +01:00
opt.: backup
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../data/model/app/backup.dart';
|
||||
import '../../data/res/path.dart';
|
||||
import '../../data/store/docker.dart';
|
||||
import '../../data/store/private_key.dart';
|
||||
import '../../data/store/server.dart';
|
||||
import '../../data/store/setting.dart';
|
||||
import '../../data/store/snippet.dart';
|
||||
import '../../locator.dart';
|
||||
|
||||
final _server = locator<ServerStore>();
|
||||
final _snippet = locator<SnippetStore>();
|
||||
final _privateKey = locator<PrivateKeyStore>();
|
||||
final _dockerHosts = locator<DockerStore>();
|
||||
final _setting = locator<SettingStore>();
|
||||
|
||||
Future<String> get backupPath async => '${await docDir}/srvbox_bak.json';
|
||||
|
||||
const backupFormatVersion = 1;
|
||||
|
||||
Future<void> backup() async {
|
||||
final result = _diyEncrtpt(
|
||||
json.encode(
|
||||
Backup(
|
||||
version: backupFormatVersion,
|
||||
date: DateTime.now().toString().split('.').first,
|
||||
spis: _server.fetch(),
|
||||
snippets: _snippet.fetch(),
|
||||
keys: _privateKey.fetch(),
|
||||
dockerHosts: _dockerHosts.fetchAll(),
|
||||
settings: _setting.toJson(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await File(await backupPath).writeAsString(result);
|
||||
}
|
||||
|
||||
void restore(Backup backup) {
|
||||
for (final s in backup.snippets) {
|
||||
_snippet.put(s);
|
||||
}
|
||||
for (final s in backup.spis) {
|
||||
_server.put(s);
|
||||
}
|
||||
for (final s in backup.keys) {
|
||||
_privateKey.put(s);
|
||||
}
|
||||
for (final k in backup.dockerHosts.keys) {
|
||||
final val = backup.dockerHosts[k];
|
||||
if (val != null && val is String && val.isNotEmpty) {
|
||||
_dockerHosts.put(k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Backup> decodeBackup(String raw) async {
|
||||
return await compute(_decode, raw);
|
||||
}
|
||||
|
||||
Backup _decode(String raw) {
|
||||
final decrypted = _diyDecrypt(raw);
|
||||
return Backup.fromJson(json.decode(decrypted));
|
||||
}
|
||||
|
||||
String _diyEncrtpt(String raw) =>
|
||||
json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
|
||||
String _diyDecrypt(String raw) {
|
||||
final list = json.decode(raw);
|
||||
final sb = StringBuffer();
|
||||
for (final e in list) {
|
||||
sb.writeCharCode((e - 1) ~/ 2);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -100,6 +100,9 @@ class ICloud {
|
||||
static Future<Iterable<ICloudErr>?> sync({
|
||||
required Iterable<String> relativePaths,
|
||||
}) async {
|
||||
final uploadFiles = <String>[];
|
||||
final downloadFiles = <String>[];
|
||||
|
||||
try {
|
||||
final errs = <ICloudErr>[];
|
||||
|
||||
@@ -147,11 +150,13 @@ class ICloud {
|
||||
|
||||
/// Local is newer than remote, so upload local file
|
||||
if (remoteDate.isBefore(localDate)) {
|
||||
await delete(relativePath);
|
||||
final err = await upload(relativePath: relativePath);
|
||||
if (err != null) {
|
||||
errs.add(err);
|
||||
}
|
||||
//_logger.info('local newer: $relativePath');
|
||||
uploadFiles.add(relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,6 +166,7 @@ class ICloud {
|
||||
errs.add(err);
|
||||
}
|
||||
//_logger.info('remote newer: $relativePath');
|
||||
downloadFiles.add(relativePath);
|
||||
}));
|
||||
|
||||
await Future.wait(mission);
|
||||
@@ -170,18 +176,18 @@ class ICloud {
|
||||
_logger.warning('Sync failed: $relativePaths', e, s);
|
||||
return [ICloudErr(type: ICloudErrType.generic, message: '$e')];
|
||||
} finally {
|
||||
_logger.info('Sync finished.');
|
||||
_logger.info('Sync upload: $uploadFiles, download: $downloadFiles');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> syncApple() async {
|
||||
if (!isIOS && !isMacOS) return;
|
||||
final docPath = await docDir;
|
||||
final dir = Directory(docPath);
|
||||
final files = await dir.list().toList();
|
||||
// filter out non-hive(db) files
|
||||
files.removeWhere((e) => !e.path.endsWith('.hive'));
|
||||
final paths = files.map((e) => e.path.replaceFirst('$docPath/', ''));
|
||||
await ICloud.sync(relativePaths: paths);
|
||||
static Future<void> syncDb() async {
|
||||
if (!isIOS && !isMacOS) return;
|
||||
final docPath = await docDir;
|
||||
final dir = Directory(docPath);
|
||||
final files = await dir.list().toList();
|
||||
// filter out non-hive(db) files
|
||||
files.removeWhere((e) => !e.path.endsWith('.hive'));
|
||||
final paths = files.map((e) => e.path.replaceFirst('$docPath/', ''));
|
||||
await ICloud.sync(relativePaths: paths);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../data/provider/app.dart';
|
||||
import '../../locator.dart';
|
||||
import '../../view/widget/rebuild.dart';
|
||||
import 'platform.dart';
|
||||
|
||||
final _app = locator<AppProvider>();
|
||||
@@ -64,10 +63,7 @@ String? getFileName(String? path) {
|
||||
return path.split('/').last;
|
||||
}
|
||||
|
||||
void rebuildAll(BuildContext context) {
|
||||
RebuildWidget.restartApp(context);
|
||||
}
|
||||
|
||||
/// Return fmt: 2021-01-01 00:00:00
|
||||
String getTime(int? unixMill) {
|
||||
return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000)
|
||||
.toString()
|
||||
|
||||
@@ -3,125 +3,17 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:toolbox/core/extension/context.dart';
|
||||
import 'package:toolbox/data/model/app/tab.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../data/model/server/snippet.dart';
|
||||
import '../../data/provider/snippet.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../../locator.dart';
|
||||
import '../../view/widget/input_field.dart';
|
||||
import '../../view/widget/picker.dart';
|
||||
import '../persistant_store.dart';
|
||||
import '../route.dart';
|
||||
import 'misc.dart';
|
||||
import 'platform.dart';
|
||||
import '../extension/stringx.dart';
|
||||
import '../extension/uint8list.dart';
|
||||
|
||||
void showSnackBar(BuildContext context, Widget child) =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: child,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
|
||||
void showSnackBarWithAction(
|
||||
BuildContext context,
|
||||
String content,
|
||||
String action,
|
||||
GestureTapCallback onTap,
|
||||
) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
action: SnackBarAction(
|
||||
label: action,
|
||||
onPressed: onTap,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void showRestartSnackbar(BuildContext context, {String? btn, String? msg}) {
|
||||
showSnackBarWithAction(
|
||||
context,
|
||||
msg ?? 'Need restart to take effect',
|
||||
btn ?? 'Restart',
|
||||
() => rebuildAll(context),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> openUrl(String url) async {
|
||||
return await launchUrl(url.uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
||||
Future<T?> showRoundDialog<T>({
|
||||
required BuildContext context,
|
||||
Widget? child,
|
||||
List<Widget>? actions,
|
||||
Widget? title,
|
||||
bool barrierDismiss = true,
|
||||
}) async {
|
||||
return await showDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismiss,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
content: child,
|
||||
actions: actions,
|
||||
actionsPadding: const EdgeInsets.all(17),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showLoadingDialog(BuildContext context, {bool barrierDismiss = false}) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: centerSizedLoading,
|
||||
barrierDismiss: barrierDismiss,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> showPwdDialog(
|
||||
BuildContext context,
|
||||
String? user,
|
||||
) async {
|
||||
if (!context.mounted) return null;
|
||||
final s = S.of(context)!;
|
||||
return await showRoundDialog<String>(
|
||||
context: context,
|
||||
title: Text(user ?? s.pwd),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
type: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
onSubmitted: (val) => context.pop(val.trim()),
|
||||
label: s.pwd,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSwitch(
|
||||
BuildContext context,
|
||||
StorePropertyBase<bool> prop, {
|
||||
void Function(bool)? func,
|
||||
}) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: prop.listenable(),
|
||||
builder: (context, bool value, widget) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (func != null) func(value);
|
||||
prop.put(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void setTransparentNavigationBar(BuildContext context) {
|
||||
if (isAndroid) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
@@ -133,18 +25,6 @@ void setTransparentNavigationBar(BuildContext context) {
|
||||
}
|
||||
}
|
||||
|
||||
String tabTitleName(BuildContext context, AppTab tab) {
|
||||
final s = S.of(context)!;
|
||||
switch (tab) {
|
||||
case AppTab.server:
|
||||
return s.server;
|
||||
case AppTab.snippet:
|
||||
return s.convert;
|
||||
case AppTab.ping:
|
||||
return 'Ping';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadFontFile(String localPath) async {
|
||||
if (localPath.isEmpty) return;
|
||||
final name = getFileName(localPath);
|
||||
@@ -156,53 +36,6 @@ Future<void> loadFontFile(String localPath) async {
|
||||
await fontLoader.load();
|
||||
}
|
||||
|
||||
void showSnippetDialog(
|
||||
BuildContext context,
|
||||
S s,
|
||||
void Function(Snippet s) onSelected,
|
||||
) {
|
||||
final provider = locator<SnippetProvider>();
|
||||
if (provider.snippets.isEmpty) {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: Text(s.noSavedSnippet),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(s.ok),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
AppRoute.snippetEdit().go(context);
|
||||
},
|
||||
child: Text(s.add),
|
||||
)
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var snippet = provider.snippets.first;
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(s.choose),
|
||||
child: Picker(
|
||||
items: provider.snippets.map((e) => Text(e.name)).toList(),
|
||||
onSelected: (idx) => snippet = provider.snippets[idx],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
onSelected(snippet);
|
||||
},
|
||||
child: Text(s.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void switchStatusBar({required bool hide}) {
|
||||
if (hide) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky,
|
||||
|
||||
Reference in New Issue
Block a user