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];
|
final si = _servers[idx];
|
||||||
if (si.client == null) return;
|
if (si.client == null) return;
|
||||||
final raw = await si.client!.run("sh $shellPath").string;
|
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();
|
final lines = raw.split(seperator).map((e) => e.trim()).toList();
|
||||||
lines.removeAt(0);
|
lines.removeAt(0);
|
||||||
|
|
||||||
|
|||||||
@@ -43,17 +43,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
|
|
||||||
static String m9(url) => "Please report bugs on ${url}";
|
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);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
@@ -67,6 +69,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("App primary color"),
|
MessageLookupByLibrary.simpleMessage("App primary color"),
|
||||||
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
|
"attention": MessageLookupByLibrary.simpleMessage("Attention"),
|
||||||
"backDir": MessageLookupByLibrary.simpleMessage("Back"),
|
"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"),
|
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||||
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
|
"choose": MessageLookupByLibrary.simpleMessage("Choose"),
|
||||||
"chooseDestination":
|
"chooseDestination":
|
||||||
@@ -116,6 +123,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"install": MessageLookupByLibrary.simpleMessage("install"),
|
"install": MessageLookupByLibrary.simpleMessage("install"),
|
||||||
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||||
"Please https://docs.docker.com/engine/install docker first."),
|
"Please https://docs.docker.com/engine/install docker first."),
|
||||||
|
"invalidJson": MessageLookupByLibrary.simpleMessage("Invalid JSON"),
|
||||||
"invalidVersionHelp": m7,
|
"invalidVersionHelp": m7,
|
||||||
"keepForeground":
|
"keepForeground":
|
||||||
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
|
MessageLookupByLibrary.simpleMessage("Keep app foreground!"),
|
||||||
@@ -163,6 +171,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
|
"pwd": MessageLookupByLibrary.simpleMessage("Password"),
|
||||||
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
|
"rename": MessageLookupByLibrary.simpleMessage("Rename"),
|
||||||
"reportBugsOnGithubIssue": m9,
|
"reportBugsOnGithubIssue": m9,
|
||||||
|
"restoreSuccess": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Restore success. Restart app to apply."),
|
||||||
|
"restoreSureWithDate": m10,
|
||||||
"result": MessageLookupByLibrary.simpleMessage("Result"),
|
"result": MessageLookupByLibrary.simpleMessage("Result"),
|
||||||
"run": MessageLookupByLibrary.simpleMessage("Run"),
|
"run": MessageLookupByLibrary.simpleMessage("Run"),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
"save": MessageLookupByLibrary.simpleMessage("Save"),
|
||||||
@@ -186,11 +197,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"sftpSSHConnected":
|
"sftpSSHConnected":
|
||||||
MessageLookupByLibrary.simpleMessage("SFTP Connected"),
|
MessageLookupByLibrary.simpleMessage("SFTP Connected"),
|
||||||
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
|
"snippet": MessageLookupByLibrary.simpleMessage("Snippet"),
|
||||||
"spentTime": m10,
|
"spentTime": m11,
|
||||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||||
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
|
"stop": MessageLookupByLibrary.simpleMessage("Stop"),
|
||||||
"sureDelete": m11,
|
"sureDelete": m12,
|
||||||
"sureToDeleteServer": m12,
|
"sureToDeleteServer": m13,
|
||||||
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
|
"ttl": MessageLookupByLibrary.simpleMessage("TTL"),
|
||||||
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
"unknown": MessageLookupByLibrary.simpleMessage("unknown"),
|
||||||
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
"unknownError": MessageLookupByLibrary.simpleMessage("Unknown error"),
|
||||||
@@ -204,9 +215,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
|
"upsideDown": MessageLookupByLibrary.simpleMessage("Upside Down"),
|
||||||
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
|
"urlOrJson": MessageLookupByLibrary.simpleMessage("URL or JSON"),
|
||||||
"user": MessageLookupByLibrary.simpleMessage("User"),
|
"user": MessageLookupByLibrary.simpleMessage("User"),
|
||||||
"versionHaveUpdate": m13,
|
"versionHaveUpdate": m14,
|
||||||
"versionUnknownUpdate": m14,
|
"versionUnknownUpdate": m15,
|
||||||
"versionUpdated": m15,
|
"versionUpdated": m16,
|
||||||
"waitConnection": MessageLookupByLibrary.simpleMessage(
|
"waitConnection": MessageLookupByLibrary.simpleMessage(
|
||||||
"Please wait for the connection to be established."),
|
"Please wait for the connection to be established."),
|
||||||
"willTakEeffectImmediately":
|
"willTakEeffectImmediately":
|
||||||
|
|||||||
@@ -43,17 +43,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
|
|
||||||
static String m9(url) => "请到 ${url} 提交问题";
|
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);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
@@ -64,6 +66,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
|
"appPrimaryColor": MessageLookupByLibrary.simpleMessage("App主要色"),
|
||||||
"attention": MessageLookupByLibrary.simpleMessage("注意"),
|
"attention": MessageLookupByLibrary.simpleMessage("注意"),
|
||||||
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
|
"backDir": MessageLookupByLibrary.simpleMessage("返回上一级"),
|
||||||
|
"backup": MessageLookupByLibrary.simpleMessage("备份"),
|
||||||
|
"backupTip": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"导出的数据仅进行了简单加密,请妥善保管。\n恢复的数据(除了设置)不会覆盖现有数据。"),
|
||||||
|
"backupVersionNotMatch":
|
||||||
|
MessageLookupByLibrary.simpleMessage("备份版本不匹配,无法恢复"),
|
||||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||||
"choose": MessageLookupByLibrary.simpleMessage("选择"),
|
"choose": MessageLookupByLibrary.simpleMessage("选择"),
|
||||||
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
|
"chooseDestination": MessageLookupByLibrary.simpleMessage("选择目标"),
|
||||||
@@ -105,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"install": MessageLookupByLibrary.simpleMessage("安装"),
|
"install": MessageLookupByLibrary.simpleMessage("安装"),
|
||||||
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
"installDockerWithUrl": MessageLookupByLibrary.simpleMessage(
|
||||||
"请先 https://docs.docker.com/engine/install docker"),
|
"请先 https://docs.docker.com/engine/install docker"),
|
||||||
|
"invalidJson": MessageLookupByLibrary.simpleMessage("无效的json,存在格式问题"),
|
||||||
"invalidVersionHelp": m7,
|
"invalidVersionHelp": m7,
|
||||||
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
|
"keepForeground": MessageLookupByLibrary.simpleMessage("请保持应用处于前台!"),
|
||||||
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
|
"keyAuth": MessageLookupByLibrary.simpleMessage("公钥认证"),
|
||||||
@@ -142,6 +150,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
|
"pwd": MessageLookupByLibrary.simpleMessage("密码"),
|
||||||
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
|
"rename": MessageLookupByLibrary.simpleMessage("重命名"),
|
||||||
"reportBugsOnGithubIssue": m9,
|
"reportBugsOnGithubIssue": m9,
|
||||||
|
"restoreSuccess":
|
||||||
|
MessageLookupByLibrary.simpleMessage("恢复成功,需要重启App来应用更改"),
|
||||||
|
"restoreSureWithDate": m10,
|
||||||
"result": MessageLookupByLibrary.simpleMessage("结果"),
|
"result": MessageLookupByLibrary.simpleMessage("结果"),
|
||||||
"run": MessageLookupByLibrary.simpleMessage("运行"),
|
"run": MessageLookupByLibrary.simpleMessage("运行"),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
||||||
@@ -160,11 +171,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"sftpSSHConnected":
|
"sftpSSHConnected":
|
||||||
MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."),
|
MessageLookupByLibrary.simpleMessage("SFTP 已连接,即将开始下载..."),
|
||||||
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
|
"snippet": MessageLookupByLibrary.simpleMessage("代码片段"),
|
||||||
"spentTime": m10,
|
"spentTime": m11,
|
||||||
"start": MessageLookupByLibrary.simpleMessage("开始"),
|
"start": MessageLookupByLibrary.simpleMessage("开始"),
|
||||||
"stop": MessageLookupByLibrary.simpleMessage("停止"),
|
"stop": MessageLookupByLibrary.simpleMessage("停止"),
|
||||||
"sureDelete": m11,
|
"sureDelete": m12,
|
||||||
"sureToDeleteServer": m12,
|
"sureToDeleteServer": m13,
|
||||||
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
"ttl": MessageLookupByLibrary.simpleMessage("缓存时间"),
|
||||||
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
||||||
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
"unknownError": MessageLookupByLibrary.simpleMessage("未知错误"),
|
||||||
@@ -177,9 +188,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
|
"upsideDown": MessageLookupByLibrary.simpleMessage("上下交换"),
|
||||||
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
|
"urlOrJson": MessageLookupByLibrary.simpleMessage("链接或JSON"),
|
||||||
"user": MessageLookupByLibrary.simpleMessage("用户"),
|
"user": MessageLookupByLibrary.simpleMessage("用户"),
|
||||||
"versionHaveUpdate": m13,
|
"versionHaveUpdate": m14,
|
||||||
"versionUnknownUpdate": m14,
|
"versionUnknownUpdate": m15,
|
||||||
"versionUpdated": m15,
|
"versionUpdated": m16,
|
||||||
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
|
"waitConnection": MessageLookupByLibrary.simpleMessage("请等待连接建立"),
|
||||||
"willTakEeffectImmediately":
|
"willTakEeffectImmediately":
|
||||||
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
|
MessageLookupByLibrary.simpleMessage("更改将会立即生效")
|
||||||
|
|||||||
@@ -1230,6 +1230,66 @@ class S {
|
|||||||
args: [],
|
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> {
|
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}.",
|
"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",
|
"noInterface": "No interface",
|
||||||
"lastTry": "Last try!",
|
"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} 提交问题。",
|
"invalidVersionHelp": "请确保正确安装了docker,或者使用的非自编译版本。如果没有以上问题,请在 {url} 提交问题。",
|
||||||
"noInterface": "没有可用的接口",
|
"noInterface": "没有可用的接口",
|
||||||
"lastTry": "最后尝试",
|
"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/data/store/setting.dart';
|
||||||
import 'package:toolbox/generated/l10n.dart';
|
import 'package:toolbox/generated/l10n.dart';
|
||||||
import 'package:toolbox/locator.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/convert.dart';
|
||||||
import 'package:toolbox/view/page/debug.dart';
|
import 'package:toolbox/view/page/debug.dart';
|
||||||
import 'package:toolbox/view/page/ping.dart';
|
import 'package:toolbox/view/page/ping.dart';
|
||||||
@@ -240,6 +241,12 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
||||||
.go(context),
|
.go(context),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.import_export),
|
||||||
|
title: Text(s.backup),
|
||||||
|
onTap: () =>
|
||||||
|
AppRoute(BackupPage(), 'backup page').go(context),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.snippet_folder),
|
leading: const Icon(Icons.snippet_folder),
|
||||||
title: Text(s.snippet),
|
title: Text(s.snippet),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:toolbox/core/route.dart';
|
import 'package:toolbox/core/route.dart';
|
||||||
@@ -26,9 +25,6 @@ class SnippetListPage extends StatefulWidget {
|
|||||||
class _SnippetListPageState extends State<SnippetListPage> {
|
class _SnippetListPageState extends State<SnippetListPage> {
|
||||||
late ServerPrivateInfo _selectedIndex;
|
late ServerPrivateInfo _selectedIndex;
|
||||||
|
|
||||||
final _importFieldController = TextEditingController();
|
|
||||||
final _exportFieldController = TextEditingController();
|
|
||||||
|
|
||||||
final _textStyle = TextStyle(color: primaryColor);
|
final _textStyle = TextStyle(color: primaryColor);
|
||||||
|
|
||||||
late S s;
|
late S s;
|
||||||
@@ -44,12 +40,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(s.snippet, style: size18),
|
title: Text(s.snippet, style: size18),
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => _showImportExport(),
|
|
||||||
tooltip: s.importAndExport,
|
|
||||||
icon: const Icon(Icons.import_export)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: FloatingActionButton(
|
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() {
|
Widget _buildBody() {
|
||||||
return Consumer<SnippetProvider>(
|
return Consumer<SnippetProvider>(
|
||||||
builder: (_, key, __) {
|
builder: (_, key, __) {
|
||||||
|
|||||||
Reference in New Issue
Block a user