opt.: sftp home & back (#533)

This commit is contained in:
lollipopkit🏳️‍⚧️
2024-08-14 19:01:44 +08:00
committed by GitHub
parent 41e3fcb23a
commit 267b0b0a69
59 changed files with 466 additions and 477 deletions

View File

@@ -30,14 +30,19 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
library_private_types_in_public_api: false library_private_types_in_public_api: true
use_build_context_synchronously: false use_build_context_synchronously: false
depend_on_referenced_packages: false depend_on_referenced_packages: false
prefer_final_locals: true prefer_final_locals: true
unnecessary_parenthesis: true unnecessary_parenthesis: true
implicit_call_tearoffs: true implicit_call_tearoffs: true
always_declare_return_types: true
always_use_package_imports: true
annotate_overrides: true
avoid_empty_else: true
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
avoid_return_types_on_setters: true
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@@ -5,10 +5,10 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../../data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
typedef _OnStdout = void Function(String data, SSHSession session); typedef OnStdout = void Function(String data, SSHSession session);
typedef _OnStdin = void Function(SSHSession session); typedef OnStdin = void Function(SSHSession session);
typedef PwdRequestFunc = Future<String?> Function(String? user); typedef PwdRequestFunc = Future<String?> Function(String? user);
@@ -16,15 +16,15 @@ extension SSHClientX on SSHClient {
/// TODO: delete [exec] /// TODO: delete [exec]
Future<SSHSession> exec( Future<SSHSession> exec(
String cmd, { String cmd, {
_OnStdout? onStderr, OnStdout? onStderr,
_OnStdout? onStdout, OnStdout? onStdout,
_OnStdin? stdin, OnStdin? stdin,
bool redirectToBash = false, // not working yet. do not use bool redirectToBash = false, // not working yet. do not use
}) async { }) async {
final session = await execute(redirectToBash ? "head -1 | bash" : cmd); final session = await execute(redirectToBash ? 'head -1 | bash' : cmd);
if (redirectToBash) { if (redirectToBash) {
session.stdin.add("$cmd\n".uint8List); session.stdin.add('$cmd\n'.uint8List);
} }
final stdoutDone = Completer<void>(); final stdoutDone = Completer<void>();
@@ -62,9 +62,9 @@ extension SSHClientX on SSHClient {
Future<int?> execWithPwd( Future<int?> execWithPwd(
String cmd, { String cmd, {
BuildContext? context, BuildContext? context,
_OnStdout? onStdout, OnStdout? onStdout,
_OnStdout? onStderr, OnStdout? onStderr,
_OnStdin? stdin, OnStdin? stdin,
bool redirectToBash = false, // not working yet. do not use bool redirectToBash = false, // not working yet. do not use
required String id, required String id,
}) async { }) async {

View File

@@ -20,18 +20,18 @@ import 'package:server_box/view/page/ssh/page.dart';
import 'package:server_box/view/page/setting/seq/virt_key.dart'; import 'package:server_box/view/page/setting/seq/virt_key.dart';
import 'package:server_box/view/page/storage/local.dart'; import 'package:server_box/view/page/storage/local.dart';
import '../data/model/server/snippet.dart'; import 'package:server_box/data/model/server/snippet.dart';
import '../view/page/editor.dart'; import 'package:server_box/view/page/editor.dart';
import '../view/page/process.dart'; import 'package:server_box/view/page/process.dart';
import '../view/page/server/edit.dart'; import 'package:server_box/view/page/server/edit.dart';
import '../view/page/server/tab.dart'; import 'package:server_box/view/page/server/tab.dart';
import '../view/page/setting/entry.dart'; import 'package:server_box/view/page/setting/entry.dart';
import '../view/page/setting/seq/srv_detail_seq.dart'; import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
import '../view/page/setting/seq/srv_seq.dart'; import 'package:server_box/view/page/setting/seq/srv_seq.dart';
import '../view/page/snippet/edit.dart'; import 'package:server_box/view/page/snippet/edit.dart';
import '../view/page/snippet/list.dart'; import 'package:server_box/view/page/snippet/list.dart';
import '../view/page/storage/sftp.dart'; import 'package:server_box/view/page/storage/sftp.dart';
import '../view/page/storage/sftp_mission.dart'; import 'package:server_box/view/page/storage/sftp_mission.dart';
class AppRoutes { class AppRoutes {
final Widget page; final Widget page;

View File

@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
/// Must put this func out of any Class. /// Must put this func out of any Class.
/// ///

View File

@@ -9,7 +9,7 @@ import 'package:server_box/data/model/app/backup.dart';
import 'package:server_box/data/model/app/sync.dart'; import 'package:server_box/data/model/app/sync.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
import '../../../data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
abstract final class ICloud { abstract final class ICloud {
static const _containerId = 'iCloud.tech.lolli.serverbox'; static const _containerId = 'iCloud.tech.lolli.serverbox';

View File

@@ -1,7 +1,7 @@
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import '../../res/build_data.dart'; import 'package:server_box/data/res/build_data.dart';
import '../server/system.dart'; import 'package:server_box/data/model/server/system.dart';
enum ShellFunc { enum ShellFunc {
status, status,
@@ -47,11 +47,11 @@ enum ShellFunc {
static String getInstallShellCmd(String id) { static String getInstallShellCmd(String id) {
final scriptDir = getScriptDir(id); final scriptDir = getScriptDir(id);
final scriptPath = '$scriptDir/$scriptFile'; final scriptPath = '$scriptDir/$scriptFile';
return """ return '''
mkdir -p $scriptDir mkdir -p $scriptDir
cat > $scriptPath cat > $scriptPath
chmod 744 $scriptPath chmod 744 $scriptPath
"""; ''';
} }
String get flag => switch (this) { String get flag => switch (this) {

View File

@@ -45,21 +45,21 @@ final class PodmanImg implements ContainerImg {
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg( factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
repository: json["repository"], repository: json['repository'],
tag: json["tag"], tag: json['tag'],
id: json["Id"], id: json['Id'],
created: json["Created"], created: json['Created'],
size: json["Size"], size: json['Size'],
containers: json["Containers"], containers: json['Containers'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"repository": repository, 'repository': repository,
"tag": tag, 'tag': tag,
"Id": id, 'Id': id,
"Created": created, 'Created': created,
"Size": size, 'Size': size,
"Containers": containers, 'Containers': containers,
}; };
} }
@@ -96,36 +96,36 @@ final class DockerImg implements ContainerImg {
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory DockerImg.fromJson(Map<String, dynamic> json) { factory DockerImg.fromJson(Map<String, dynamic> json) {
final containers = switch (json["Containers"]) { final containers = switch (json['Containers']) {
final String a => a, final String a => a,
final Object? a => a.toString(), final Object? a => a.toString(),
}; };
final repo = switch (json["Repository"] ?? json["Names"]) { final repo = switch (json['Repository'] ?? json['Names']) {
final String a => a, final String a => a,
final List a => a.firstOrNull.toString(), final List a => a.firstOrNull.toString(),
final Object? a => a.toString(), final Object? a => a.toString(),
}; };
final size = switch (json["Size"]) { final size = switch (json['Size']) {
final String a => a, final String a => a,
final int a => a.bytes2Str, final int a => a.bytes2Str,
final Object? a => a.toString(), final Object? a => a.toString(),
}; };
return DockerImg( return DockerImg(
containers: containers, containers: containers,
createdAt: json["CreatedAt"], createdAt: json['CreatedAt'],
id: json["ID"] ?? json["Id"] ?? '', id: json['ID'] ?? json['Id'] ?? '',
repository: repo, repository: repo,
size: size, size: size,
tag: json["Tag"], tag: json['Tag'],
); );
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"Containers": containers, 'Containers': containers,
"CreatedAt": createdAt, 'CreatedAt': createdAt,
"ID": id, 'ID': id,
"Repository": repository, 'Repository': repository,
"Size": size, 'Size': size,
"Tag": tag, 'Tag': tag,
}; };
} }

View File

@@ -84,29 +84,29 @@ final class PodmanPs implements ContainerPs {
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs( factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
command: json["Command"] == null command: json['Command'] == null
? [] ? []
: List<String>.from(json["Command"]!.map((x) => x)), : List<String>.from(json['Command']!.map((x) => x)),
created: created:
json["Created"] == null ? null : DateTime.parse(json["Created"]), json['Created'] == null ? null : DateTime.parse(json['Created']),
exited: json["Exited"], exited: json['Exited'],
id: json["Id"], id: json['Id'],
image: json["Image"], image: json['Image'],
names: json["Names"] == null names: json['Names'] == null
? [] ? []
: List<String>.from(json["Names"]!.map((x) => x)), : List<String>.from(json['Names']!.map((x) => x)),
startedAt: json["StartedAt"], startedAt: json['StartedAt'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"Command": 'Command':
command == null ? [] : List<dynamic>.from(command!.map((x) => x)), command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
"Created": created?.toIso8601String(), 'Created': created?.toIso8601String(),
"Exited": exited, 'Exited': exited,
"Id": id, 'Id': id,
"Image": image, 'Image': image,
"Names": names == null ? [] : List<dynamic>.from(names!.map((x) => x)), 'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
"StartedAt": startedAt, 'StartedAt': startedAt,
}; };
} }

View File

@@ -34,9 +34,9 @@ class UpgradePkgInfo {
} }
void _parseApt(String raw) { void _parseApt(String raw) {
final split1 = raw.split("/"); final split1 = raw.split('/');
package = split1[0]; package = split1[0];
final split2 = split1[1].split(" "); final split2 = split1[1].split(' ');
newVersion = split2[1]; newVersion = split2[1];
arch = split2[2]; arch = split2[2];
nowVersion = split2[5].replaceFirst(']', ''); nowVersion = split2[5].replaceFirst(']', '');
@@ -53,7 +53,7 @@ class UpgradePkgInfo {
} }
void _parseZypper(String raw) { void _parseZypper(String raw) {
final cols = raw.split("|"); final cols = raw.split('|');
package = cols[2].trim(); package = cols[2].trim();
nowVersion = cols[3].trim(); nowVersion = cols[3].trim();
newVersion = cols[4].trim(); newVersion = cols[4].trim();

View File

@@ -1,4 +1,4 @@
import '../../res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class Conn { class Conn {
final int maxConn; final int maxConn;

View File

@@ -30,11 +30,11 @@ final class ServerCustom {
static ServerCustom fromJson(Map<String, dynamic> json) { static ServerCustom fromJson(Map<String, dynamic> json) {
//final temperature = json["temperature"] as String?; //final temperature = json["temperature"] as String?;
final pveAddr = json["pveAddr"] as String?; final pveAddr = json['pveAddr'] as String?;
final pveIgnoreCert = json["pveIgnoreCert"] as bool; final pveIgnoreCert = json['pveIgnoreCert'] as bool;
final cmds = json["cmds"] as Map<String, dynamic>?; final cmds = json['cmds'] as Map<String, dynamic>?;
final preferTempDev = json["preferTempDev"] as String?; final preferTempDev = json['preferTempDev'] as String?;
final logoUrl = json["logoUrl"] as String?; final logoUrl = json['logoUrl'] as String?;
return ServerCustom( return ServerCustom(
//temperature: temperature, //temperature: temperature,
pveAddr: pveAddr, pveAddr: pveAddr,
@@ -51,18 +51,18 @@ final class ServerCustom {
// json["temperature"] = temperature; // json["temperature"] = temperature;
// } // }
if (pveAddr != null) { if (pveAddr != null) {
json["pveAddr"] = pveAddr; json['pveAddr'] = pveAddr;
} }
json["pveIgnoreCert"] = pveIgnoreCert; json['pveIgnoreCert'] = pveIgnoreCert;
if (cmds != null) { if (cmds != null) {
json["cmds"] = cmds; json['cmds'] = cmds;
} }
if (preferTempDev != null) { if (preferTempDev != null) {
json["preferTempDev"] = preferTempDev; json['preferTempDev'] = preferTempDev;
} }
if (logoUrl != null) { if (logoUrl != null) {
json["logoUrl"] = logoUrl; json['logoUrl'] = logoUrl;
} }
return json; return json;
} }

View File

@@ -1,7 +1,7 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
import '../../res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class Disk { class Disk {
final String fs; final String fs;

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'time_seq.dart'; import 'package:server_box/data/model/server/time_seq.dart';
class NetSpeedPart extends TimeSeqIface<NetSpeedPart> { class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
final String device; final String device;

View File

@@ -28,13 +28,13 @@ class PrivateKeyInfo {
} }
PrivateKeyInfo.fromJson(Map<String, dynamic> json) PrivateKeyInfo.fromJson(Map<String, dynamic> json)
: id = json["id"].toString(), : id = json['id'].toString(),
key = json["private_key"].toString(); key = json['private_key'].toString();
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, String>{}; final data = <String, String>{};
data["id"] = id; data['id'] = id;
data["private_key"] = key; data['private_key'] = key;
return data; return data;
} }
} }

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import '../../../data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
class _ProcValIdxMap { class _ProcValIdxMap {
final int pid; final int pid;
@@ -58,7 +58,7 @@ class Proc {
required this.command, required this.command,
}); });
factory Proc.parse(String raw, _ProcValIdxMap map) { factory Proc._parse(String raw, _ProcValIdxMap map) {
final parts = raw.split(RegExp(r'\s+')); final parts = raw.split(RegExp(r'\s+'));
return Proc( return Proc(
user: map.user == null ? null : parts[map.user!], user: map.user == null ? null : parts[map.user!],
@@ -139,7 +139,7 @@ class PsResult {
final line = lines[i]; final line = lines[i];
if (line.isEmpty) continue; if (line.isEmpty) continue;
try { try {
procs.add(Proc.parse(line, map)); procs.add(Proc._parse(line, map));
} catch (e, trace) { } catch (e, trace) {
errs.add('$line: $e'); errs.add('$line: $e');
Loggers.app.warning('Process failed', e, trace); Loggers.app.warning('Process failed', e, trace);

View File

@@ -90,5 +90,5 @@ enum ServerConn {
/// Status parsing finished /// Status parsing finished
finished; finished;
operator <(ServerConn other) => index < other.index; bool operator <(ServerConn other) => index < other.index;
} }

View File

@@ -6,7 +6,7 @@ import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/provider.dart';
import '../app/error.dart'; import 'package:server_box/data/model/app/error.dart';
part 'server_private_info.g.dart'; part 'server_private_info.g.dart';
@@ -67,23 +67,23 @@ class ServerPrivateInfo {
}) : id = '$user@$ip:$port'; }) : id = '$user@$ip:$port';
static ServerPrivateInfo fromJson(Map<String, dynamic> json) { static ServerPrivateInfo fromJson(Map<String, dynamic> json) {
final ip = json["ip"] as String? ?? ''; final ip = json['ip'] as String? ?? '';
final port = json["port"] as int? ?? 22; final port = json['port'] as int? ?? 22;
final user = json["user"] as String? ?? 'root'; final user = json['user'] as String? ?? 'root';
final name = json["name"] as String? ?? ''; final name = json['name'] as String? ?? '';
final pwd = json["pwd"] as String? ?? json["authorization"] as String?; final pwd = json['pwd'] as String? ?? json['authorization'] as String?;
final keyId = json["pubKeyId"] as String?; final keyId = json['pubKeyId'] as String?;
final tags = (json["tags"] as List?)?.cast<String>(); final tags = (json['tags'] as List?)?.cast<String>();
final alterUrl = json["alterUrl"] as String?; final alterUrl = json['alterUrl'] as String?;
final autoConnect = json["autoConnect"] as bool?; final autoConnect = json['autoConnect'] as bool?;
final jumpId = json["jumpId"] as String?; final jumpId = json['jumpId'] as String?;
final custom = json["customCmd"] == null final custom = json['customCmd'] == null
? null ? null
: ServerCustom.fromJson(json["custom"].cast<String, dynamic>()); : ServerCustom.fromJson(json['custom'].cast<String, dynamic>());
final wolCfg = json["wolCfg"] == null final wolCfg = json['wolCfg'] == null
? null ? null
: WakeOnLanCfg.fromJson(json["wolCfg"].cast<String, dynamic>()); : WakeOnLanCfg.fromJson(json['wolCfg'].cast<String, dynamic>());
final envs_ = json["envs"] as Map<String, dynamic>?; final envs_ = json['envs'] as Map<String, dynamic>?;
final envs = <String, String>{}; final envs = <String, String>{};
if (envs_ != null) { if (envs_ != null) {
envs_.forEach((key, value) { envs_.forEach((key, value) {
@@ -112,36 +112,36 @@ class ServerPrivateInfo {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data["name"] = name; data['name'] = name;
data["ip"] = ip; data['ip'] = ip;
data["port"] = port; data['port'] = port;
data["user"] = user; data['user'] = user;
if (pwd != null) { if (pwd != null) {
data["pwd"] = pwd; data['pwd'] = pwd;
} }
if (keyId != null) { if (keyId != null) {
data["pubKeyId"] = keyId; data['pubKeyId'] = keyId;
} }
if (tags != null) { if (tags != null) {
data["tags"] = tags; data['tags'] = tags;
} }
if (alterUrl != null) { if (alterUrl != null) {
data["alterUrl"] = alterUrl; data['alterUrl'] = alterUrl;
} }
if (autoConnect != null) { if (autoConnect != null) {
data["autoConnect"] = autoConnect; data['autoConnect'] = autoConnect;
} }
if (jumpId != null) { if (jumpId != null) {
data["jumpId"] = jumpId; data['jumpId'] = jumpId;
} }
if (custom != null) { if (custom != null) {
data["custom"] = custom?.toJson(); data['custom'] = custom?.toJson();
} }
if (wolCfg != null) { if (wolCfg != null) {
data["wolCfg"] = wolCfg?.toJson(); data['wolCfg'] = wolCfg?.toJson();
} }
if (envs != null) { if (envs != null) {
data["envs"] = envs; data['envs'] = envs;
} }
return data; return data;
} }
@@ -160,7 +160,7 @@ class ServerPrivateInfo {
custom?.cmds != old.custom?.cmds; custom?.cmds != old.custom?.cmds;
} }
_IpPort fromStringUrl() { IpPort fromStringUrl() {
if (alterUrl == null) { if (alterUrl == null) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
} }
@@ -177,7 +177,7 @@ class ServerPrivateInfo {
if (port <= 0 || port > 65535) { if (port <= 0 || port > 65535) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
} }
return _IpPort(ip_, port_); return IpPort(ip_, port_);
} }
@override @override
@@ -208,9 +208,9 @@ class ServerPrivateInfo {
); );
} }
class _IpPort { class IpPort {
final String ip; final String ip;
final int port; final int port;
_IpPort(this.ip, this.port); IpPort(this.ip, this.port);
} }

View File

@@ -5,12 +5,12 @@ import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
import '../app/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import 'cpu.dart'; import 'package:server_box/data/model/server/cpu.dart';
import 'disk.dart'; import 'package:server_box/data/model/server/disk.dart';
import 'memory.dart'; import 'package:server_box/data/model/server/memory.dart';
import 'net_speed.dart'; import 'package:server_box/data/model/server/net_speed.dart';
import 'conn.dart'; import 'package:server_box/data/model/server/conn.dart';
class ServerStatusUpdateReq { class ServerStatusUpdateReq {
final ServerStatus ss; final ServerStatus ss;

View File

@@ -5,7 +5,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart'; import 'package:xterm/core.dart';
import '../app/tag_pickable.dart'; import 'package:server_box/data/model/app/tag_pickable.dart';
part 'snippet.g.dart'; part 'snippet.g.dart';

View File

@@ -2,12 +2,14 @@ import 'package:fl_lib/fl_lib.dart';
class AbsolutePath { class AbsolutePath {
String _path; String _path;
final _prePath = <String>[];
AbsolutePath(this._path);
String get path => _path; String get path => _path;
final List<String> _prePath;
AbsolutePath(this._path) : _prePath = ['/']; /// Update path, not set path
set path(String newPath) {
void update(String newPath) {
_prePath.add(_path); _prePath.add(_path);
if (newPath == '..') { if (newPath == '..') {
_path = _path.substring(0, _path.lastIndexOf('/')); _path = _path.substring(0, _path.lastIndexOf('/'));
@@ -16,10 +18,6 @@ class AbsolutePath {
} }
return; return;
} }
if (newPath == '/') {
_path = '/';
return;
}
if (newPath.startsWith('/')) { if (newPath.startsWith('/')) {
_path = newPath; _path = newPath;
return; return;

View File

@@ -2,9 +2,11 @@ import 'package:dartssh2/dartssh2.dart';
import 'package:server_box/data/model/sftp/absolute_path.dart'; import 'package:server_box/data/model/sftp/absolute_path.dart';
class SftpBrowserStatus { class SftpBrowserStatus {
List<SftpName>? files; final List<SftpName> files = [];
AbsolutePath? path; final AbsolutePath path = AbsolutePath('/');
SftpClient? client; SftpClient? client;
SftpBrowserStatus(); SftpBrowserStatus(SSHClient client) {
client.sftp().then((value) => this.client = value);
}
} }

View File

@@ -70,7 +70,7 @@ class ContainerProvider extends ChangeNotifier {
} }
final res = await client?.run(_wrap(ContainerCmdType.images.exec(type))); final res = await client?.run(_wrap(ContainerCmdType.images.exec(type)));
if (res?.string.toLowerCase().contains("permission denied") ?? false) { if (res?.string.toLowerCase().contains('permission denied') ?? false) {
return sudoCompleter.complete(true); return sudoCompleter.complete(true);
} }
return sudoCompleter.complete(false); return sudoCompleter.complete(false);

View File

@@ -104,7 +104,7 @@ final class PveProvider extends ChangeNotifier {
socket.cast<List<int>>().pipe(forward.sink); socket.cast<List<int>>().pipe(forward.sink);
});*/ });*/
if (url.isScheme("https")) { if (url.isScheme('https')) {
return SecureSocket.startConnect('localhost', _localPort, return SecureSocket.startConnect('localhost', _localPort,
onBadCertificate: (_) => true); onBadCertificate: (_) => true);
} else { } else {

View File

@@ -14,12 +14,12 @@ import 'package:server_box/data/model/server/system.dart';
// import 'package:server_box/data/res/provider.dart'; // import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../../core/utils/server.dart'; import 'package:server_box/core/utils/server.dart';
import '../model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import '../model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import '../model/server/server_status_update_req.dart'; import 'package:server_box/data/model/server/server_status_update_req.dart';
import '../model/server/try_limiter.dart'; import 'package:server_box/data/model/server/try_limiter.dart';
import '../res/status.dart'; import 'package:server_box/data/res/status.dart';
class ServerProvider extends ChangeNotifier { class ServerProvider extends ChangeNotifier {
final Map<String, Server> _servers = {}; final Map<String, Server> _servers = {};

View File

@@ -1,7 +1,7 @@
// This file is generated by fl_build. Do not edit. // This file is generated by fl_build. Do not edit.
class BuildData { class BuildData {
static const String name = "ServerBox"; static const String name = 'ServerBox';
static const int build = 1058; static const int build = 1058;
static const int script = 56; static const int script = 56;
} }

View File

@@ -1,12 +1,12 @@
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/temp.dart'; import 'package:server_box/data/model/server/temp.dart';
import '../model/server/cpu.dart'; import 'package:server_box/data/model/server/cpu.dart';
import '../model/server/disk.dart'; import 'package:server_box/data/model/server/disk.dart';
import '../model/server/memory.dart'; import 'package:server_box/data/model/server/memory.dart';
import '../model/server/net_speed.dart'; import 'package:server_box/data/model/server/net_speed.dart';
import '../model/server/conn.dart'; import 'package:server_box/data/model/server/conn.dart';
import '../model/server/system.dart'; import 'package:server_box/data/model/server/system.dart';
abstract final class InitStatus { abstract final class InitStatus {
static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore( static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore(

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import '../model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
class PrivateKeyStore extends PersistentStore { class PrivateKeyStore extends PersistentStore {
PrivateKeyStore() : super('key'); PrivateKeyStore() : super('key');

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import '../model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
class ServerStore extends PersistentStore { class ServerStore extends PersistentStore {
ServerStore() : super('server'); ServerStore() : super('server');

View File

@@ -3,8 +3,8 @@ import 'package:server_box/data/model/app/menu/server_func.dart';
import 'package:server_box/data/model/app/server_detail_card.dart'; import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart'; import 'package:server_box/data/model/ssh/virtual_key.dart';
import '../model/app/net_view.dart'; import 'package:server_box/data/model/app/net_view.dart';
import '../res/default.dart'; import 'package:server_box/data/res/default.dart';
class SettingStore extends PersistentStore { class SettingStore extends PersistentStore {
SettingStore() : super('setting'); SettingStore() : super('setting');

View File

@@ -1,6 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import '../model/server/snippet.dart'; import 'package:server_box/data/model/server/snippet.dart';
class SnippetStore extends PersistentStore { class SnippetStore extends PersistentStore {
SnippetStore() : super('snippet'); SnippetStore() : super('snippet');

View File

@@ -65,7 +65,6 @@
"goBackQ": "Zurückkommen?", "goBackQ": "Zurückkommen?",
"goto": "Pfad öffnen", "goto": "Pfad öffnen",
"hideTitleBar": "Titelleiste ausblenden", "hideTitleBar": "Titelleiste ausblenden",
"hideTitleBarTip": "Nach dem Einschalten halten Sie bitte die drei Tasten in der oberen rechten Ecke gedrückt, um sie zu ziehen.",
"highlight": "Code highlight", "highlight": "Code highlight",
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren", "homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
"host": "Host", "host": "Host",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Go back?", "goBackQ": "Go back?",
"goto": "Go to", "goto": "Go to",
"hideTitleBar": "Hide title bar", "hideTitleBar": "Hide title bar",
"hideTitleBarTip": "After turning it on, please hold down the three buttons in the top right corner to drag.",
"highlight": "Code highlighting", "highlight": "Code highlighting",
"homeWidgetUrlConfig": "Config home widget url", "homeWidgetUrlConfig": "Config home widget url",
"host": "Host", "host": "Host",

View File

@@ -65,7 +65,6 @@
"goBackQ": "¿Regresar?", "goBackQ": "¿Regresar?",
"goto": "Ir a", "goto": "Ir a",
"hideTitleBar": "Ocultar barra de título", "hideTitleBar": "Ocultar barra de título",
"hideTitleBarTip": "Después de encenderlo, mantenga presionados los tres botones en la esquina superior derecha para arrastrar.",
"highlight": "Resaltar código", "highlight": "Resaltar código",
"homeWidgetUrlConfig": "Configuración de URL del widget de inicio", "homeWidgetUrlConfig": "Configuración de URL del widget de inicio",
"host": "Anfitrión", "host": "Anfitrión",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Revenir en arrière ?", "goBackQ": "Revenir en arrière ?",
"goto": "Aller à", "goto": "Aller à",
"hideTitleBar": "Masquer la barre de titre", "hideTitleBar": "Masquer la barre de titre",
"hideTitleBarTip": "Après l'avoir allumé, veuillez maintenir les trois boutons dans le coin supérieur droit pour les faire glisser.",
"highlight": "Mise en surbrillance du code", "highlight": "Mise en surbrillance du code",
"homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil", "homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil",
"host": "Hôte", "host": "Hôte",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Datang kembali?", "goBackQ": "Datang kembali?",
"goto": "Pergi ke", "goto": "Pergi ke",
"hideTitleBar": "Sembunyikan bilah judul", "hideTitleBar": "Sembunyikan bilah judul",
"hideTitleBarTip": "Setelah dinyalakan, tekan dan tahan tiga tombol di sudut kanan atas untuk menyeret.",
"highlight": "Sorotan kode", "highlight": "Sorotan kode",
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah", "homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
"host": "Host", "host": "Host",

View File

@@ -65,7 +65,6 @@
"goBackQ": "戻りますか?", "goBackQ": "戻りますか?",
"goto": "移動", "goto": "移動",
"hideTitleBar": "タイトルバーを非表示にする", "hideTitleBar": "タイトルバーを非表示にする",
"hideTitleBarTip": "電源を入れた後、右上隅の3つのボタンを押し続けてドラッグしてください。",
"highlight": "コードハイライト", "highlight": "コードハイライト",
"homeWidgetUrlConfig": "ホームウィジェットURL設定", "homeWidgetUrlConfig": "ホームウィジェットURL設定",
"host": "ホスト", "host": "ホスト",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Terug gaan?", "goBackQ": "Terug gaan?",
"goto": "Ga naar", "goto": "Ga naar",
"hideTitleBar": "Titelbalk verbergen", "hideTitleBar": "Titelbalk verbergen",
"hideTitleBarTip": "Houd na het inschakelen de drie knoppen in de rechterbovenhoek ingedrukt om te slepen.",
"highlight": "Code-highlight", "highlight": "Code-highlight",
"homeWidgetUrlConfig": "Home-widget-url configureren", "homeWidgetUrlConfig": "Home-widget-url configureren",
"host": "Host", "host": "Host",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Voltar?", "goBackQ": "Voltar?",
"goto": "Ir para", "goto": "Ir para",
"hideTitleBar": "Ocultar barra de título", "hideTitleBar": "Ocultar barra de título",
"hideTitleBarTip": "Após ligar, segure os três botões no canto superior direito para arrastar.",
"highlight": "Destaque de código", "highlight": "Destaque de código",
"homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial", "homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial",
"host": "Host", "host": "Host",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Вернуться?", "goBackQ": "Вернуться?",
"goto": "перейти к", "goto": "перейти к",
"hideTitleBar": "Скрыть заголовок", "hideTitleBar": "Скрыть заголовок",
"hideTitleBarTip": "После включения удерживайте три кнопки в правом верхнем углу, чтобы перетаскивать.",
"highlight": "подсветка кода", "highlight": "подсветка кода",
"homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана", "homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана",
"host": "хост", "host": "хост",

View File

@@ -65,7 +65,6 @@
"goBackQ": "Geri dön?", "goBackQ": "Geri dön?",
"goto": "Git", "goto": "Git",
"hideTitleBar": "Başlık çubuğunu gizle", "hideTitleBar": "Başlık çubuğunu gizle",
"hideTitleBarTip": "Açtıktan sonra, sağ üst köşedeki üç düğmeyi basılı tutarak sürükleyin.",
"highlight": "Kod vurgulama", "highlight": "Kod vurgulama",
"homeWidgetUrlConfig": "Ana sayfa widget URL'sini yapılandır", "homeWidgetUrlConfig": "Ana sayfa widget URL'sini yapılandır",
"host": "Sunucu", "host": "Sunucu",

View File

@@ -65,7 +65,6 @@
"goBackQ": "返回?", "goBackQ": "返回?",
"goto": "前往", "goto": "前往",
"hideTitleBar": "隐藏标题栏", "hideTitleBar": "隐藏标题栏",
"hideTitleBarTip": "开启后请按住右上角三个按钮来拖动",
"highlight": "代码高亮", "highlight": "代码高亮",
"homeWidgetUrlConfig": "桌面部件链接配置", "homeWidgetUrlConfig": "桌面部件链接配置",
"host": "主机", "host": "主机",

View File

@@ -65,7 +65,6 @@
"goBackQ": "返回?", "goBackQ": "返回?",
"goto": "前往", "goto": "前往",
"hideTitleBar": "隱藏標題欄", "hideTitleBar": "隱藏標題欄",
"hideTitleBarTip": "開啟後請按住右上角三個按鈕來拖動",
"highlight": "代碼高亮", "highlight": "代碼高亮",
"homeWidgetUrlConfig": "桌面部件鏈接配置", "homeWidgetUrlConfig": "桌面部件鏈接配置",
"host": "主機", "host": "主機",

View File

@@ -339,7 +339,7 @@ class BackupPage extends StatelessWidget {
Future<void> _onTapWebdavUp(BuildContext context) async { Future<void> _onTapWebdavUp(BuildContext context) async {
webdavLoading.value = true; webdavLoading.value = true;
final date = DateTime.now().ymdhms(ymdSep: "-", hmsSep: "-", sep: "-"); final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
final bakName = '$date-${Miscs.bakFileName}'; final bakName = '$date-${Miscs.bakFileName}';
try { try {
await Backup.backup(bakName); await Backup.backup(bakName);

View File

@@ -11,10 +11,10 @@ import 'package:server_box/data/model/container/image.dart';
import 'package:server_box/data/model/container/type.dart'; import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../../data/model/container/ps.dart'; import 'package:server_box/data/model/container/ps.dart';
import '../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import '../../data/provider/container.dart'; import 'package:server_box/data/provider/container.dart';
import '../widget/two_line_text.dart'; import 'package:server_box/view/widget/two_line_text.dart';
class ContainerPage extends StatefulWidget { class ContainerPage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;

View File

@@ -12,7 +12,7 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/highlight.dart'; import 'package:server_box/data/res/highlight.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../widget/two_line_text.dart'; import 'package:server_box/view/widget/two_line_text.dart';
class EditorPage extends StatefulWidget { class EditorPage extends StatefulWidget {
/// If path is not null, then it's a file editor /// If path is not null, then it's a file editor

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/provider.dart';
import '../../data/model/server/ping_result.dart'; import 'package:server_box/data/model/server/ping_result.dart';
/// Only permit ipv4 / ipv6 / domain chars /// Only permit ipv4 / ipv6 / domain chars
final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+'); final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+');

View File

@@ -8,8 +8,8 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/provider.dart';
import '../../../core/utils/server.dart'; import 'package:server_box/core/utils/server.dart';
import '../../../data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
const _format = 'text/plain'; const _format = 'text/plain';
@@ -107,7 +107,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
} }
String _standardizeLineSeparators(String value) { String _standardizeLineSeparators(String value) {
return value.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); return value.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
} }
Widget _buildFAB() { Widget _buildFAB() {

View File

@@ -7,9 +7,9 @@ import 'package:provider/provider.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../data/model/server/private_key_info.dart'; import 'package:server_box/data/model/server/private_key_info.dart';
import '../../../data/provider/private_key.dart'; import 'package:server_box/data/provider/private_key.dart';
class PrivateKeysListPage extends StatefulWidget { class PrivateKeysListPage extends StatefulWidget {
const PrivateKeysListPage({super.key}); const PrivateKeysListPage({super.key});

View File

@@ -6,10 +6,10 @@ import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../../data/model/app/shell_func.dart'; import 'package:server_box/data/model/app/shell_func.dart';
import '../../data/model/server/proc.dart'; import 'package:server_box/data/model/server/proc.dart';
import '../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import '../widget/two_line_text.dart'; import 'package:server_box/view/widget/two_line_text.dart';
class ProcessPage extends StatefulWidget { class ProcessPage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;

View File

@@ -20,9 +20,9 @@ import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/widget/server_func_btns.dart'; import 'package:server_box/view/widget/server_func_btns.dart';
import '../../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../../data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import '../../../../data/provider/server.dart'; import 'package:server_box/data/provider/server.dart';
part 'misc.dart'; part 'misc.dart';

View File

@@ -9,9 +9,9 @@ import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/provider.dart';
import '../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import '../../../data/provider/private_key.dart'; import 'package:server_box/data/provider/private_key.dart';
class ServerEditPage extends StatefulWidget { class ServerEditPage extends StatefulWidget {
const ServerEditPage({super.key, this.spi}); const ServerEditPage({super.key, this.spi});

View File

@@ -13,12 +13,12 @@ import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/widget/percent_circle.dart'; import 'package:server_box/view/widget/percent_circle.dart';
import '../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../data/model/app/net_view.dart'; import 'package:server_box/data/model/app/net_view.dart';
import '../../../data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import '../../../data/provider/server.dart'; import 'package:server_box/data/provider/server.dart';
import '../../widget/server_func_btns.dart'; import 'package:server_box/view/widget/server_func_btns.dart';
class ServerPage extends StatefulWidget { class ServerPage extends StatefulWidget {
const ServerPage({super.key}); const ServerPage({super.key});

View File

@@ -10,9 +10,9 @@ import 'package:server_box/data/res/rebuild.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/res/url.dart'; import 'package:server_box/data/res/url.dart';
import '../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../data/model/app/net_view.dart'; import 'package:server_box/data/model/app/net_view.dart';
import '../../../data/res/build_data.dart'; import 'package:server_box/data/res/build_data.dart';
const _kIconSize = 23.0; const _kIconSize = 23.0;
@@ -1032,7 +1032,6 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildHideTitleBar() { Widget _buildHideTitleBar() {
return ListTile( return ListTile(
title: Text(l10n.hideTitleBar), title: Text(l10n.hideTitleBar),
subtitle: Text(l10n.hideTitleBarTip, style: UIs.textGrey),
trailing: StoreSwitch(prop: _setting.hideTitleBar), trailing: StoreSwitch(prop: _setting.hideTitleBar),
); );
} }

View File

@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import '../../../data/model/server/snippet.dart'; import 'package:server_box/data/model/server/snippet.dart';
import '/core/route.dart'; import 'package:server_box/core/route.dart';
import '/data/provider/snippet.dart'; import 'package:server_box/data/provider/snippet.dart';
class SnippetListPage extends StatefulWidget { class SnippetListPage extends StatefulWidget {
const SnippetListPage({super.key}); const SnippetListPage({super.key});

View File

@@ -18,10 +18,10 @@ import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:xterm/core.dart'; import 'package:xterm/core.dart';
import 'package:xterm/ui.dart' hide TerminalThemes; import 'package:xterm/ui.dart' hide TerminalThemes;
import '../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import '../../../data/model/ssh/virtual_key.dart'; import 'package:server_box/data/model/ssh/virtual_key.dart';
import '../../../data/res/terminal.dart'; import 'package:server_box/data/res/terminal.dart';
const _echoPWD = 'echo \$PWD'; const _echoPWD = 'echo \$PWD';

View File

@@ -9,8 +9,8 @@ import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/provider.dart';
import 'package:server_box/view/widget/omit_start_text.dart'; import 'package:server_box/view/widget/omit_start_text.dart';
import '../../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../../data/model/app/path_with_prefix.dart'; import 'package:server_box/data/model/app/path_with_prefix.dart';
class LocalStoragePage extends StatefulWidget { class LocalStoragePage extends StatefulWidget {
final bool isPickFile; final bool isPickFile;

View File

@@ -9,7 +9,6 @@ import 'package:server_box/core/extension/sftpfile.dart';
import 'package:server_box/core/route.dart'; import 'package:server_box/core/route.dart';
import 'package:server_box/core/utils/comparator.dart'; import 'package:server_box/core/utils/comparator.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/sftp/absolute_path.dart';
import 'package:server_box/data/model/sftp/browser_status.dart'; import 'package:server_box/data/model/sftp/browser_status.dart';
import 'package:server_box/data/model/sftp/worker.dart'; import 'package:server_box/data/model/sftp/worker.dart';
import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/misc.dart';
@@ -38,30 +37,34 @@ class SftpPage extends StatefulWidget {
} }
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin { class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
final _status = SftpBrowserStatus(); late final _status = SftpBrowserStatus(_client);
late final _client = widget.spi.server?.client; late final _client = widget.spi.server!.client!;
final _sortOption = _SortOption().vn;
final _sortOption =
ValueNotifier(_SortOption(sortBy: _SortType.name, reversed: false));
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final children = [
Btn.icon(
icon: const Icon(Icons.downloading),
onTap: () => AppRoutes.sftpMission().go(context),
),
_buildSortMenu(),
_buildSearchBtn(),
];
if (isDesktop) children.add(_buildRefreshBtn());
return Scaffold( return Scaffold(
appBar: CustomAppBar( appBar: CustomAppBar(
leading: IconButton(
icon: const BackButtonIcon(),
onPressed: () {
_status.path?.update('/');
context.pop();
},
),
title: TwoLineText(up: 'SFTP', down: widget.spi.name), title: TwoLineText(up: 'SFTP', down: widget.spi.name),
actions: [ actions: children,
IconButton(
icon: const Icon(Icons.downloading),
onPressed: () => AppRoutes.sftpMission().go(context),
), ),
ValBuilder( body: _buildFileView(),
bottomNavigationBar: _buildBottom(),
);
}
Widget _buildSortMenu() {
return ValBuilder(
listenable: _sortOption, listenable: _sortOption,
builder: (value) { builder: (value) {
return PopupMenuButton<_SortType>( return PopupMenuButton<_SortType>(
@@ -75,41 +78,32 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
]; ];
return options.map((r) { return options.map((r) {
final (type, name) = r; final (type, name) = r;
final selected = type == currentSelectedOption.sortBy;
final title = selected
? "$name (${currentSelectedOption.reversed ? '-' : '+'})"
: name;
return PopupMenuItem( return PopupMenuItem(
value: type, value: type,
child: Text( child: Text(
type == currentSelectedOption.sortBy title,
? "$name (${currentSelectedOption.reversed ? '-' : '+'})"
: name,
style: TextStyle( style: TextStyle(
color: type == currentSelectedOption.sortBy color: selected ? UIs.primaryColor : null,
? UIs.primaryColor fontWeight: selected ? FontWeight.bold : null,
: null,
fontWeight: type == currentSelectedOption.sortBy
? FontWeight.bold
: null,
), ),
), ),
); );
}).toList(); }).toList();
}, },
onSelected: (sortBy) { onSelected: (sortBy) {
final oldValue = _sortOption.value; final old = _sortOption.value;
if (oldValue.sortBy == sortBy) { if (old.sortBy == sortBy) {
_sortOption.value = _SortOption( _sortOption.value = old.copyWith(reversed: !old.reversed);
sortBy: sortBy, reversed: !oldValue.reversed);
} else { } else {
_sortOption.value = _sortOption.value = old.copyWith(sortBy: sortBy);
_SortOption(sortBy: sortBy, reversed: false);
} }
}, },
); );
}, },
),
],
),
body: _buildFileView(),
bottomNavigationBar: _buildBottom(),
); );
} }
@@ -117,32 +111,25 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
final children = widget.isSelect final children = widget.isSelect
? [ ? [
IconButton( IconButton(
onPressed: () => context.pop(_status.path?.path), onPressed: () => context.pop(_status.path.path),
icon: const Icon(Icons.done), icon: const Icon(Icons.done),
), ),
_buildSearchBtn(), _buildSearchBtn(),
] ]
: [ : [
IconButton( _buildBackBtn(),
padding: const EdgeInsets.all(0), _buildHomeBtn(),
onPressed: () async {
await _backward();
},
icon: const Icon(Icons.arrow_back),
),
_buildAddBtn(), _buildAddBtn(),
_buildGotoBtn(), _buildGotoBtn(),
_buildUploadBtn(), _buildUploadBtn(),
_buildSearchBtn(),
]; ];
if (isDesktop) children.add(_buildRefreshBtn());
return SafeArea( return SafeArea(
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11), padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
OmitStartText(_status.path?.path ?? '...'), OmitStartText(_status.path.path),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children, children: children,
@@ -153,169 +140,18 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
); );
} }
Widget _buildSearchBtn() {
return IconButton(
onPressed: () async {
Stream<SftpName> find(String query) async* {
final fs = _status.files;
if (fs == null) return;
for (final f in fs) {
if (f.filename.contains(query)) yield f;
}
}
final search = SearchPage(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
future: (q) => find(q).toList(),
builder: (ctx, e) => _buildItem(e, beforeTap: () => ctx.pop()),
);
await showSearch(context: context, delegate: search);
},
icon: const Icon(Icons.search),
);
}
Widget _buildUploadBtn() {
return IconButton(
onPressed: () async {
final idx = await context.showRoundDialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.open_in_new),
title: Text(l10n.system),
onTap: () => context.pop(1),
),
ListTile(
leading: const Icon(Icons.folder),
title: Text(l10n.inner),
onTap: () => context.pop(0),
),
],
));
final path = await () async {
switch (idx) {
case 0:
return await AppRoutes.localStorage(isPickFile: true)
.go<String>(context);
case 1:
return await Pfs.pickFilePath();
default:
return null;
}
}();
if (path == null) {
return;
}
final remoteDir = _status.path?.path;
if (remoteDir == null) {
context.showSnackBar('remote path is null');
return;
}
final fileName = path.split(Platform.pathSeparator).lastOrNull;
final remotePath = '$remoteDir/$fileName';
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
Pros.sftp.add(
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
);
},
icon: const Icon(Icons.upload_file),
);
}
Widget _buildAddBtn() {
return IconButton(
onPressed: () => context.showRoundDialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.folder),
title: Text(libL10n.folder),
onTap: _mkdir,
),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(libL10n.file),
onTap: _newFile,
),
],
),
),
icon: const Icon(Icons.add),
);
}
Widget _buildGotoBtn() {
return IconButton(
padding: const EdgeInsets.all(0),
onPressed: () async {
final p = await context.showRoundDialog<String>(
title: l10n.goto,
child: Autocomplete<String>(
optionsBuilder: (val) {
if (!Stores.setting.recordHistory.fetch()) {
return [];
}
return Stores.history.sftpGoPath.all.cast<String>().where(
(element) => element.contains(val.text),
);
},
fieldViewBuilder: (_, controller, node, __) {
return Input(
autoFocus: true,
icon: Icons.abc,
label: libL10n.path,
node: node,
controller: controller,
suggestion: true,
onSubmitted: (value) => context.pop(value),
);
},
),
);
if (p == null || p.isEmpty) {
return;
}
_status.path?.update(p);
final suc = await _listDir() ?? false;
if (suc && Stores.setting.recordHistory.fetch()) {
Stores.history.sftpGoPath.add(p);
}
},
icon: const Icon(Icons.gps_fixed),
);
}
Widget _buildRefreshBtn() {
return IconButton(
onPressed: () => _listDir(),
icon: const Icon(Icons.refresh),
);
}
Widget _buildFileView() { Widget _buildFileView() {
if (_status.files == null) { if (_status.files.isEmpty) return Center(child: Text(libL10n.empty));
return UIs.centerLoading;
}
if (_status.files!.isEmpty) {
return const Center(
child: Text('~'),
);
}
return RefreshIndicator( return RefreshIndicator(
onRefresh: _listDir,
child: FadeIn( child: FadeIn(
key: Key(widget.spi.name + _status.path!.path), key: Key(widget.spi.name + _status.path.path),
child: ValBuilder( child: ValBuilder(
listenable: _sortOption, listenable: _sortOption,
builder: (sortOption) { builder: (sortOption) {
final files = sortOption.sortBy.sort( final files = sortOption.sortBy.sort(
_status.files!, _status.files,
reversed: sortOption.reversed, reversed: sortOption.reversed,
); );
return ListView.builder( return ListView.builder(
@@ -326,7 +162,6 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
}, },
), ),
), ),
onRefresh: () => _listDir(),
); );
} }
@@ -351,7 +186,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
onTap: () { onTap: () {
beforeTap?.call(); beforeTap?.call();
if (isDir) { if (isDir) {
_status.path?.update(file.filename); _status.path.path = file.filename;
_listDir(); _listDir();
} else { } else {
_onItemPress(file, true); _onItemPress(file, true);
@@ -402,7 +237,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
final permStr = newPerm.perm; final permStr = newPerm.perm;
if (ok == true && permStr != perm.perm) { if (ok == true && permStr != perm.perm) {
await context.showLoadingDialog(fn: () async { await context.showLoadingDialog(fn: () async {
await _client!.run('chmod $permStr "${_getRemotePath(file)}"'); await _client.run('chmod $permStr "${_getRemotePath(file)}"');
await _listDir(); await _listDir();
}); });
} }
@@ -570,7 +405,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
fn: () async { fn: () async {
final remotePath = _getRemotePath(file); final remotePath = _getRemotePath(file);
if (useRmr) { if (useRmr) {
await _client!.run('rm -r "$remotePath"'); await _client.run('rm -r "$remotePath"');
} else if (file.attr.isDirectory) { } else if (file.attr.isDirectory) {
await _status.client!.rmdir(remotePath); await _status.client!.rmdir(remotePath);
} else { } else {
@@ -606,7 +441,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
final (suc, err) = await context.showLoadingDialog( final (suc, err) = await context.showLoadingDialog(
fn: () async { fn: () async {
final dir = '${_status.path!.path}/$text'; final dir = '${_status.path.path}/$text';
await _status.client!.mkdir(dir); await _status.client!.mkdir(dir);
return true; return true;
}, },
@@ -651,8 +486,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
final (suc, err) = await context.showLoadingDialog( final (suc, err) = await context.showLoadingDialog(
fn: () async { fn: () async {
final path = '${_status.path!.path}/$text'; final path = '${_status.path.path}/$text';
await _client!.run('touch "$path"'); await _client.run('touch "$path"');
return true; return true;
}, },
); );
@@ -748,7 +583,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
} }
String _getRemotePath(SftpName name) { String _getRemotePath(SftpName name) {
final prePath = _status.path!.path; final prePath = _status.path.path;
// Only support Linux as remote now, so the seperator is '/' // Only support Linux as remote now, so the seperator is '/'
return prePath.joinPath(name.filename, seperator: '/'); return prePath.joinPath(name.filename, seperator: '/');
} }
@@ -761,8 +596,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
Future<bool?> _listDir() async { Future<bool?> _listDir() async {
final (ret, err) = await context.showLoadingDialog( final (ret, err) = await context.showLoadingDialog(
fn: () async { fn: () async {
_status.client ??= await _client?.sftp(); _status.client ??= await _client.sftp();
final listPath = _status.path?.path ?? '/'; final listPath = _status.path.path;
final fs = await _status.client?.listdir(listPath); final fs = await _status.client?.listdir(listPath);
if (fs == null) { if (fs == null) {
return false; return false;
@@ -776,16 +611,16 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
fs.removeAt(0); fs.removeAt(0);
} }
/// Issue #96 if (fs.isNotEmpty &&
/// Due to [WillPopScope] added in this page fs.firstOrNull?.filename == '..' &&
/// There is no need to keep '..' folder in listdir _status.path.path == '/') {
/// So remove it
if (fs.isNotEmpty && fs.firstOrNull?.filename == '..') {
fs.removeAt(0); fs.removeAt(0);
} }
if (mounted) { if (mounted) {
setState(() { setState(() {
_status.files = fs; _status.files
..clear()
..addAll(fs);
}); });
// Only update history when success // Only update history when success
@@ -803,11 +638,162 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
} }
Future<void> _backward() async { Future<void> _backward() async {
if (_status.path?.undo() ?? false) { if (_status.path.undo()) {
await _listDir(); await _listDir();
} }
} }
Widget _buildBackBtn() {
return Btn.icon(
onTap: _backward,
icon: const Icon(Icons.arrow_back),
);
}
Widget _buildSearchBtn() {
return Btn.icon(
onTap: () {
Stream<SftpName> find(String query) async* {
final fs = _status.files;
for (final f in fs) {
if (f.filename.contains(query)) yield f;
}
}
showSearch(
context: context,
delegate: SearchPage(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
future: (q) => find(q).toList(),
builder: (ctx, e) => _buildItem(e, beforeTap: () => ctx.pop()),
));
},
icon: const Icon(Icons.search),
);
}
Widget _buildUploadBtn() {
return Btn.icon(
onTap: () async {
final idx = await context.showRoundDialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Btn.tile(
icon: const Icon(Icons.open_in_new),
text: l10n.system,
onTap: () => context.pop(1),
),
Btn.tile(
icon: const Icon(Icons.folder),
text: l10n.inner,
onTap: () => context.pop(0),
),
],
));
final path = switch (idx) {
0 =>
await AppRoutes.localStorage(isPickFile: true).go<String>(context),
1 => await Pfs.pickFilePath(),
_ => null,
};
if (path == null) return;
final remoteDir = _status.path.path;
final fileName = path.split(Platform.pathSeparator).lastOrNull;
final remotePath = '$remoteDir/$fileName';
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
Pros.sftp.add(
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
);
},
icon: const Icon(Icons.upload_file),
);
}
Widget _buildAddBtn() {
return Btn.icon(
onTap: () => context.showRoundDialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Btn.tile(
icon: const Icon(Icons.folder),
text: libL10n.folder,
onTap: _mkdir,
),
Btn.tile(
icon: const Icon(Icons.insert_drive_file),
text: libL10n.file,
onTap: _newFile,
),
],
),
),
icon: const Icon(Icons.add),
);
}
Widget _buildGotoBtn() {
return Btn.icon(
onTap: () async {
final p = await context.showRoundDialog<String>(
title: l10n.goto,
child: Autocomplete<String>(
optionsBuilder: (val) {
if (!Stores.setting.recordHistory.fetch()) {
return [];
}
return Stores.history.sftpGoPath.all.cast<String>().where(
(element) => element.contains(val.text),
);
},
fieldViewBuilder: (_, controller, node, __) {
return Input(
autoFocus: true,
icon: Icons.abc,
label: libL10n.path,
node: node,
controller: controller,
suggestion: true,
onSubmitted: (value) => context.pop(value),
);
},
),
);
if (p == null || p.isEmpty) {
return;
}
_status.path.path = p;
final suc = await _listDir() ?? false;
if (suc && Stores.setting.recordHistory.fetch()) {
Stores.history.sftpGoPath.add(p);
}
},
icon: const Icon(Icons.gps_fixed),
);
}
Widget _buildRefreshBtn() {
return Btn.icon(
onTap: _listDir,
icon: const Icon(Icons.refresh),
);
}
Widget _buildHomeBtn() {
return IconButton(
onPressed: () {
final user = widget.spi.user;
_status.path.path = user != 'root' ? '/home/$user' : '/root';
_listDir();
},
icon: const Icon(Icons.home),
);
}
@override @override
FutureOr<void> afterFirstLayout(BuildContext context) { FutureOr<void> afterFirstLayout(BuildContext context) {
var initPath = '/'; var initPath = '/';
@@ -817,7 +803,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
initPath = history; initPath = history;
} }
} }
_status.path = AbsolutePath(widget.initPath ?? initPath);
_status.path.path = widget.initPath ?? initPath;
_listDir(); _listDir();
} }
} }
@@ -947,5 +934,15 @@ class _SortOption {
final _SortType sortBy; final _SortType sortBy;
final bool reversed; final bool reversed;
_SortOption({required this.sortBy, required this.reversed}); _SortOption({this.sortBy = _SortType.name, this.reversed = false});
_SortOption copyWith({
_SortType? sortBy,
bool? reversed,
}) {
return _SortOption(
sortBy: sortBy ?? this.sortBy,
reversed: reversed ?? this.reversed,
);
}
} }

View File

@@ -10,9 +10,9 @@ import 'package:server_box/data/res/provider.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/systemd.dart'; import 'package:server_box/view/page/systemd.dart';
import '../../core/route.dart'; import 'package:server_box/core/route.dart';
import '../../core/utils/server.dart'; import 'package:server_box/core/utils/server.dart';
import '../../data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
class ServerFuncBtnsTopRight extends StatelessWidget { class ServerFuncBtnsTopRight extends StatelessWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
@@ -196,17 +196,17 @@ void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
await file.delete(); await file.delete();
} }
await file.writeAsString(getPrivateKey(spi.keyId!)); await file.writeAsString(getPrivateKey(spi.keyId!));
extraArgs.addAll(["-i", path]); extraArgs.addAll(['-i', path]);
} }
final sshCommand = ["ssh", "${spi.user}@${spi.ip}"] + extraArgs; final sshCommand = ['ssh', '${spi.user}@${spi.ip}'] + extraArgs;
final system = Pfs.type; final system = Pfs.type;
switch (system) { switch (system) {
case Pfs.windows: case Pfs.windows:
await Process.start("cmd", ["/c", "start"] + sshCommand); await Process.start('cmd', ['/c', 'start'] + sshCommand);
break; break;
case Pfs.linux: case Pfs.linux:
await Process.start("x-terminal-emulator", ["-e"] + sshCommand); await Process.start('x-terminal-emulator', ['-e'] + sshCommand);
break; break;
default: default:
context.showSnackBar('Mismatch system: $system'); context.showSnackBar('Mismatch system: $system');

View File

@@ -59,7 +59,7 @@ final class UnixPermEditor extends StatefulWidget {
{super.key, required this.perm, required this.onChanged}); {super.key, required this.perm, required this.onChanged});
@override @override
_UnixPermEditorState createState() => _UnixPermEditorState(); State<UnixPermEditor> createState() => _UnixPermEditorState();
} }
final class _UnixPermEditorState extends State<UnixPermEditor> { final class _UnixPermEditorState extends State<UnixPermEditor> {