#25 new: sftp upload

This commit is contained in:
lollipopkit
2023-05-30 19:49:54 +08:00
parent a1e80fd806
commit 92ffed6541
25 changed files with 409 additions and 177 deletions

View File

@@ -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:

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 => '上下交換';

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;

View 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);
}
}

View File

@@ -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,
));
}
}

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "用户",

View File

@@ -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": "用戶",

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),

View File

@@ -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;

View File

@@ -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(),

View File

@@ -112,6 +112,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
onChanged: (p0) => setState(() {
_tags = p0;
}),
s: _s.tag,
)
],
);

View File

@@ -0,0 +1,5 @@
// import 'package:flutter/material.dart';
// class SnippetGroupOrderPage extends StatelessWidget {
// final
// }

View File

@@ -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({

View File

@@ -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,12 +51,10 @@ class TagEditor extends StatelessWidget {
Widget _buildTagItem(String tag, Function(String) onTagDelete) {
return Padding(
padding: EdgeInsets.only(right: 7),
padding: const EdgeInsets.only(right: 7),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(20.0),
),
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
color: primaryColor,
),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
@@ -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);