diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart index 4d7824b1..c0cd1bae 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart @@ -846,6 +846,12 @@ abstract class S { /// **'Password'** String get pwd; + /// No description provided for @remotePath. + /// + /// In en, this message translates to: + /// **'Remote path'** + String get remotePath; + /// No description provided for @rename. /// /// In en, this message translates to: @@ -1050,6 +1056,12 @@ abstract class S { /// **'Are you sure to delete server [{server}]?'** String sureToDeleteServer(Object server); + /// No description provided for @tag. + /// + /// In en, this message translates to: + /// **'Tags'** + String get tag; + /// No description provided for @terminal. /// /// In en, this message translates to: @@ -1134,6 +1146,12 @@ abstract class S { /// **'Current version is too low, please update to v1.0.{newest}'** String updateTipTooLow(Object newest); + /// No description provided for @upload. + /// + /// In en, this message translates to: + /// **'Upload'** + String get upload; + /// No description provided for @upsideDown. /// /// In en, this message translates to: diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart index b22b8fb5..e44b2289 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart @@ -401,6 +401,9 @@ class SDe extends S { @override String get pwd => 'Passwort'; + @override + String get remotePath => 'Entfernte Pfade'; + @override String get rename => 'Umbenennen'; @@ -515,6 +518,9 @@ class SDe extends S { return 'Bist du sicher, dass du [$server] löschen willst?'; } + @override + String get tag => 'Tags'; + @override String get terminal => 'Terminal'; @@ -561,6 +567,9 @@ class SDe extends S { return 'Aktuelle Version ist zu alt, bitte update auf v1.0.$newest'; } + @override + String get upload => 'Hochladen'; + @override String get upsideDown => 'Upside Down'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart index 3fa5db14..30908d69 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart @@ -401,6 +401,9 @@ class SEn extends S { @override String get pwd => 'Password'; + @override + String get remotePath => 'Remote path'; + @override String get rename => 'Rename'; @@ -515,6 +518,9 @@ class SEn extends S { return 'Are you sure to delete server [$server]?'; } + @override + String get tag => 'Tags'; + @override String get terminal => 'Terminal'; @@ -561,6 +567,9 @@ class SEn extends S { return 'Current version is too low, please update to v1.0.$newest'; } + @override + String get upload => 'Upload'; + @override String get upsideDown => 'Upside Down'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart index 8a1f2009..daa8c02a 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart @@ -401,6 +401,9 @@ class SZh extends S { @override String get pwd => '密码'; + @override + String get remotePath => '远端路径'; + @override String get rename => '重命名'; @@ -515,6 +518,9 @@ class SZh extends S { return '你确定要删除服务器 [$server] 吗?'; } + @override + String get tag => '标签'; + @override String get terminal => '终端'; @@ -561,6 +567,9 @@ class SZh extends S { return '当前版本过低,请升级至 v1.0.$newest'; } + @override + String get upload => '上传'; + @override String get upsideDown => '上下交换'; @@ -996,6 +1005,9 @@ class SZhTw extends SZh { @override String get pwd => '密碼'; + @override + String get remotePath => '遠端路徑'; + @override String get rename => '重命名'; @@ -1110,6 +1122,9 @@ class SZhTw extends SZh { return '你確定要刪除服務器 [$server] 嗎?'; } + @override + String get tag => '标签'; + @override String get terminal => '终端機'; @@ -1156,6 +1171,9 @@ class SZhTw extends SZh { return '當前版本過低,請升級至 v1.0.$newest'; } + @override + String get upload => '上傳'; + @override String get upsideDown => '上下交換'; diff --git a/lib/core/utils/server.dart b/lib/core/utils/server.dart index a14632ee..72bb4076 100644 --- a/lib/core/utils/server.dart +++ b/lib/core/utils/server.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/foundation.dart'; +import 'package:toolbox/data/model/app/error.dart'; import '../../data/model/server/server_private_info.dart'; import '../../data/store/private_key.dart'; @@ -50,6 +51,12 @@ Future genClient( ); } final key = locator().get(spi.pubKeyId!); + if (key == null) { + throw SSHErr( + type: SSHErrType.noPrivateKey, + message: 'key [${spi.pubKeyId}] not found', + ); + } onStatus_(GenSSHClientStatus.key); return SSHClient( socket, diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index 52047ab3..d2d01906 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -15,6 +15,21 @@ abstract class Err { Err({required this.from, required this.type, this.message}); } +enum SSHErrType { + unknown, + noPrivateKey; +} + +class SSHErr extends Err { + SSHErr({required SSHErrType type, String? message}) + : super(from: ErrFrom.ssh, type: type, message: message); + + @override + String toString() { + return 'SSHErr<$type>: $message'; + } +} + enum DockerErrType { unknown, noClient, diff --git a/lib/data/model/sftp/download_item.dart b/lib/data/model/sftp/download_item.dart deleted file mode 100644 index bccdb3a9..00000000 --- a/lib/data/model/sftp/download_item.dart +++ /dev/null @@ -1,16 +0,0 @@ -import '../server/server_private_info.dart'; - -class DownloadItem { - DownloadItem(this.spi, this.remotePath, this.localPath); - - final ServerPrivateInfo spi; - final String remotePath; - final String localPath; -} - -class DownloadItemEvent { - DownloadItemEvent(this.item, this.privateKey); - - final DownloadItem item; - final String? privateKey; -} diff --git a/lib/data/model/sftp/download_worker.dart b/lib/data/model/sftp/download_worker.dart deleted file mode 100644 index 58a1c51e..00000000 --- a/lib/data/model/sftp/download_worker.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:dartssh2/dartssh2.dart'; -import 'package:easy_isolate/easy_isolate.dart'; - -import 'download_item.dart'; -import 'download_status.dart'; - -class SftpDownloadWorker { - SftpDownloadWorker( - {required this.onNotify, required this.item, this.privateKey}); - - final Function(Object event) onNotify; - final DownloadItem item; - final worker = Worker(); - final String? privateKey; - - void dispose() { - worker.dispose(); - } - - /// Initiate the worker (new thread) and start listen from messages between - /// the threads - Future init() async { - if (worker.isInitialized) worker.dispose(); - await worker.init( - mainMessageHandler, - isolateMessageHandler, - errorHandler: print, - ); - worker.sendMessage(DownloadItemEvent(item, privateKey)); - } - - /// Handle the messages coming from the isolate - void mainMessageHandler(dynamic data, SendPort isolateSendPort) { - onNotify(data); - } - - /// Handle the messages coming from the main - static isolateMessageHandler( - dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async { - if (data is DownloadItemEvent) { - try { - mainSendPort.send(SftpWorkerStatus.preparing); - final watch = Stopwatch()..start(); - final item = data.item; - final spi = item.spi; - final socket = await SSHSocket.connect(spi.ip, spi.port); - SSHClient client; - if (spi.pubKeyId == null) { - client = SSHClient(socket, - username: spi.user, onPasswordRequest: () => spi.pwd); - } else { - client = SSHClient(socket, - username: spi.user, - identities: SSHKeyPair.fromPem(data.privateKey!)); - } - mainSendPort.send(SftpWorkerStatus.sshConnectted); - - final remotePath = item.remotePath; - final localPath = item.localPath; - await Directory(localPath.substring(0, item.localPath.lastIndexOf('/'))) - .create(recursive: true); - final local = File(localPath); - if (await local.exists()) { - await local.delete(); - } - final localFile = local.openWrite(mode: FileMode.append); - final file = await (await client.sftp()).open(remotePath); - final size = (await file.stat()).size; - if (size == null) { - mainSendPort.send(Exception('can not get file size')); - return; - } - const defaultChunkSize = 1024 * 1024; - final chunkSize = size > defaultChunkSize ? defaultChunkSize : size; - mainSendPort.send(size); - mainSendPort.send(SftpWorkerStatus.downloading); - for (var i = 0; i < size; i += chunkSize) { - final fileData = file.read(length: chunkSize); - await for (var form in fileData) { - localFile.add(form); - mainSendPort.send((i + form.length) / size * 100); - } - } - await localFile.close(); - await file.close(); - mainSendPort.send(watch.elapsed); - mainSendPort.send(SftpWorkerStatus.finished); - } catch (e) { - mainSendPort.send(e); - } - } - } -} diff --git a/lib/data/model/sftp/download_status.dart b/lib/data/model/sftp/req.dart similarity index 51% rename from lib/data/model/sftp/download_status.dart rename to lib/data/model/sftp/req.dart index 41823265..f37618e3 100644 --- a/lib/data/model/sftp/download_status.dart +++ b/lib/data/model/sftp/req.dart @@ -1,12 +1,29 @@ -import 'package:toolbox/data/model/sftp/download_worker.dart'; +import '../server/server_private_info.dart'; +import 'worker.dart'; -import 'download_item.dart'; +class SftpReqItem { + final ServerPrivateInfo spi; + final String remotePath; + final String localPath; -class SftpDownloadStatus { + SftpReqItem(this.spi, this.remotePath, this.localPath); +} + +enum SftpReqType { download, upload } + +class SftpReq { + final SftpReqItem item; + final String? privateKey; + final SftpReqType type; + + SftpReq({required this.item, this.privateKey, required this.type}); +} + +class SftpReqStatus { final int id; - final DownloadItem item; + final SftpReqItem item; final void Function() notifyListeners; - late SftpDownloadWorker worker; + late SftpWorker worker; String get fileName => item.localPath.split('/').last; @@ -17,16 +34,23 @@ class SftpDownloadStatus { Exception? error; Duration? spentTime; - SftpDownloadStatus(this.item, this.notifyListeners, {String? key}) - : id = DateTime.now().microsecondsSinceEpoch { - worker = - SftpDownloadWorker(onNotify: onNotify, item: item, privateKey: key); + SftpReqStatus({ + required this.item, + required this.notifyListeners, + required SftpReqType type, + String? key, + }) : id = DateTime.now().microsecondsSinceEpoch { + worker = SftpWorker( + onNotify: onNotify, + item: item, + privateKey: key, + type: type, + ); worker.init(); } @override - bool operator ==(Object other) => - other is SftpDownloadStatus && id == other.id; + bool operator ==(Object other) => other is SftpReqStatus && id == other.id; @override int get hashCode => id ^ super.hashCode; diff --git a/lib/data/model/sftp/worker.dart b/lib/data/model/sftp/worker.dart new file mode 100644 index 00000000..d0799ce4 --- /dev/null +++ b/lib/data/model/sftp/worker.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:dartssh2/dartssh2.dart'; +import 'package:easy_isolate/easy_isolate.dart'; +import 'package:toolbox/core/utils/misc.dart'; + +import 'req.dart'; + +class SftpWorker { + final Function(Object event) onNotify; + final SftpReqItem item; + final String? privateKey; + final SftpReqType type; + + final worker = Worker(); + + SftpWorker({ + required this.onNotify, + required this.item, + required this.type, + this.privateKey, + }); + + void dispose() { + worker.dispose(); + } + + /// Initiate the worker (new thread) and start listen from messages between + /// the threads + Future init() async { + if (worker.isInitialized) worker.dispose(); + await worker.init( + mainMessageHandler, + isolateMessageHandler, + errorHandler: print, + ); + worker.sendMessage(SftpReq(item: item, privateKey: privateKey, type: type)); + } + + /// Handle the messages coming from the isolate + void mainMessageHandler(dynamic data, SendPort isolateSendPort) { + onNotify(data); + } +} + +/// Handle the messages coming from the main +Future isolateMessageHandler( + dynamic data, + SendPort mainSendPort, + SendErrorFunction sendError, +) async { + switch (data.runtimeType) { + case SftpReq: + switch (data.type) { + case SftpReqType.download: + await _download(data, mainSendPort, sendError); + break; + case SftpReqType.upload: + await _upload(data, mainSendPort, sendError); + break; + default: + sendError(Exception('unknown type')); + } + break; + default: + sendError(Exception('unknown event')); + } +} + +Future _download( + SftpReq data, + SendPort mainSendPort, + SendErrorFunction sendError, +) async { + try { + mainSendPort.send(SftpWorkerStatus.preparing); + final watch = Stopwatch()..start(); + final item = data.item; + final spi = item.spi; + final socket = await SSHSocket.connect(spi.ip, spi.port); + SSHClient client; + if (spi.pubKeyId == null) { + client = SSHClient(socket, + username: spi.user, onPasswordRequest: () => spi.pwd); + } else { + client = SSHClient(socket, + username: spi.user, identities: SSHKeyPair.fromPem(data.privateKey!)); + } + mainSendPort.send(SftpWorkerStatus.sshConnectted); + + final remotePath = item.remotePath; + final localPath = item.localPath; + await Directory(localPath.substring(0, item.localPath.lastIndexOf('/'))) + .create(recursive: true); + final local = File(localPath); + if (await local.exists()) { + await local.delete(); + } + final localFile = local.openWrite(mode: FileMode.append); + final file = await (await client.sftp()).open(remotePath); + final size = (await file.stat()).size; + if (size == null) { + mainSendPort.send(Exception('can not get file size')); + return; + } + const defaultChunkSize = 1024 * 1024; + final chunkSize = size > defaultChunkSize ? defaultChunkSize : size; + mainSendPort.send(size); + mainSendPort.send(SftpWorkerStatus.downloading); + for (var i = 0; i < size; i += chunkSize) { + final fileData = file.read(length: chunkSize); + await for (var form in fileData) { + localFile.add(form); + mainSendPort.send((i + form.length) / size * 100); + } + } + await localFile.close(); + await file.close(); + mainSendPort.send(watch.elapsed); + mainSendPort.send(SftpWorkerStatus.finished); + } catch (e) { + mainSendPort.send(e); + } +} + +Future _upload( + SftpReq data, + SendPort mainSendPort, + SendErrorFunction sendError, +) async { + try { + mainSendPort.send(SftpWorkerStatus.preparing); + final watch = Stopwatch()..start(); + final item = data.item; + final spi = item.spi; + final socket = await SSHSocket.connect(spi.ip, spi.port); + SSHClient client; + if (spi.pubKeyId == null) { + client = SSHClient(socket, + username: spi.user, onPasswordRequest: () => spi.pwd); + } else { + client = SSHClient(socket, + username: spi.user, identities: SSHKeyPair.fromPem(data.privateKey!)); + } + mainSendPort.send(SftpWorkerStatus.sshConnectted); + + final localPath = item.localPath; + final remotePath = + item.remotePath + (getFileName(localPath) ?? 'srvbox_sftp_upload'); + final local = File(localPath); + if (!await local.exists()) { + mainSendPort.send(Exception('local file not exists')); + return; + } + final localFile = local.openRead().cast(); + final sftp = await client.sftp(); + final file = await sftp.open(remotePath, + mode: SftpFileOpenMode.write | SftpFileOpenMode.create); + final writer = file.write(localFile); + await writer.done; + await file.close(); + mainSendPort.send(watch.elapsed); + mainSendPort.send(SftpWorkerStatus.finished); + } catch (e) { + mainSendPort.send(e); + } +} diff --git a/lib/data/provider/sftp_download.dart b/lib/data/provider/sftp.dart similarity index 50% rename from lib/data/provider/sftp_download.dart rename to lib/data/provider/sftp.dart index b5c598b5..b4ebacdc 100644 --- a/lib/data/provider/sftp_download.dart +++ b/lib/data/provider/sftp.dart @@ -1,14 +1,13 @@ import 'package:toolbox/core/provider_base.dart'; -import '../model/sftp/download_item.dart'; -import '../model/sftp/download_status.dart'; +import '../model/sftp/req.dart'; class SftpProvider extends ProviderBase { - final List _status = []; - List get status => _status; + final List _status = []; + List get status => _status; - List gets({int? id, String? fileName}) { - var found = []; + List gets({int? id, String? fileName}) { + var found = []; if (id != null) { found = _status.where((e) => e.id == id).toList(); } @@ -20,13 +19,18 @@ class SftpProvider extends ProviderBase { return found; } - SftpDownloadStatus? get({int? id, String? name}) { + SftpReqStatus? get({int? id, String? name}) { final found = gets(id: id, fileName: name); if (found.isEmpty) return null; return found.first; } - void add(DownloadItem item, {String? key}) { - _status.add(SftpDownloadStatus(item, notifyListeners, key: key)); + void add(SftpReqItem item, SftpReqType type, {String? key}) { + _status.add(SftpReqStatus( + item: item, + notifyListeners: notifyListeners, + key: key, + type: type, + )); } } diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart index d28b532e..a693e2d6 100644 --- a/lib/data/store/private_key.dart +++ b/lib/data/store/private_key.dart @@ -18,7 +18,8 @@ class PrivateKeyStore extends PersistentStore { return ps; } - PrivateKeyInfo get(String id) { + PrivateKeyInfo? get(String? id) { + if (id == null) return null; return box.get(id); } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 71bf0132..f325a368 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -125,6 +125,7 @@ "privateKey": "Private Key", "pushToken": "Push Token", "pwd": "Passwort", + "remotePath": "Entfernte Pfade", "rename": "Umbenennen", "reportBugsOnGithubIssue": "Bitte Bugs auf {url} melden", "restart": "Neustart", @@ -159,6 +160,7 @@ "sureDirEmpty": "Stelle sicher, dass der Ordner leer ist.", "sureNoPwd": "Bist du sicher, dass du kein Passwort verwenden willst?", "sureToDeleteServer": "Bist du sicher, dass du [{server}] löschen willst?", + "tag": "Tags", "terminal": "Terminal", "theme": "Themen", "themeMode": "Thememodus", @@ -173,6 +175,7 @@ "updateServerStatusInterval": "Aktualisierungsintervall des Serverstatus", "updateTip": "Update: v1.0.{newest}", "updateTipTooLow": "Aktuelle Version ist zu alt, bitte update auf v1.0.{newest}", + "upload": "Hochladen", "upsideDown": "Upside Down", "urlOrJson": "URL oder JSON", "user": "Benutzer", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 398bde7a..f8ea78f4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -125,6 +125,7 @@ "privateKey": "Private Key", "pushToken": "Push token", "pwd": "Password", + "remotePath": "Remote path", "rename": "Rename", "reportBugsOnGithubIssue": "Please report bugs on {url}", "restart": "Restart", @@ -159,6 +160,7 @@ "sureDirEmpty": "Make sure dir is empty.", "sureNoPwd": "Are you sure to use no password?", "sureToDeleteServer": "Are you sure to delete server [{server}]?", + "tag": "Tags", "terminal": "Terminal", "theme": "Theme", "themeMode": "Theme mode", @@ -173,6 +175,7 @@ "updateServerStatusInterval": "Server status update interval", "updateTip": "Update: v1.0.{newest}", "updateTipTooLow": "Current version is too low, please update to v1.0.{newest}", + "upload": "Upload", "upsideDown": "Upside Down", "urlOrJson": "URL or JSON", "user": "User", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 29b8f55e..2b9d2c2b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -125,6 +125,7 @@ "privateKey": "私钥", "pushToken": "消息推送 Token", "pwd": "密码", + "remotePath": "远端路径", "rename": "重命名", "reportBugsOnGithubIssue": "请到 {url} 提交问题", "restart": "重启", @@ -159,6 +160,7 @@ "sureDirEmpty": "请确保文件夹为空", "sureNoPwd": "确认使用无密码?", "sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?", + "tag": "标签", "terminal": "终端", "theme": "主题", "themeMode": "主题模式", @@ -173,6 +175,7 @@ "updateServerStatusInterval": "服务器状态刷新间隔", "updateTip": "新版本: v1.0.{newest}", "updateTipTooLow": "当前版本过低,请升级至 v1.0.{newest}", + "upload": "上传", "upsideDown": "上下交换", "urlOrJson": "链接或JSON", "user": "用户", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 08de02c7..218ff1c3 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -125,6 +125,7 @@ "privateKey": "私鑰", "pushToken": "消息推送 Token", "pwd": "密碼", + "remotePath": "遠端路徑", "rename": "重命名", "reportBugsOnGithubIssue": "請到 {url} 提交問題", "restart": "重啓", @@ -159,6 +160,7 @@ "sureDirEmpty": "請確保文件夾為空", "sureNoPwd": "確認使用無密碼?", "sureToDeleteServer": "你確定要刪除服務器 [{server}] 嗎?", + "tag": "标签", "terminal": "终端機", "theme": "主題", "themeMode": "主題模式", @@ -173,6 +175,7 @@ "updateServerStatusInterval": "服務器狀態更新間隔", "updateTip": "新版本: v1.0.{newest}", "updateTipTooLow": "當前版本過低,請升級至 v1.0.{newest}", + "upload": "上傳", "upsideDown": "上下交換", "urlOrJson": "鏈接或JSON", "user": "用戶", diff --git a/lib/locator.dart b/lib/locator.dart index 1a4c52db..278f5784 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -6,7 +6,7 @@ import 'data/provider/docker.dart'; import 'data/provider/pkg.dart'; import 'data/provider/private_key.dart'; import 'data/provider/server.dart'; -import 'data/provider/sftp_download.dart'; +import 'data/provider/sftp.dart'; import 'data/provider/snippet.dart'; import 'data/provider/virtual_keyboard.dart'; import 'data/service/app.dart'; diff --git a/lib/main.dart b/lib/main.dart index 5b0061b0..aea7fc54 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,7 @@ import 'data/provider/docker.dart'; import 'data/provider/pkg.dart'; import 'data/provider/private_key.dart'; import 'data/provider/server.dart'; -import 'data/provider/sftp_download.dart'; +import 'data/provider/sftp.dart'; import 'data/provider/snippet.dart'; import 'data/provider/virtual_keyboard.dart'; import 'data/store/setting.dart'; diff --git a/lib/view/page/sftp/downloaded.dart b/lib/view/page/sftp/downloaded.dart index 0b29adc2..addbc0f3 100644 --- a/lib/view/page/sftp/downloaded.dart +++ b/lib/view/page/sftp/downloaded.dart @@ -3,9 +3,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:toolbox/core/extension/navigator.dart'; +import 'package:toolbox/data/model/sftp/req.dart'; +import 'package:toolbox/data/provider/server.dart'; +import 'package:toolbox/data/provider/sftp.dart'; import 'package:toolbox/data/res/misc.dart'; +import 'package:toolbox/locator.dart'; import 'package:toolbox/view/page/editor.dart'; import 'package:toolbox/view/widget/input_field.dart'; +import 'package:toolbox/view/widget/picker.dart'; import '../../../core/extension/numx.dart'; import '../../../core/extension/stringx.dart'; @@ -217,6 +222,51 @@ class _SFTPDownloadedPageState extends State { ); }, ), + ListTile( + leading: const Icon(Icons.upload), + title: Text(_s.upload), + onTap: () async { + context.pop(); + final remotePath = await showRoundDialog( + context: context, + title: Text(_s.remotePath), + child: Input( + controller: TextEditingController(text: '/'), + onSubmitted: (p0) { + context.pop(p0); + }, + )); + if (remotePath == null) { + showSnackBar(context, Text(_s.fieldMustNotEmpty)); + return; + } + final serverProvider = locator(); + final ids = serverProvider.serverOrder; + var idx = 0; + await showRoundDialog( + context: context, + title: Text(_s.server), + child: Picker( + items: ids.map((e) => Text(e)).toList(), + onSelected: (idx_) => idx = idx_, + ), + actions: [ + TextButton( + onPressed: () => context.pop(), child: Text(_s.ok)), + ], + ); + final id = ids[idx]; + final spi = serverProvider.servers[id]?.spi; + if (spi == null) { + showSnackBar(context, Text(_s.noResult)); + return; + } + locator().add( + SftpReqItem(spi, remotePath, file.absolute.path), + SftpReqType.upload, + ); + }, + ), ListTile( leading: const Icon(Icons.open_in_new), title: Text(_s.open), diff --git a/lib/view/page/sftp/downloading.dart b/lib/view/page/sftp/downloading.dart index d8067473..cfbee264 100644 --- a/lib/view/page/sftp/downloading.dart +++ b/lib/view/page/sftp/downloading.dart @@ -5,8 +5,8 @@ import 'package:provider/provider.dart'; import '../../../core/extension/numx.dart'; import '../../../core/utils/misc.dart'; import '../../../core/utils/ui.dart'; -import '../../../data/model/sftp/download_status.dart'; -import '../../../data/provider/sftp_download.dart'; +import '../../../data/model/sftp/req.dart'; +import '../../../data/provider/sftp.dart'; import '../../../data/res/ui.dart'; import '../../widget/round_rect_card.dart'; @@ -57,7 +57,7 @@ class _SFTPDownloadingPageState extends State { }); } - Widget _wrapInCard(SftpDownloadStatus status, String? subtitle, + Widget _wrapInCard(SftpReqStatus status, String? subtitle, {Widget? trailing}) { return RoundRectCard( ListTile( @@ -73,7 +73,7 @@ class _SFTPDownloadingPageState extends State { ); } - Widget _buildItem(SftpDownloadStatus status) { + Widget _buildItem(SftpReqStatus status) { if (status.error != null) { showSnackBar(context, Text(status.error.toString())); status.error = null; diff --git a/lib/view/page/sftp/view.dart b/lib/view/page/sftp/view.dart index feebdc57..2f7f20e0 100644 --- a/lib/view/page/sftp/view.dart +++ b/lib/view/page/sftp/view.dart @@ -18,9 +18,9 @@ import '../../../data/model/server/server.dart'; import '../../../data/model/server/server_private_info.dart'; import '../../../data/model/sftp/absolute_path.dart'; import '../../../data/model/sftp/browser_status.dart'; -import '../../../data/model/sftp/download_item.dart'; +import '../../../data/model/sftp/req.dart'; import '../../../data/provider/server.dart'; -import '../../../data/provider/sftp_download.dart'; +import '../../../data/provider/sftp.dart'; import '../../../data/res/path.dart'; import '../../../data/res/ui.dart'; import '../../../data/store/private_key.dart'; @@ -313,6 +313,7 @@ class _SFTPPageState extends State { void _download(BuildContext context, SftpName name) { showRoundDialog( context: context, + title: Text(_s.attention), child: Text('${_s.dl2Local(name.filename)}\n${_s.keepForeground}'), actions: [ TextButton( @@ -325,16 +326,16 @@ class _SFTPPageState extends State { final remotePath = _getRemotePath(name); final local = '${(await sftpDir).path}$remotePath'; final pubKeyId = widget.spi.pubKeyId; + final key = locator().get(pubKeyId)?.privateKey; locator().add( - DownloadItem( + SftpReqItem( widget.spi, remotePath, local, ), - key: pubKeyId == null - ? null - : locator().get(pubKeyId).privateKey, + SftpReqType.download, + key: key, ); context.pop(); @@ -354,6 +355,7 @@ class _SFTPPageState extends State { showRoundDialog( context: context, child: child, + title: Text(_s.attention), actions: [ TextButton( onPressed: () => context.pop(), diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index 87890638..406624a4 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -112,6 +112,7 @@ class _SnippetEditPageState extends State onChanged: (p0) => setState(() { _tags = p0; }), + s: _s.tag, ) ], ); diff --git a/lib/view/page/snippet/group_order.dart b/lib/view/page/snippet/group_order.dart new file mode 100644 index 00000000..c04bb452 --- /dev/null +++ b/lib/view/page/snippet/group_order.dart @@ -0,0 +1,5 @@ +// import 'package:flutter/material.dart'; + +// class SnippetGroupOrderPage extends StatelessWidget { +// final +// } diff --git a/lib/view/widget/picker.dart b/lib/view/widget/picker.dart index 10c110c7..48a1b052 100644 --- a/lib/view/widget/picker.dart +++ b/lib/view/widget/picker.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; class Picker extends StatelessWidget { final List items; - final Function(int idx) onSelected; + final void Function(int idx) onSelected; final double height; const Picker({ diff --git a/lib/view/widget/tag.dart b/lib/view/widget/tag.dart index 7ae43f64..5e2aec1d 100644 --- a/lib/view/widget/tag.dart +++ b/lib/view/widget/tag.dart @@ -6,9 +6,11 @@ import '../../data/res/color.dart'; class TagEditor extends StatelessWidget { final List tags; + final String s; final void Function(List)? onChanged; - const TagEditor({super.key, required this.tags, this.onChanged}); + const TagEditor( + {super.key, required this.tags, this.onChanged, required this.s}); @override Widget build(BuildContext context) { @@ -36,7 +38,7 @@ class TagEditor extends StatelessWidget { List tags, Function(String) onTagDelete, ) { - if (tags.isEmpty) return Text('Tags'); + if (tags.isEmpty) return Text(s); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -49,35 +51,33 @@ class TagEditor extends StatelessWidget { Widget _buildTagItem(String tag, Function(String) onTagDelete) { return Padding( - padding: EdgeInsets.only(right: 7), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(20.0), + padding: const EdgeInsets.only(right: 7), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + color: primaryColor, ), - color: primaryColor, - ), - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( '#$tag', style: const TextStyle(color: Colors.white), ), - const SizedBox(width: 4.0), - InkWell( - child: const Icon( - Icons.cancel, - size: 14.0, - color: Colors.white, - ), - onTap: () { - onTagDelete(tag); - }, - ) - ], - ), + const SizedBox(width: 4.0), + InkWell( + child: const Icon( + Icons.cancel, + size: 14.0, + color: Colors.white, + ), + onTap: () { + onTagDelete(tag); + }, + ) + ], + ), ), ); } @@ -87,20 +87,20 @@ class TagEditor extends StatelessWidget { List tags, void Function(List)? onChanged, ) { - final _textEditingController = TextEditingController(); + final textEditingController = TextEditingController(); showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Add Tag'), content: Input( - controller: _textEditingController, + controller: textEditingController, hint: 'Tag', ), actions: [ TextButton( onPressed: () { - final tag = _textEditingController.text; + final tag = textEditingController.text; tags.add(tag.trim()); onChanged?.call(tags); Navigator.pop(context);