mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
#25 new: sftp upload
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => '上下交換';
|
||||
|
||||
|
||||
@@ -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<SSHClient> genClient(
|
||||
);
|
||||
}
|
||||
final key = locator<PrivateKeyStore>().get(spi.pubKeyId!);
|
||||
if (key == null) {
|
||||
throw SSHErr(
|
||||
type: SSHErrType.noPrivateKey,
|
||||
message: 'key [${spi.pubKeyId}] not found',
|
||||
);
|
||||
}
|
||||
onStatus_(GenSSHClientStatus.key);
|
||||
return SSHClient(
|
||||
socket,
|
||||
|
||||
@@ -15,6 +15,21 @@ abstract class Err<T> {
|
||||
Err({required this.from, required this.type, this.message});
|
||||
}
|
||||
|
||||
enum SSHErrType {
|
||||
unknown,
|
||||
noPrivateKey;
|
||||
}
|
||||
|
||||
class SSHErr extends Err<SSHErrType> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<void> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
170
lib/data/model/sftp/worker.dart
Normal file
170
lib/data/model/sftp/worker.dart
Normal file
@@ -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<void> 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<void> 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<void> _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<void> _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<Uint8List>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<SftpDownloadStatus> _status = [];
|
||||
List<SftpDownloadStatus> get status => _status;
|
||||
final List<SftpReqStatus> _status = [];
|
||||
List<SftpReqStatus> get status => _status;
|
||||
|
||||
List<SftpDownloadStatus> gets({int? id, String? fileName}) {
|
||||
var found = <SftpDownloadStatus>[];
|
||||
List<SftpReqStatus> gets({int? id, String? fileName}) {
|
||||
var found = <SftpReqStatus>[];
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "用户",
|
||||
|
||||
@@ -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": "用戶",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<SFTPDownloadedPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
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<ServerProvider>();
|
||||
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<SftpProvider>().add(
|
||||
SftpReqItem(spi, remotePath, file.absolute.path),
|
||||
SftpReqType.upload,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(_s.open),
|
||||
|
||||
@@ -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<SFTPDownloadingPage> {
|
||||
});
|
||||
}
|
||||
|
||||
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<SFTPDownloadingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(SftpDownloadStatus status) {
|
||||
Widget _buildItem(SftpReqStatus status) {
|
||||
if (status.error != null) {
|
||||
showSnackBar(context, Text(status.error.toString()));
|
||||
status.error = null;
|
||||
|
||||
@@ -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<SFTPPage> {
|
||||
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<SFTPPage> {
|
||||
final remotePath = _getRemotePath(name);
|
||||
final local = '${(await sftpDir).path}$remotePath';
|
||||
final pubKeyId = widget.spi.pubKeyId;
|
||||
final key = locator<PrivateKeyStore>().get(pubKeyId)?.privateKey;
|
||||
|
||||
locator<SftpProvider>().add(
|
||||
DownloadItem(
|
||||
SftpReqItem(
|
||||
widget.spi,
|
||||
remotePath,
|
||||
local,
|
||||
),
|
||||
key: pubKeyId == null
|
||||
? null
|
||||
: locator<PrivateKeyStore>().get(pubKeyId).privateKey,
|
||||
SftpReqType.download,
|
||||
key: key,
|
||||
);
|
||||
|
||||
context.pop();
|
||||
@@ -354,6 +355,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
child: child,
|
||||
title: Text(_s.attention),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
|
||||
@@ -112,6 +112,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
onChanged: (p0) => setState(() {
|
||||
_tags = p0;
|
||||
}),
|
||||
s: _s.tag,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
5
lib/view/page/snippet/group_order.dart
Normal file
5
lib/view/page/snippet/group_order.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
// class SnippetGroupOrderPage extends StatelessWidget {
|
||||
// final
|
||||
// }
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class Picker extends StatelessWidget {
|
||||
final List<Widget> items;
|
||||
final Function(int idx) onSelected;
|
||||
final void Function(int idx) onSelected;
|
||||
final double height;
|
||||
|
||||
const Picker({
|
||||
|
||||
@@ -6,9 +6,11 @@ import '../../data/res/color.dart';
|
||||
|
||||
class TagEditor extends StatelessWidget {
|
||||
final List<String> tags;
|
||||
final String s;
|
||||
final void Function(List<String>)? 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<String> 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<String> tags,
|
||||
void Function(List<String>)? 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);
|
||||
|
||||
Reference in New Issue
Block a user