From 0fdc1b784b8d53bb705e7772026a1b789bf254d0 Mon Sep 17 00:00:00 2001 From: Junyuan Feng Date: Sun, 22 May 2022 13:02:54 +0800 Subject: [PATCH] feat. & fix. - support backup & restore - fix when client.run empty return --- lib/data/model/app/backup.dart | 52 +++++++ lib/data/provider/server.dart | 6 + lib/generated/intl/messages_en.dart | 35 +++-- lib/generated/intl/messages_zh.dart | 35 +++-- lib/generated/l10n.dart | 60 +++++++++ lib/l10n/intl_en.arb | 8 +- lib/l10n/intl_zh.arb | 8 +- lib/view/page/backup.dart | 201 ++++++++++++++++++++++++++++ lib/view/page/home.dart | 7 + lib/view/page/snippet/list.dart | 101 -------------- 10 files changed, 386 insertions(+), 127 deletions(-) create mode 100644 lib/data/model/app/backup.dart create mode 100644 lib/view/page/backup.dart diff --git a/lib/data/model/app/backup.dart b/lib/data/model/app/backup.dart new file mode 100644 index 00000000..690e78eb --- /dev/null +++ b/lib/data/model/app/backup.dart @@ -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 spis; + final List snippets; + final List 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 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 toJson() => { + 'version': version, + 'date': date, + 'spis': spis, + 'snippets': snippets, + 'keys': keys, + 'primaryColor': primaryColor, + 'serverStatusUpdateInterval': serverStatusUpdateInterval, + 'launchPage': launchPage, + }; +} diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 4d00a9d5..08ae4af3 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -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); diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index b8c32790..e736e2d1 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -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 _notInlinedMessages(_) => { @@ -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": diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index af814b75..f11a577e 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -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 _notInlinedMessages(_) => { @@ -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("更改将会立即生效") diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 98bafb00..15146e21 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -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 { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0095653c..8092b9eb 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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." } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index e4d80712..ba86dbe7 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -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来应用更改" } \ No newline at end of file diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart new file mode 100644 index 00000000..2442ca2d --- /dev/null +++ b/lib/view/page/backup.dart @@ -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(); + final server = locator(); + final snippet = locator(); + final privateKey = locator(); + + @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 _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 _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 _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 _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(); +} diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 6898f54d..acbbc0f2 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -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 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), diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index 45c9c1cf..ffbf4f8a 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -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 { 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 { 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 { ); } - Future _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 _showExportDialog() async { - Navigator.of(context).pop(); - _exportFieldController.text = locator().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 _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 _import(String text) async { - if (text.isEmpty) { - showSnackBar(context, Text(s.fieldMustNotEmpty)); - return; - } - final snippetProvider = locator(); - 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( builder: (_, key, __) {