mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat. & fix.
- support backup & restore - fix when client.run empty return
This commit is contained in:
52
lib/data/model/app/backup.dart
Normal file
52
lib/data/model/app/backup.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
|
||||
class Backup {
|
||||
// backup format version
|
||||
final int version;
|
||||
final String date;
|
||||
final List<ServerPrivateInfo> spis;
|
||||
final List<Snippet> snippets;
|
||||
final List<PrivateKeyInfo> keys;
|
||||
final int primaryColor;
|
||||
final int serverStatusUpdateInterval;
|
||||
final int launchPage;
|
||||
|
||||
Backup(
|
||||
this.version,
|
||||
this.date,
|
||||
this.spis,
|
||||
this.snippets,
|
||||
this.keys,
|
||||
this.primaryColor,
|
||||
this.serverStatusUpdateInterval,
|
||||
this.launchPage,
|
||||
);
|
||||
|
||||
Backup.fromJson(Map<String, dynamic> json)
|
||||
: version = json['version'] as int,
|
||||
date = json['date'],
|
||||
spis = (json['spis'] as List)
|
||||
.map((e) => ServerPrivateInfo.fromJson(e))
|
||||
.toList(),
|
||||
snippets =
|
||||
(json['snippets'] as List).map((e) => Snippet.fromJson(e)).toList(),
|
||||
keys = (json['keys'] as List)
|
||||
.map((e) => PrivateKeyInfo.fromJson(e))
|
||||
.toList(),
|
||||
primaryColor = json['primaryColor'],
|
||||
serverStatusUpdateInterval = json['serverStatusUpdateInterval'],
|
||||
launchPage = json['launchPage'];
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'version': version,
|
||||
'date': date,
|
||||
'spis': spis,
|
||||
'snippets': snippets,
|
||||
'keys': keys,
|
||||
'primaryColor': primaryColor,
|
||||
'serverStatusUpdateInterval': serverStatusUpdateInterval,
|
||||
'launchPage': launchPage,
|
||||
};
|
||||
}
|
||||
@@ -193,6 +193,12 @@ class ServerProvider extends BusyProvider {
|
||||
final si = _servers[idx];
|
||||
if (si.client == null) return;
|
||||
final raw = await si.client!.run("sh $shellPath").string;
|
||||
if (raw.isEmpty) {
|
||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
||||
_servers[idx].status.failedInfo = 'Empty output';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
final lines = raw.split(seperator).map((e) => e.trim()).toList();
|
||||
lines.removeAt(0);
|
||||
|
||||
|
||||
@@ -43,17 +43,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
|
||||
static String m9(url) => "Please report bugs on ${url}";
|
||||
|
||||
static String m10(time) => "Spent time: ${time}";
|
||||
static String m10(date) => "Are you sure to restore from ${date} ?";
|
||||
|
||||
static String m11(name) => "Are you sure to delete [${name}]?";
|
||||
static String m11(time) => "Spent time: ${time}";
|
||||
|
||||
static String m12(server) => "Are you sure to delete server [${server}]?";
|
||||
static String m12(name) => "Are you sure to delete [${name}]?";
|
||||
|
||||
static String m13(build) => "Found: v1.0.${build}, click to update";
|
||||
static String m13(server) => "Are you sure to delete server [${server}]?";
|
||||
|
||||
static String m14(build) => "Current: v1.0.${build}";
|
||||
static String m14(build) => "Found: v1.0.${build}, click to update";
|
||||
|
||||
static String m15(build) => "Current: v1.0.${build}, is up to date";
|
||||
static String m15(build) => "Current: v1.0.${build}";
|
||||
|
||||
static String m16(build) => "Current: v1.0.${build}, is up to date";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
@@ -67,6 +69,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("App primary color"),
|
||||
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
|
||||
"backDir": MessageLookupByLibrary.simpleMessage("Back"),
|
||||
"backup": MessageLookupByLibrary.simpleMessage("Backup"),
|
||||
"backupTip": MessageLookupByLibrary.simpleMessage(
|
||||
"The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting)."),
|
||||
"backupVersionNotMatch": MessageLookupByLibrary.simpleMessage(
|
||||
"Backup version is not match."),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
|
||||
"chooseDestination":
|
||||
@@ -116,6 +123,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"install": MessageLookupByLibrary.simpleMessage("install"),
|
||||
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||
"Please https://docs.docker.com/engine/install docker first."),
|
||||
"invalidJson": MessageLookupByLibrary.simpleMessage("Invalid JSON"),
|
||||
"invalidVersionHelp": m7,
|
||||
"keepForeground":
|
||||
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
|
||||
@@ -163,6 +171,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
|
||||
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
|
||||
"reportBugsOnGithubIssue": m9,
|
||||
"restoreSuccess": MessageLookupByLibrary.simpleMessage(
|
||||
"Restore success. Restart app to apply."),
|
||||
"restoreSureWithDate": m10,
|
||||
"result": MessageLookupByLibrary.simpleMessage("Result"),
|
||||
"run": MessageLookupByLibrary.simpleMessage("Run"),
|
||||
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
||||
@@ -186,11 +197,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"sftpSSHConnected":
|
||||
MessageLookupByLibrary.simpleMessage("SFTP Connected"),
|
||||
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
|
||||
"spentTime": m10,
|
||||
"spentTime": m11,
|
||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
|
||||
"sureDelete": m11,
|
||||
"sureToDeleteServer": m12,
|
||||
"sureDelete": m12,
|
||||
"sureToDeleteServer": m13,
|
||||
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
|
||||
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
||||
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
||||
@@ -204,9 +215,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
|
||||
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
|
||||
"user": MessageLookupByLibrary.simpleMessage("User"),
|
||||
"versionHaveUpdate": m13,
|
||||
"versionUnknownUpdate": m14,
|
||||
"versionUpdated": m15,
|
||||
"versionHaveUpdate": m14,
|
||||
"versionUnknownUpdate": m15,
|
||||
"versionUpdated": m16,
|
||||
"waitConnection": MessageLookupByLibrary.simpleMessage(
|
||||
"Please wait for the connection to be established."),
|
||||
"willTakEeffectImmediately":
|
||||
|
||||
@@ -43,17 +43,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
|
||||
static String m9(url) => "请到 ${url} 提交问题";
|
||||
|
||||
static String m10(time) => "耗时: ${time}";
|
||||
static String m10(date) => "确定恢复 ${date} 的备份吗?";
|
||||
|
||||
static String m11(name) => "确定删除[${name}]?";
|
||||
static String m11(time) => "耗时: ${time}";
|
||||
|
||||
static String m12(server) => "你确定要删除服务器 [${server}] 吗?";
|
||||
static String m12(name) => "确定删除[${name}]?";
|
||||
|
||||
static String m13(build) => "找到新版本:v1.0.${build}, 点击更新";
|
||||
static String m13(server) => "你确定要删除服务器 [${server}] 吗?";
|
||||
|
||||
static String m14(build) => "当前:v1.0.${build}";
|
||||
static String m14(build) => "找到新版本:v1.0.${build}, 点击更新";
|
||||
|
||||
static String m15(build) => "当前:v1.0.${build}, 已是最新版本";
|
||||
static String m15(build) => "当前:v1.0.${build}";
|
||||
|
||||
static String m16(build) => "当前:v1.0.${build}, 已是最新版本";
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
@@ -64,6 +66,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
|
||||
"attention": MessageLookupByLibrary.simpleMessage("注意"),
|
||||
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
|
||||
"backup": MessageLookupByLibrary.simpleMessage("备份"),
|
||||
"backupTip": MessageLookupByLibrary.simpleMessage(
|
||||
"导出的数据仅进行了简单加密,请妥善保管。\n恢复的数据(除了设置)不会覆盖现有数据。"),
|
||||
"backupVersionNotMatch":
|
||||
MessageLookupByLibrary.simpleMessage("备份版本不匹配,无法恢复"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"choose": MessageLookupByLibrary.simpleMessage("选择"),
|
||||
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
|
||||
@@ -105,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"install": MessageLookupByLibrary.simpleMessage("安装"),
|
||||
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||
"请先 https://docs.docker.com/engine/install docker"),
|
||||
"invalidJson": MessageLookupByLibrary.simpleMessage("无效的json,存在格式问题"),
|
||||
"invalidVersionHelp": m7,
|
||||
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
|
||||
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
|
||||
@@ -142,6 +150,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
|
||||
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
|
||||
"reportBugsOnGithubIssue": m9,
|
||||
"restoreSuccess":
|
||||
MessageLookupByLibrary.simpleMessage("恢复成功,需要重启App来应用更改"),
|
||||
"restoreSureWithDate": m10,
|
||||
"result": MessageLookupByLibrary.simpleMessage("结果"),
|
||||
"run": MessageLookupByLibrary.simpleMessage("运行"),
|
||||
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
||||
@@ -160,11 +171,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"sftpSSHConnected":
|
||||
MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."),
|
||||
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
|
||||
"spentTime": m10,
|
||||
"spentTime": m11,
|
||||
"start": MessageLookupByLibrary.simpleMessage("开始"),
|
||||
"stop": MessageLookupByLibrary.simpleMessage("停止"),
|
||||
"sureDelete": m11,
|
||||
"sureToDeleteServer": m12,
|
||||
"sureDelete": m12,
|
||||
"sureToDeleteServer": m13,
|
||||
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
||||
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
||||
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
||||
@@ -177,9 +188,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
|
||||
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
|
||||
"user": MessageLookupByLibrary.simpleMessage("用户"),
|
||||
"versionHaveUpdate": m13,
|
||||
"versionUnknownUpdate": m14,
|
||||
"versionUpdated": m15,
|
||||
"versionHaveUpdate": m14,
|
||||
"versionUnknownUpdate": m15,
|
||||
"versionUpdated": m16,
|
||||
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
|
||||
"willTakEeffectImmediately":
|
||||
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
|
||||
|
||||
@@ -1230,6 +1230,66 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).`
|
||||
String get backupTip {
|
||||
return Intl.message(
|
||||
'The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).',
|
||||
name: 'backupTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Backup`
|
||||
String get backup {
|
||||
return Intl.message(
|
||||
'Backup',
|
||||
name: 'backup',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Are you sure to restore from {date} ?`
|
||||
String restoreSureWithDate(Object date) {
|
||||
return Intl.message(
|
||||
'Are you sure to restore from $date ?',
|
||||
name: 'restoreSureWithDate',
|
||||
desc: '',
|
||||
args: [date],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Backup version is not match.`
|
||||
String get backupVersionNotMatch {
|
||||
return Intl.message(
|
||||
'Backup version is not match.',
|
||||
name: 'backupVersionNotMatch',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Invalid JSON`
|
||||
String get invalidJson {
|
||||
return Intl.message(
|
||||
'Invalid JSON',
|
||||
name: 'invalidJson',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Restore success. Restart app to apply.`
|
||||
String get restoreSuccess {
|
||||
return Intl.message(
|
||||
'Restore success. Restart app to apply.',
|
||||
name: 'restoreSuccess',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -116,5 +116,11 @@
|
||||
"invalidVersionHelp": "Please make sure that docker is installed correctly, or that you are using a non-self-compiled version. If you don't have the above issues, please submit an issue on {url}.",
|
||||
"noInterface": "No interface",
|
||||
"lastTry": "Last try!",
|
||||
"pingNoServer": "No server to ping.\nPlease add a server in server tab."
|
||||
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
|
||||
"backupTip": "The exported data is simply encrypted. \nPlease keep it safe.\nRestoring will not overwrite existing data (except setting).",
|
||||
"backup": "Backup",
|
||||
"restoreSureWithDate": "Are you sure to restore from {date} ?",
|
||||
"backupVersionNotMatch": "Backup version is not match.",
|
||||
"invalidJson": "Invalid JSON",
|
||||
"restoreSuccess": "Restore success. Restart app to apply."
|
||||
}
|
||||
@@ -116,5 +116,11 @@
|
||||
"invalidVersionHelp": "请确保正确安装了docker,或者使用的非自编译版本。如果没有以上问题,请在 {url} 提交问题。",
|
||||
"noInterface": "没有可用的接口",
|
||||
"lastTry": "最后尝试",
|
||||
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试"
|
||||
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
|
||||
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。\n恢复的数据(除了设置)不会覆盖现有数据。",
|
||||
"backup": "备份",
|
||||
"restoreSureWithDate": "确定恢复 {date} 的备份吗?",
|
||||
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
|
||||
"invalidJson": "无效的json,存在格式问题",
|
||||
"restoreSuccess": "恢复成功,需要重启App来应用更改"
|
||||
}
|
||||
201
lib/view/page/backup.dart
Normal file
201
lib/view/page/backup.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/data/store/snippet.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
const backupFormatVersion = 1;
|
||||
|
||||
class BackupPage extends StatelessWidget {
|
||||
BackupPage({Key? key}) : super(key: key);
|
||||
|
||||
final setting = locator<SettingStore>();
|
||||
final server = locator<ServerStore>();
|
||||
final snippet = locator<SnippetStore>();
|
||||
final privateKey = locator<PrivateKeyStore>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final media = MediaQuery.of(context);
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(s.importAndExport, style: size18),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(37),
|
||||
child: Text(
|
||||
s.backupTip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 77,
|
||||
),
|
||||
_buildCard(s.import, Icons.download, media,
|
||||
() => _showImportDialog(context, s)),
|
||||
_buildCard(s.export, Icons.file_upload, media,
|
||||
() => _showExportDialog(context, s))
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(String text, IconData icon, MediaQueryData media,
|
||||
FutureOr Function() onTap) {
|
||||
return RoundRectCard(InkWell(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: media.size.width * 0.77,
|
||||
height: media.size.height * 0.17,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: media.size.height * 0.05),
|
||||
const SizedBox(height: 10),
|
||||
Text(text, style: TextStyle(fontSize: media.size.height * 0.02)),
|
||||
],
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
Future<void> _showExportDialog(BuildContext context, S s) async {
|
||||
final exportFieldController = TextEditingController()
|
||||
..text = _diyEncrtpt(json.encode(Backup(
|
||||
backupFormatVersion,
|
||||
DateTime.now().toString().split('.').first,
|
||||
server.fetch(),
|
||||
snippet.fetch(),
|
||||
privateKey.fetch(),
|
||||
setting.primaryColor.fetch() ?? Colors.pinkAccent.value,
|
||||
setting.serverStatusUpdateInterval.fetch() ?? 2,
|
||||
setting.launchPage.fetch() ?? 0,
|
||||
)));
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.export,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 7,
|
||||
controller: exportFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
child: Text(s.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: exportFieldController.text));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _showImportDialog(BuildContext context, S s) async {
|
||||
final importFieldController = TextEditingController();
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.import,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 3,
|
||||
controller: importFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
await _import(importFieldController.text.trim(), context, s),
|
||||
child: const Text('GO'),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _import(String text, BuildContext context, S s) async {
|
||||
if (text.isEmpty) {
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
_importBackup(text, context, s);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Future<void> _importBackup(String raw, BuildContext context, S s) async {
|
||||
try {
|
||||
final backup = await compute(_decode, raw);
|
||||
if (backupFormatVersion != backup.version) {
|
||||
showSnackBar(context, Text(s.backupVersionNotMatch));
|
||||
return;
|
||||
}
|
||||
|
||||
await showRoundDialog(
|
||||
context, s.attention, Text(s.restoreSureWithDate(backup.date)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
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);
|
||||
}
|
||||
setting.primaryColor.put(backup.primaryColor);
|
||||
setting.serverStatusUpdateInterval
|
||||
.put(backup.serverStatusUpdateInterval);
|
||||
setting.launchPage.put(backup.launchPage);
|
||||
Navigator.of(context).pop();
|
||||
showSnackBar(context, Text(s.restoreSuccess));
|
||||
},
|
||||
child: Text(s.ok),
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text(s.invalidJson));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/backup.dart';
|
||||
import 'package:toolbox/view/page/convert.dart';
|
||||
import 'package:toolbox/view/page/debug.dart';
|
||||
import 'package:toolbox/view/page/ping.dart';
|
||||
@@ -240,6 +241,12 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.import_export),
|
||||
title: Text(s.backup),
|
||||
onTap: () =>
|
||||
AppRoute(BackupPage(), 'backup page').go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.snippet_folder),
|
||||
title: Text(s.snippet),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
@@ -26,9 +25,6 @@ class SnippetListPage extends StatefulWidget {
|
||||
class _SnippetListPageState extends State<SnippetListPage> {
|
||||
late ServerPrivateInfo _selectedIndex;
|
||||
|
||||
final _importFieldController = TextEditingController();
|
||||
final _exportFieldController = TextEditingController();
|
||||
|
||||
final _textStyle = TextStyle(color: primaryColor);
|
||||
|
||||
late S s;
|
||||
@@ -44,12 +40,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(s.snippet, style: size18),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _showImportExport(),
|
||||
tooltip: s.importAndExport,
|
||||
icon: const Icon(Icons.import_export)),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -60,97 +50,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showImportExport() async {
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.choose,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(s.import),
|
||||
leading: const Icon(Icons.download),
|
||||
onTap: () => _showImportDialog(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(s.export),
|
||||
leading: const Icon(Icons.file_upload),
|
||||
onTap: () => _showExportDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
[]);
|
||||
}
|
||||
|
||||
Future<void> _showExportDialog() async {
|
||||
Navigator.of(context).pop();
|
||||
_exportFieldController.text = locator<SnippetProvider>().export;
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.export,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 3,
|
||||
controller: _exportFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
child: Text(s.ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _showImportDialog() async {
|
||||
Navigator.of(context).pop();
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.import,
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: s.urlOrJson,
|
||||
),
|
||||
maxLines: 2,
|
||||
controller: _importFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
await _import(_importFieldController.text.trim()),
|
||||
child: const Text('GO'),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _import(String text) async {
|
||||
if (text.isEmpty) {
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
final snippetProvider = locator<SnippetProvider>();
|
||||
if (text.startsWith('http')) {
|
||||
final resp = await Dio().get(text);
|
||||
if (resp.statusCode != 200) {
|
||||
showSnackBar(
|
||||
context, Text(s.httpFailedWithCode(resp.statusCode ?? '-1')));
|
||||
return;
|
||||
}
|
||||
for (final snippet in getSnippetList(resp.data)) {
|
||||
snippetProvider.add(snippet);
|
||||
}
|
||||
} else {
|
||||
for (final snippet in getSnippetList(text)) {
|
||||
snippetProvider.add(snippet);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<SnippetProvider>(
|
||||
builder: (_, key, __) {
|
||||
|
||||
Reference in New Issue
Block a user