Compare commits

..

12 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
46a12bc844 bump: v1201 2025-07-28 22:27:56 +08:00
lollipopkit🏳️‍⚧️
8d597294a4 feat: amd gpu (#831) 2025-07-28 22:26:29 +08:00
lollipopkit🏳️‍⚧️
682a6e4f2d feat: custom pwd of bak (#827) 2025-07-25 16:38:28 +08:00
lollipopkit🏳️‍⚧️
8c3302cf0d chore: update script location in Attention notice (#825)
Fixes #824
2025-07-21 16:42:16 +08:00
lollipopkit🏳️‍⚧️
ec4bf3df24 opt.: sftp dl 2025-07-21 16:20:27 +08:00
lollipopkit🏳️‍⚧️
263d4eabb4 feat: store critical data in secure store (#821) 2025-07-17 18:26:34 +08:00
lollipopkit🏳️‍⚧️
c6439673b8 feat: shift key in ssh term (#819) 2025-07-17 18:18:18 +08:00
lollipopkit🏳️‍⚧️
a35d21981b opt.: watch sync mechanism (#817)
* opt.: watch sync mechanism
Fixes #816

* opt.
2025-07-17 16:55:56 +08:00
Tom
dbc873c0c0 feat: enhance server card layout and add logo display functionality (#804) 2025-06-27 18:55:48 +08:00
Integral
e69808a2f6 fix: disable APK signing block to resolve F-Droid build issues (#793)
Thanks for the patch from @linsui.
2025-06-16 01:51:30 +08:00
lollipopkit🏳️‍⚧️
55b3ba63ec opt.: ui 2025-06-12 22:04:03 +08:00
ИEØ_ΙΙØZ
006e66d825 update: app_zh_tw.arb (#790) 2025-06-11 17:07:22 +08:00
78 changed files with 2097 additions and 642 deletions

View File

@@ -92,6 +92,13 @@ android {
// No applicationIdSuffix or resValue here
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
}
flutter {

View File

@@ -14,7 +14,8 @@
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:hasFragileUserData="true"
android:restoreAnyVersion="true"
tools:targetApi="q">

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>

View File

@@ -8,6 +8,8 @@ PODS:
- Flutter (1.0.0)
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- icloud_storage (0.0.1):
- Flutter
- local_auth_darwin (0.0.1):
@@ -38,6 +40,7 @@ DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -60,6 +63,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
icloud_storage:
:path: ".symlinks/plugins/icloud_storage/ios"
local_auth_darwin:
@@ -87,6 +92,7 @@ SPEC CHECKSUMS:
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499

View File

@@ -672,7 +672,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -682,7 +682,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -808,7 +808,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -818,7 +818,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -836,7 +836,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -846,7 +846,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -867,7 +867,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -880,7 +880,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -906,7 +906,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -919,7 +919,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -942,7 +942,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -955,7 +955,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -978,7 +978,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -990,7 +990,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1019,7 +1019,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1031,7 +1031,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;
@@ -1057,7 +1057,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1069,7 +1069,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;

View File

@@ -14,13 +14,20 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
set {
Store.setCtx(newValue)
updateUrls(newValue)
// Notify the view to update, but the [urls] are already published
// so the view will automatically update when [urls] changes.
// DispatchQueue.main.async {
// self.objectWillChange.send()
// }
}
get {
return _ctx
}
}
var userInfo: [String: Any] = [:]
@Published var urls: [String] = []
override init() {
super.init()
if !WCSession.isSupported() {
@@ -29,24 +36,85 @@ class PhoneConnMgr: NSObject, WCSessionDelegate, ObservableObject {
session = WCSession.default
session?.delegate = self
session?.activate()
ctx = Store.getCtx()
_ctx = Store.getCtx()
updateUrls(_ctx)
}
func updateUrls(_ val: [String: Any]) {
if let urls = val["urls"] as? [String] {
self.urls = urls.filter { !$0.isEmpty }
DispatchQueue.main.async {
self.urls = urls.filter { !$0.isEmpty }
}
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
func session(
_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
// Request latest data when the session is activated
if activationState == .activated {
requestLatestData()
}
}
// implement session:didReceiveApplicationContext:
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
ctx = applicationContext
// Receive realtime msgs
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
DispatchQueue.main.async {
self.ctx = message
}
}
// Receive UserInfo
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
DispatchQueue.main.async {
self.ctx = userInfo
}
}
// Receive Application Context
func session(
_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]
) {
DispatchQueue.main.async {
self.ctx = applicationContext
}
}
private func requestLatestData(timeout: TimeInterval = 5.0, maxRetries: Int = 1) {
guard let session = session, session.isReachable else { return }
var didReceiveResponse = false
var retries = 0
func sendRequest() {
session.sendMessage(["action": "requestData"]) { response in
didReceiveResponse = true
DispatchQueue.main.async {
self.ctx = response
}
} errorHandler: { error in
print("Request data failed: \(error)")
// Optionally, handle error UI here
}
// Timeout handling
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in
guard let self = self else { return }
if !didReceiveResponse {
if retries < maxRetries {
retries += 1
print("No response, retrying requestLatestData (\(retries))...")
sendRequest()
} else {
print("Request data timed out after \(retries + 1) attempts.")
// Optionally, update UI to indicate timeout
}
}
}
}
sendRequest()
}
}

View File

@@ -58,7 +58,7 @@ Future<SSHClient> genClient(
Spi? jumpSpi,
/// Handle keyboard-interactive authentication
FutureOr<List<String>?> Function(SSHUserInfoRequest)? onKeyboardInteractive,
SSHUserInfoRequestHandler? onKeyboardInteractive,
}) async {
onStatus?.call(GenSSHClientStatus.socket);

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/app.dart';
@@ -13,7 +12,7 @@ abstract final class KeybordInteractive {
}) async {
try {
final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog(
title: l10n.pwd,
title: libL10n.pwd,
id: spi.id,
label: spi.id,
);

View File

@@ -74,15 +74,27 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
);
}
static Future<String> backup([String? name]) async {
static Future<String> backup([String? name, String? password]) async {
final bak = await BackupV2.loadFromStore();
final result = json.encode(bak.toJson());
var result = json.encode(bak.toJson());
if (password != null && password.isNotEmpty) {
result = Cryptor.encrypt(result, password);
}
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result);
return path;
}
factory BackupV2.fromJsonString(String jsonString) {
factory BackupV2.fromJsonString(String jsonString, [String? password]) {
if (Cryptor.isEncrypted(jsonString)) {
if (password == null || password.isEmpty) {
throw Exception('Backup is encrypted but no password provided');
}
jsonString = Cryptor.decrypt(jsonString, password);
}
final map = json.decode(jsonString) as Map<String, dynamic>;
return BackupV2.fromJson(map);
}

View File

@@ -0,0 +1,198 @@
import 'package:computer/computer.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
import 'package:server_box/data/model/app/bak/backup_source.dart';
import 'package:server_box/data/model/app/bak/utils.dart';
import 'package:server_box/data/res/store.dart';
/// Service class for handling backup operations
class BackupService {
/// Perform backup operation with the given source
static Future<void> backup(BuildContext context, BackupSource source) async {
final password = await _getBackupPassword(context);
if (password == null) return;
try {
final path = await BackupV2.backup(null, password.isEmpty ? null : password);
await source.saveContent(path);
// Show success message for clipboard source
if (source is ClipboardBackupSource) {
context.showSnackBar(libL10n.success);
}
} catch (e, s) {
context.showErrDialog(e, s, libL10n.backup);
}
}
/// Perform restore operation with the given source
static Future<void> restore(BuildContext context, BackupSource source) async {
final text = await source.getContent();
if (text == null) {
// Show empty message for clipboard source
if (source is ClipboardBackupSource) {
context.showSnackBar(libL10n.empty);
}
return;
}
await restoreFromText(context, text);
}
/// Handle password dialog for backup operations
static Future<String?> _getBackupPassword(BuildContext context) async {
final savedPassword = await Stores.setting.backupasswd.read();
String? password;
if (savedPassword != null && savedPassword.isNotEmpty) {
// Use saved password or ask for custom password
final useCustom = await context.showRoundDialog<bool>(
title: l10n.backupPassword,
child: Text(l10n.backupPasswordTip),
actions: [
Btn.cancel(),
TextButton(onPressed: () => context.pop(false), child: Text(l10n.backupPasswordSet)),
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.custom)),
],
);
if (useCustom == null) return null;
if (useCustom) {
password = await _showPasswordDialog(context, initial: savedPassword);
} else {
password = savedPassword;
}
} else {
// No saved password, ask if user wants to set one
password = await _showPasswordDialog(context);
}
return password;
}
/// Handle restore from text with decryption support
static Future<void> restoreFromText(BuildContext context, String text) async {
// Check if backup is encrypted
final isEncrypted = Cryptor.isEncrypted(text);
String? password;
if (!isEncrypted) {
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text),
);
if (err != null || backup == null) return;
await _confirmAndRestore(context, backup);
} catch (e, s) {
Loggers.app.warning('Import backup failed', e, s);
context.showErrDialog(e, s, libL10n.restore);
}
return;
}
// Try with saved password first
final savedPassword = await Stores.setting.backupasswd.read();
if (savedPassword != null && savedPassword.isNotEmpty) {
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (
text,
savedPassword,
)),
);
if (err == null && backup != null) {
await _confirmAndRestore(context, backup);
return;
}
} catch (e) {
// Saved password failed, will prompt for manual input
}
}
// Prompt for password with retry logic
while (true) {
password = await _showPasswordDialog(context, title: libL10n.pwd, hint: l10n.backupEncrypted);
if (password == null) return; // User cancelled
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), (
text,
password,
)),
);
if (err != null || backup == null) continue;
await _confirmAndRestore(context, backup);
return;
} catch (e) {
if (e.toString().contains('incorrect password') || e.toString().contains('Failed to decrypt')) {
final retry = await context.showRoundDialog<bool>(
title: l10n.backupPasswordWrong,
child: Text(l10n.backupPasswordWrong),
actions: [
TextButton(onPressed: () => context.pop(false), child: Text(libL10n.cancel)),
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.retry)),
],
);
if (retry != true) return;
continue; // Try again
} else {
// Other error, show and exit
context.showErrDialog(e, null, libL10n.restore);
return;
}
}
}
}
/// Confirm and execute restore operation
static Future<void> _confirmAndRestore(BuildContext context, (dynamic, String) backup) async {
await context.showRoundDialog(
title: libL10n.restore,
child: Text(libL10n.askContinue('${libL10n.restore} ${libL10n.backup}(${backup.$2})')),
actions: Btn.ok(
onTap: () async {
await backup.$1.merge(force: true);
context.pop();
},
).toList,
);
}
/// Show password input dialog
static Future<String?> _showPasswordDialog(
BuildContext context, {
String? initial,
String? title,
String? hint,
}) async {
final controller = TextEditingController(text: initial ?? '');
final result = await context.showRoundDialog<String>(
title: title ?? libL10n.pwd,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(hint ?? l10n.backupPasswordTip, style: UIs.textGrey),
UIs.height13,
Input(
label: l10n.backupPassword,
controller: controller,
obscureText: true,
onSubmitted: (_) => context.pop(controller.text),
),
],
),
actions: [
Btn.cancel(),
TextButton(onPressed: () => context.pop(controller.text), child: Text(libL10n.ok)),
],
);
controller.dispose();
return result;
}
}

View File

@@ -0,0 +1,62 @@
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
/// Abstract interface for backup content sources
abstract class BackupSource {
/// Get content from this source for restore
Future<String?> getContent();
/// Save content to this source for backup
Future<void> saveContent(String filePath);
/// Display name for this source
String get displayName;
/// Icon for this source
IconData get icon;
}
/// File-based backup source
class FileBackupSource implements BackupSource {
@override
Future<String?> getContent() async {
return await Pfs.pickFileString();
}
@override
Future<void> saveContent(String filePath) async {
await Pfs.sharePaths(paths: [filePath]);
}
@override
String get displayName => libL10n.file;
@override
IconData get icon => Icons.file_open;
}
/// Clipboard-based backup source
class ClipboardBackupSource implements BackupSource {
@override
Future<String?> getContent() async {
final text = await Pfs.paste();
if (text == null || text.isEmpty) {
return null;
}
return text.trim();
}
@override
Future<void> saveContent(String filePath) async {
final content = await File(filePath).readAsString();
Pfs.copy(content);
}
@override
String get displayName => libL10n.clipboard;
@override
IconData get icon => Icons.content_paste;
}

View File

@@ -3,9 +3,9 @@ import 'package:server_box/data/model/app/bak/backup.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
abstract final class MergeableUtils {
static (Mergeable, String) fromJsonString(String json) {
static (Mergeable, String) fromJsonString(String json, [String? password]) {
try {
final bak = BackupV2.fromJsonString(json);
final bak = BackupV2.fromJsonString(json, password);
return (bak, DateTime.fromMillisecondsSinceEpoch(bak.date).hms());
} catch (e) {
final bak = Backup.fromJsonString(json);

View File

@@ -16,6 +16,12 @@ enum ShellFunc {
/// The suffix `\t` is for formatting
static const cmdDivider = '\necho $seperator\n\t';
/// Cached Linux status commands string
static final _linuxStatusCmds = StatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
/// Cached BSD status commands string
static final _bsdStatusCmds = BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider);
/// srvboxm -> ServerBox Mobile
static const scriptFile = 'srvboxm_v${BuildData.script}.sh';
static const scriptDirHome = '~/.config/server_box';
@@ -28,13 +34,10 @@ enum ShellFunc {
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
/// it will be changed to [scriptDirHome]/[scriptFile].
static String getScriptDir(String id) {
final customScriptDir = ServerProvider.pick(
id: id,
)?.value.spi.custom?.scriptDir;
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) return customScriptDir;
return _scriptDirMap.putIfAbsent(id, () {
return scriptDirTmp;
});
_scriptDirMap[id] ??= scriptDirTmp;
return _scriptDirMap[id]!;
}
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
@@ -68,43 +71,24 @@ chmod 755 $scriptPath
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
String get name {
switch (this) {
case ShellFunc.status:
return 'status';
// case ShellFunc.docker:
// // `dockeR` -> avoid conflict with `docker` command
// return 'dockeR';
case ShellFunc.process:
return 'process';
case ShellFunc.shutdown:
return 'ShutDown';
case ShellFunc.reboot:
return 'Reboot';
case ShellFunc.suspend:
return 'Suspend';
}
}
String get name => switch (this) {
ShellFunc.status => 'status',
ShellFunc.process => 'process',
ShellFunc.shutdown => 'ShutDown',
ShellFunc.reboot => 'Reboot',
ShellFunc.suspend => 'Suspend',
};
String get _cmd {
switch (this) {
case ShellFunc.status:
return '''
String get _cmd => switch (this) {
ShellFunc.status =>
'''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\t${StatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
\t$_linuxStatusCmds
else
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
fi''';
// case ShellFunc.docker:
// return '''
// result=\$(docker version 2>&1 | grep "permission denied")
// if [ "\$result" != "" ]; then
// \t${_dockerCmds.join(_cmdDivider)}
// else
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
// fi''';
case ShellFunc.process:
return '''
\t$_bsdStatusCmds
fi''',
ShellFunc.process =>
'''
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
@@ -114,30 +98,29 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
else
\tps -ax
fi
''';
case ShellFunc.shutdown:
return '''
''',
ShellFunc.shutdown =>
'''
if [ "\$userId" = "0" ]; then
\tshutdown -h now
else
\tsudo -S shutdown -h now
fi''';
case ShellFunc.reboot:
return '''
fi''',
ShellFunc.reboot =>
'''
if [ "\$userId" = "0" ]; then
\treboot
else
\tsudo -S reboot
fi''';
case ShellFunc.suspend:
return '''
fi''',
ShellFunc.suspend =>
'''
if [ "\$userId" = "0" ]; then
\tsystemctl suspend
else
\tsudo -S systemctl suspend
fi''';
}
}
fi''',
};
static String allScript(Map<String, String>? customCmds) {
final sb = StringBuffer();
@@ -163,9 +146,7 @@ exec 2>/dev/null
// Write each func
for (final func in values) {
final customCmdsStr = () {
if (func == ShellFunc.status &&
customCmds != null &&
customCmds.isNotEmpty) {
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
}
return '';
@@ -212,18 +193,15 @@ enum StatusCmdType {
cpu._('cat /proc/stat | grep cpu'),
uptime._('uptime'),
conn._('cat /proc/net/snmp'),
disk._(
'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
),
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
host._('cat /etc/hostname'),
diskio._('cat /proc/diskstats'),
battery._(
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done',
),
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
nvidia._('nvidia-smi -q -x'),
amd._('if command -v amd-smi >/dev/null 2>&1; then amd-smi list --json && amd-smi metric --json; elif command -v rocm-smi >/dev/null 2>&1; then rocm-smi --json || rocm-smi --showunique --showuse --showtemp --showfan --showclocks --showmemuse --showpower; elif command -v radeontop >/dev/null 2>&1; then timeout 2s radeontop -d - -l 1 | tail -n +2; else echo "No AMD GPU monitoring tools found"; fi'),
sensors._('sensors'),
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
@@ -258,6 +236,8 @@ extension StatusCmdTypeX on StatusCmdType {
StatusCmdType.host => l10n.host,
StatusCmdType.uptime => l10n.uptime,
StatusCmdType.battery => l10n.battery,
StatusCmdType.sensors => l10n.sensors,
StatusCmdType.disk => l10n.disk,
final val => val.name,
};
}

View File

@@ -0,0 +1,188 @@
import 'dart:convert';
/// AMD GPU monitoring data structures
/// Supports both amd-smi and rocm-smi tools
/// Example JSON output:
/// [
/// {
/// "name": "AMD Radeon RX 7900 XTX",
/// "device_id": "0",
/// "temp": 45,
/// "power": "120W / 355W",
/// "memory": {
/// "total": 24576,
/// "used": 1024,
/// "unit": "MB",
/// "processes": [
/// {
/// "pid": 2456,
/// "name": "firefox",
/// "memory": 512
/// }
/// ]
/// },
/// "utilization": 75,
/// "fan_speed": 1200,
/// "clock_speed": 2400
/// }
/// ]
class AmdSmi {
static List<AmdSmiItem> fromJson(String raw) {
try {
final jsonData = json.decode(raw);
if (jsonData is! List) return [];
return jsonData
.map((gpu) => _parseGpuItem(gpu))
.where((item) => item != null)
.cast<AmdSmiItem>()
.toList();
} catch (e) {
return [];
}
}
static AmdSmiItem? _parseGpuItem(Map<String, dynamic> gpu) {
try {
final name = gpu['name'] ?? gpu['card_model'] ?? gpu['device_name'] ?? 'Unknown AMD GPU';
final deviceId = gpu['device_id']?.toString() ?? gpu['gpu_id']?.toString() ?? '0';
// Temperature parsing
final tempRaw = gpu['temperature'] ?? gpu['temp'] ?? gpu['gpu_temp'];
final temp = _parseIntValue(tempRaw);
// Power parsing
final powerDraw = gpu['power_draw'] ?? gpu['current_power'];
final powerCap = gpu['power_cap'] ?? gpu['power_limit'] ?? gpu['max_power'];
final power = _formatPower(powerDraw, powerCap);
// Memory parsing
final memory = _parseMemory(gpu['memory'] ?? gpu['vram'] ?? {});
// Utilization parsing
final utilization = _parseIntValue(gpu['utilization'] ?? gpu['gpu_util'] ?? gpu['activity']);
// Fan speed parsing
final fanSpeed = _parseIntValue(gpu['fan_speed'] ?? gpu['fan_rpm']);
// Clock speed parsing
final clockSpeed = _parseIntValue(gpu['clock_speed'] ?? gpu['gpu_clock'] ?? gpu['sclk']);
return AmdSmiItem(
deviceId: deviceId,
name: name,
temp: temp,
power: power,
memory: memory,
utilization: utilization,
fanSpeed: fanSpeed,
clockSpeed: clockSpeed,
);
} catch (e) {
return null;
}
}
static int _parseIntValue(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is String) {
// Remove units and parse (e.g., "45°C" -> 45, "1200 RPM" -> 1200)
final cleanValue = value.replaceAll(RegExp(r'[^\d]'), '');
return int.tryParse(cleanValue) ?? 0;
}
return 0;
}
static String _formatPower(dynamic draw, dynamic cap) {
final drawValue = _parseIntValue(draw);
final capValue = _parseIntValue(cap);
if (drawValue == 0 && capValue == 0) return 'N/A';
if (capValue == 0) return '${drawValue}W';
return '${drawValue}W / ${capValue}W';
}
static AmdSmiMem _parseMemory(Map<String, dynamic> memData) {
final total = _parseIntValue(memData['total'] ?? memData['total_memory']);
final used = _parseIntValue(memData['used'] ?? memData['used_memory']);
final unit = memData['unit']?.toString() ?? 'MB';
final processes = <AmdSmiMemProcess>[];
final processesData = memData['processes'];
if (processesData is List) {
for (final proc in processesData) {
if (proc is Map<String, dynamic>) {
final process = _parseProcess(proc);
if (process != null) processes.add(process);
}
}
}
return AmdSmiMem(total, used, unit, processes);
}
static AmdSmiMemProcess? _parseProcess(Map<String, dynamic> procData) {
final pid = _parseIntValue(procData['pid']);
final name = procData['name']?.toString() ?? procData['process_name']?.toString() ?? 'Unknown';
final memory = _parseIntValue(procData['memory'] ?? procData['used_memory']);
if (pid == 0) return null;
return AmdSmiMemProcess(pid, name, memory);
}
}
class AmdSmiItem {
final String deviceId;
final String name;
final int temp;
final String power;
final AmdSmiMem memory;
final int utilization;
final int fanSpeed;
final int clockSpeed;
const AmdSmiItem({
required this.deviceId,
required this.name,
required this.temp,
required this.power,
required this.memory,
required this.utilization,
required this.fanSpeed,
required this.clockSpeed,
});
@override
String toString() {
return 'AmdSmiItem{name: $name, temp: $temp, power: $power, utilization: $utilization%, memory: $memory}';
}
}
class AmdSmiMem {
final int total;
final int used;
final String unit;
final List<AmdSmiMemProcess> processes;
const AmdSmiMem(this.total, this.used, this.unit, this.processes);
@override
String toString() {
return 'AmdSmiMem{total: $total, used: $used, unit: $unit, processes: ${processes.length}}';
}
}
class AmdSmiMemProcess {
final int pid;
final String name;
final int memory;
const AmdSmiMemProcess(this.pid, this.name, this.memory);
@override
String toString() {
return 'AmdSmiMemProcess{pid: $pid, name: $name, memory: $memory}';
}
}

View File

@@ -1,6 +1,7 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
import 'package:server_box/data/model/server/battery.dart';
import 'package:server_box/data/model/server/conn.dart';
import 'package:server_box/data/model/server/cpu.dart';
@@ -42,6 +43,7 @@ class ServerStatus {
DiskIO diskIO;
List<DiskSmart> diskSmart;
List<NvidiaSmiItem>? nvidia;
List<AmdSmiItem>? amd;
final List<Battery> batteries = [];
final Map<StatusCmdType, String> more = {};
final List<SensorItem> sensors = [];

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
import 'package:server_box/data/model/server/battery.dart';
import 'package:server_box/data/model/server/conn.dart';
import 'package:server_box/data/model/server/cpu.dart';
@@ -143,6 +144,12 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
Loggers.app.warning(e, s);
}
try {
req.ss.amd = AmdSmi.fromJson(StatusCmdType.amd.find(segments));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
final battery = StatusCmdType.battery.find(segments);

View File

@@ -99,16 +99,21 @@ Future<void> _download(
mainSendPort.send(size);
mainSendPort.send(SftpWorkerStatus.loading);
// Read 2m each time
// Issue #161
// The download speed is about 2m/s may due to single core performance
const defaultChunkSize = 1024 * 1024 * 2;
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
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);
// Due to single core performance, limit the chunk size
const defaultChunkSize = 1024 * 1024 * 5;
var totalRead = 0;
while (totalRead < size) {
final remaining = size - totalRead;
final chunkSize = remaining > defaultChunkSize ? defaultChunkSize : remaining;
dprint('Size: $size, Total Read: $totalRead, Chunk Size: $chunkSize');
final fileData = file.read(offset: totalRead, length: chunkSize);
await for (var chunk in fileData) {
localFile.add(chunk);
totalRead += chunk.length;
mainSendPort.send(totalRead / size * 100);
}
}

View File

@@ -21,6 +21,7 @@ enum VirtKey {
right,
clipboard,
ime,
shift,
pgup,
pgdn,
slash,
@@ -105,6 +106,7 @@ extension VirtKeyX on VirtKey {
VirtKey.right,
VirtKey.clipboard,
VirtKey.ime,
VirtKey.shift,
];
/// Corresponding [TerminalKey]
@@ -119,6 +121,7 @@ extension VirtKeyX on VirtKey {
VirtKey.left => TerminalKey.arrowLeft,
VirtKey.down => TerminalKey.arrowDown,
VirtKey.right => TerminalKey.arrowRight,
VirtKey.shift => TerminalKey.shift,
VirtKey.pgup => TerminalKey.pageUp,
VirtKey.pgdn => TerminalKey.pageDown,
VirtKey.f1 => TerminalKey.f1,
@@ -161,7 +164,7 @@ extension VirtKeyX on VirtKey {
};
bool get toggleable => switch (this) {
VirtKey.alt || VirtKey.ctrl => true,
VirtKey.alt || VirtKey.ctrl || VirtKey.shift => true,
_ => false,
};

View File

@@ -23,6 +23,15 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
}
}
bool _shift = false;
bool get shift => _shift;
set shift(bool value) {
if (value != _shift) {
_shift = value;
notifyListeners();
}
}
void reset(TerminalKeyboardEvent e) {
if (e.ctrl) {
ctrl = false;
@@ -30,6 +39,9 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
if (e.alt) {
alt = false;
}
if (e.shift) {
shift = false;
}
notifyListeners();
}
@@ -38,6 +50,7 @@ class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
final e = event.copyWith(
ctrl: event.ctrl || ctrl,
alt: event.alt || alt,
shift: event.shift || shift,
);
if (Stores.setting.sshVirtualKeyAutoOff.fetch()) {
reset(e);

View File

@@ -3,6 +3,6 @@
abstract class BuildData {
static const String name = "ServerBox";
static const int build = 1189;
static const int script = 64;
static const int build = 1201;
static const int script = 65;
}

View File

@@ -118,6 +118,7 @@ abstract final class GithubIds {
'rhwong',
'AstroEngineeer',
'mochasweet',
'back-lacking',
};
}

View File

@@ -270,4 +270,7 @@ class SettingStore extends HiveStore {
/// Have notified user for notificaiton permission or not
late final noNotiPerm = propertyDefault('noNotiPerm', false);
/// The backup password
late final backupasswd = SecureProp('bakPasswd');
}

View File

@@ -188,7 +188,7 @@ abstract class AppLocalizations {
/// No description provided for @backupTip.
///
/// In en, this message translates to:
/// **'The exported data is weakly encrypted. \nPlease keep it safe.'**
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
String get backupTip;
/// No description provided for @backupVersionNotMatch.
@@ -197,6 +197,48 @@ abstract class AppLocalizations {
/// **'Backup version is not match.'**
String get backupVersionNotMatch;
/// No description provided for @backupPassword.
///
/// In en, this message translates to:
/// **'Backup password'**
String get backupPassword;
/// No description provided for @backupPasswordTip.
///
/// In en, this message translates to:
/// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**
String get backupPasswordTip;
/// No description provided for @backupPasswordWrong.
///
/// In en, this message translates to:
/// **'Incorrect backup password'**
String get backupPasswordWrong;
/// No description provided for @backupEncrypted.
///
/// In en, this message translates to:
/// **'Backup is encrypted'**
String get backupEncrypted;
/// No description provided for @backupNotEncrypted.
///
/// In en, this message translates to:
/// **'Backup is not encrypted'**
String get backupNotEncrypted;
/// No description provided for @backupPasswordSet.
///
/// In en, this message translates to:
/// **'Backup password set'**
String get backupPasswordSet;
/// No description provided for @backupPasswordRemoved.
///
/// In en, this message translates to:
/// **'Backup password removed'**
String get backupPasswordRemoved;
/// No description provided for @battery.
///
/// In en, this message translates to:
@@ -944,12 +986,6 @@ abstract class AppLocalizations {
/// **'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'**
String get pveVersionLow;
/// No description provided for @pwd.
///
/// In en, this message translates to:
/// **'Password'**
String get pwd;
/// No description provided for @read.
///
/// In en, this message translates to:
@@ -1505,7 +1541,7 @@ abstract class AppLocalizations {
/// No description provided for @writeScriptTip.
///
/// In en, this message translates to:
/// **'After connecting to the server, a script will be written to ~/.config/server_box to monitor the system status. You can review the script content.'**
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
String get writeScriptTip;
}

View File

@@ -47,12 +47,34 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get backupTip =>
'Das Backup wird nur einfach verschlüsselt.\nBitte bewahre die Datei sicher auf.';
'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.';
@override
String get backupVersionNotMatch =>
'Die Backup-Version stimmt nicht überein.';
@override
String get backupPassword => 'Backup-Passwort';
@override
String get backupPasswordTip =>
'Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.';
@override
String get backupPasswordWrong => 'Falsches Backup-Passwort';
@override
String get backupEncrypted => 'Backup ist verschlüsselt';
@override
String get backupNotEncrypted => 'Backup ist nicht verschlüsselt';
@override
String get backupPasswordSet => 'Backup-Passwort gesetzt';
@override
String get backupPasswordRemoved => 'Backup-Passwort entfernt';
@override
String get battery => 'Batterie';
@@ -470,9 +492,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get pveVersionLow =>
'Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.';
@override
String get pwd => 'Passwort';
@override
String get read => 'Lesen';
@@ -775,5 +794,5 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get writeScriptTip =>
'Nach der Verbindung mit dem Server wird ein Skript in ~/.config/server_box geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
}

View File

@@ -47,11 +47,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get backupTip =>
'The exported data is weakly encrypted. \nPlease keep it safe.';
'The exported data can be encrypted with password. \nPlease keep it safe.';
@override
String get backupVersionNotMatch => 'Backup version is not match.';
@override
String get backupPassword => 'Backup password';
@override
String get backupPasswordTip =>
'Set a password to encrypt backup files. Leave empty to disable encryption.';
@override
String get backupPasswordWrong => 'Incorrect backup password';
@override
String get backupEncrypted => 'Backup is encrypted';
@override
String get backupNotEncrypted => 'Backup is not encrypted';
@override
String get backupPasswordSet => 'Backup password set';
@override
String get backupPasswordRemoved => 'Backup password removed';
@override
String get battery => 'Battery';
@@ -468,9 +490,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get pveVersionLow =>
'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.';
@override
String get pwd => 'Password';
@override
String get read => 'Read';
@@ -769,5 +788,5 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get writeScriptTip =>
'After connecting to the server, a script will be written to ~/.config/server_box to monitor the system status. You can review the script content.';
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
}

View File

@@ -47,12 +47,34 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get backupTip =>
'Los datos exportados solo están encriptados de manera básica, por favor guárdalos en un lugar seguro.';
'Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.';
@override
String get backupVersionNotMatch =>
'La versión de la copia de seguridad no coincide, no se puede restaurar';
@override
String get backupPassword => 'Contraseña de respaldo';
@override
String get backupPasswordTip =>
'Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.';
@override
String get backupPasswordWrong => 'Contraseña de respaldo incorrecta';
@override
String get backupEncrypted => 'El respaldo está encriptado';
@override
String get backupNotEncrypted => 'El respaldo no está encriptado';
@override
String get backupPasswordSet => 'Contraseña de respaldo establecida';
@override
String get backupPasswordRemoved => 'Contraseña de respaldo eliminada';
@override
String get battery => 'Batería';
@@ -472,9 +494,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get pveVersionLow =>
'Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.';
@override
String get pwd => 'Contraseña';
@override
String get read => 'Leer';
@@ -777,5 +796,5 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get writeScriptTip =>
'Después de conectarse al servidor, se escribirá un script en ~/.config/server_box para monitorear el estado del sistema. Puedes revisar el contenido del script.';
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
}

View File

@@ -47,12 +47,34 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get backupTip =>
'Les données exportées sont simplement chiffrées. \nVeuillez les garder en sécurité.';
'Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.';
@override
String get backupVersionNotMatch =>
'La version de sauvegarde ne correspond pas.';
@override
String get backupPassword => 'Mot de passe de sauvegarde';
@override
String get backupPasswordTip =>
'Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.';
@override
String get backupPasswordWrong => 'Mot de passe de sauvegarde incorrect';
@override
String get backupEncrypted => 'La sauvegarde est chiffrée';
@override
String get backupNotEncrypted => 'La sauvegarde n\'est pas chiffrée';
@override
String get backupPasswordSet => 'Mot de passe de sauvegarde défini';
@override
String get backupPasswordRemoved => 'Mot de passe de sauvegarde supprimé';
@override
String get battery => 'Batterie';
@@ -473,9 +495,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get pveVersionLow =>
'Cette fonctionnalité est actuellement en phase de test et n\'a été testée que sur PVE 8+. Veuillez l\'utiliser avec prudence.';
@override
String get pwd => 'Mot de passe';
@override
String get read => 'Lire';
@@ -779,5 +798,5 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get writeScriptTip =>
'Après la connexion au serveur, un script sera écrit dans ~/.config/server_box pour surveiller létat du système. Vous pouvez examiner le contenu du script.';
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller létat du système. Vous pouvez examiner le contenu du script.';
}

View File

@@ -47,11 +47,33 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get backupTip =>
'Data yang diekspor hanya dienkripsi.\nTolong jaga keamanannya.';
'Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.';
@override
String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';
@override
String get backupPassword => 'Kata sandi cadangan';
@override
String get backupPasswordTip =>
'Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.';
@override
String get backupPasswordWrong => 'Kata sandi cadangan salah';
@override
String get backupEncrypted => 'Cadangan telah dienkripsi';
@override
String get backupNotEncrypted => 'Cadangan tidak dienkripsi';
@override
String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';
@override
String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';
@override
String get battery => 'Baterai';
@@ -468,9 +490,6 @@ class AppLocalizationsId extends AppLocalizations {
String get pveVersionLow =>
'Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.';
@override
String get pwd => 'Kata sandi';
@override
String get read => 'Baca';
@@ -768,5 +787,5 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get writeScriptTip =>
'Setelah terhubung ke server, sebuah skrip akan ditulis ke ~/.config/server_box untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
}

View File

@@ -43,11 +43,33 @@ class AppLocalizationsJa extends AppLocalizations {
String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新';
@override
String get backupTip => 'エクスポートされたデータは簡単に暗号化されています。適切に保管してください。';
String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。';
@override
String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';
@override
String get backupPassword => 'バックアップパスワード';
@override
String get backupPasswordTip =>
'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。';
@override
String get backupPasswordWrong => 'バックアップパスワードが間違っています';
@override
String get backupEncrypted => 'バックアップは暗号化されています';
@override
String get backupNotEncrypted => 'バックアップは暗号化されていません';
@override
String get backupPasswordSet => 'バックアップパスワードが設定されました';
@override
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
@override
String get battery => 'バッテリー';
@@ -453,9 +475,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get pveVersionLow => 'この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。';
@override
String get pwd => 'パスワード';
@override
String get read => '読み取り';
@@ -748,5 +767,5 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get writeScriptTip =>
'サーバーに接続すると、システムの状態を監視するためのスクリプトが ~/.config/server_box に書き込まれます。スクリプトの内容を確認できます。';
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
}

View File

@@ -47,11 +47,33 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get backupTip =>
'De geëxporteerde gegevens zijn simpelweg versleuteld. \nBewaar deze aub veilig.';
'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.';
@override
String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';
@override
String get backupPassword => 'Back-up wachtwoord';
@override
String get backupPasswordTip =>
'Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.';
@override
String get backupPasswordWrong => 'Onjuist back-up wachtwoord';
@override
String get backupEncrypted => 'Back-up is versleuteld';
@override
String get backupNotEncrypted => 'Back-up is niet versleuteld';
@override
String get backupPasswordSet => 'Back-up wachtwoord ingesteld';
@override
String get backupPasswordRemoved => 'Back-up wachtwoord verwijderd';
@override
String get battery => 'Batterij';
@@ -469,9 +491,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get pveVersionLow =>
'Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.';
@override
String get pwd => 'Wachtwoord';
@override
String get read => 'Lezen';
@@ -774,5 +793,5 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get writeScriptTip =>
'Na het verbinden met de server wordt een script geschreven naar ~/.config/server_box om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
}

View File

@@ -47,12 +47,34 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get backupTip =>
'Os dados exportados são criptografados de forma simples, por favor, guarde-os com segurança.';
'Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.';
@override
String get backupVersionNotMatch =>
'Versão de backup não compatível, não é possível restaurar';
@override
String get backupPassword => 'Senha de backup';
@override
String get backupPasswordTip =>
'Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.';
@override
String get backupPasswordWrong => 'Senha de backup incorreta';
@override
String get backupEncrypted => 'Backup está criptografado';
@override
String get backupNotEncrypted => 'Backup não está criptografado';
@override
String get backupPasswordSet => 'Senha de backup definida';
@override
String get backupPasswordRemoved => 'Senha de backup removida';
@override
String get battery => 'Bateria';
@@ -469,9 +491,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get pveVersionLow =>
'Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.';
@override
String get pwd => 'Senha';
@override
String get read => 'Leitura';
@@ -771,5 +790,5 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get writeScriptTip =>
'Após conectar ao servidor, um script será escrito em ~/.config/server_box para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
}

View File

@@ -47,12 +47,34 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get backupTip =>
'Экспортированные данные зашифрованы простым способом \nПожалуйста, храните их в безопасности.';
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
@override
String get backupVersionNotMatch =>
'Версия резервной копии не совпадает, восстановление невозможно';
@override
String get backupPassword => 'Пароль резервной копии';
@override
String get backupPasswordTip =>
'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.';
@override
String get backupPasswordWrong => 'Неверный пароль резервной копии';
@override
String get backupEncrypted => 'Резервная копия зашифрована';
@override
String get backupNotEncrypted => 'Резервная копия не зашифрована';
@override
String get backupPasswordSet => 'Пароль резервной копии установлен';
@override
String get backupPasswordRemoved => 'Пароль резервной копии удален';
@override
String get battery => 'Батарея';
@@ -470,9 +492,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get pveVersionLow =>
'Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.';
@override
String get pwd => 'Пароль';
@override
String get read => 'Чтение';
@@ -774,5 +793,5 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get writeScriptTip =>
'После подключения к серверу скрипт будет записан в ~/.config/server_box для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
}

View File

@@ -46,11 +46,33 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get backupTip =>
'Dışa aktarılan veriler zayıf bir şekilde şifrelenmiştir. \nLütfen güvenli bir şekilde saklayın.';
'Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.';
@override
String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';
@override
String get backupPassword => 'Yedekleme parolası';
@override
String get backupPasswordTip =>
'Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.';
@override
String get backupPasswordWrong => 'Yanlış yedekleme parolası';
@override
String get backupEncrypted => 'Yedekleme şifrelenmiş';
@override
String get backupNotEncrypted => 'Yedekleme şifreli değil';
@override
String get backupPasswordSet => 'Yedekleme parolası ayarlandı';
@override
String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';
@override
String get battery => 'Pil';
@@ -467,9 +489,6 @@ class AppLocalizationsTr extends AppLocalizations {
String get pveVersionLow =>
'Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.';
@override
String get pwd => 'Şifre';
@override
String get read => 'Oku';
@@ -769,5 +788,5 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get writeScriptTip =>
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için ~/.config/server_box dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
}

View File

@@ -47,12 +47,34 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get backupTip =>
'Експортовані дані слабо зашифровані. \nБудь ласка, зберігайте їх у безпеці.';
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
@override
String get backupVersionNotMatch =>
'Версія резервного копіювання не збіглася.';
@override
String get backupPassword => 'Пароль резервного копіювання';
@override
String get backupPasswordTip =>
'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.';
@override
String get backupPasswordWrong => 'Неправильний пароль резервного копіювання';
@override
String get backupEncrypted => 'Резервна копія зашифрована';
@override
String get backupNotEncrypted => 'Резервна копія не зашифрована';
@override
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
@override
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
@override
String get battery => 'Акумулятор';
@@ -472,9 +494,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get pveVersionLow =>
'Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.';
@override
String get pwd => 'Пароль';
@override
String get read => 'Читати';
@@ -775,5 +794,5 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get writeScriptTip =>
'Після підключення до сервера скрипт буде записано у ~/.config/server_box для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
}

View File

@@ -42,11 +42,32 @@ class AppLocalizationsZh extends AppLocalizations {
String get autoUpdateHomeWidget => '自动更新桌面小部件';
@override
String get backupTip => '导出的数据仅进行了简单加密,请妥善保管。';
String get backupTip => '导出的数据可以使用密码加密,请妥善保管。';
@override
String get backupVersionNotMatch => '备份版本不匹配,无法恢复';
@override
String get backupPassword => '备份密码';
@override
String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';
@override
String get backupPasswordWrong => '备份密码错误';
@override
String get backupEncrypted => '备份已加密';
@override
String get backupNotEncrypted => '备份未加密';
@override
String get backupPasswordSet => '备份密码已设置';
@override
String get backupPasswordRemoved => '备份密码已移除';
@override
String get battery => '电池';
@@ -446,9 +467,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用';
@override
String get pwd => '密码';
@override
String get read => '';
@@ -735,7 +753,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get writeScriptTip =>
'在连接服务器后,会向 ~/.config/server_box 写入脚本来监测系统状态,你可以审查脚本内容。';
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
}
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
@@ -749,10 +767,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get acceptBeta => '接受測試版更新推送';
@override
String get addSystemPrivateKeyTip => '前沒有任何私鑰,是否添加系統自帶的 (~/.ssh/id_rsa)';
String get addSystemPrivateKeyTip => '前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)';
@override
String get added2List => '添加至任務列表';
String get added2List => '新增至任務清單';
@override
String get addr => '位址';
@@ -761,54 +779,75 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get alreadyLastDir => '已經是最上層目錄了';
@override
String get authFailTip => '認證失敗,請檢查密碼/鑰/主機/用戶等是否錯誤。';
String get authFailTip => '認證失敗,請檢查密碼/鑰/主機/使用者等是否錯誤。';
@override
String get autoBackupConflict => '只能同時開啓一個自動備份';
@override
String get autoConnect => '自動連';
String get autoConnect => '自動連';
@override
String get autoRun => '自動';
String get autoRun => '自動';
@override
String get autoUpdateHomeWidget => '自動更新桌面小部件';
String get autoUpdateHomeWidget => '自動更新桌面小工具';
@override
String get backupTip => '匯出的資料僅進行了簡單加密,請妥善保管。';
String get backupTip => '匯出的資料可以使用密碼加密。 \n請妥善保管。';
@override
String get backupVersionNotMatch => '備份版本不匹配,無法還原';
String get backupVersionNotMatch => '備份版本不相符,無法還原';
@override
String get backupPassword => '備份密碼';
@override
String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';
@override
String get backupPasswordWrong => '備份密碼錯誤';
@override
String get backupEncrypted => '備份已加密';
@override
String get backupNotEncrypted => '備份未加密';
@override
String get backupPasswordSet => '備份密碼已設定';
@override
String get backupPasswordRemoved => '備份密碼已移除';
@override
String get battery => '電池';
@override
String get bgRun => '後台運';
String get bgRun => '背景執';
@override
String get bgRunTip =>
'此開關只代表程式會嘗試在後台運行,具體能否在後臺行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池化”MIUI / HyperOS 請修改省電策略為“無限制”。';
'此開關只代表程式會嘗試在背景執行,具體能否在後臺行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”MIUI / HyperOS 請修改省電策略為“無限制”。';
@override
String get closeAfterSave => '儲存後關閉';
@override
String get cmd => '';
String get cmd => '';
@override
String get collapseUITip => '是否預設折疊 UI 中存在的長列表';
@override
String get conn => '';
String get conn => '';
@override
String get container => '容器';
@override
String get containerTrySudoTip =>
'例如App 內設使用者為 aaa但是 Docker 安裝在 root 使用者,這時就需要開啟此選項';
'例如App 內設使用者為 aaa但是 Docker 安裝在 root 使用者,這時就需要開啟此選項';
@override
String get convert => '轉換';
@@ -823,14 +862,14 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get cursorType => '游標類型';
@override
String get customCmd => '自訂';
String get customCmd => '自訂';
@override
String get customCmdDocUrl =>
'https://github.com/lollipopkit/flutter_server_box/wiki/主页#自定义';
'https://github.com/lollipopkit/flutter_server_box/wiki/主页#自定义';
@override
String get customCmdHint => '\"令名稱\": \"\"';
String get customCmdHint => '\"令名稱\": \"\"';
@override
String get decode => '解碼';
@@ -839,7 +878,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get decompress => '解壓縮';
@override
String get deleteServers => '量刪除伺服器';
String get deleteServers => '量刪除伺服器';
@override
String get desktopTerminalTip => '啟動 SSH 連線時用於打開終端機模擬器的指令。';
@@ -848,7 +887,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get dirEmpty => '請確保資料夾為空';
@override
String get disconnected => '接斷開';
String get disconnected => '線中斷';
@override
String get disk => '磁碟';
@@ -869,11 +908,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get dockerEmptyRunningItems =>
'沒有正在行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變 DOCKER_HOST 沒有被正確讀取。你可以通過在終端內運行 `echo \$DOCKER_HOST` 來獲取。';
'沒有正在行的容器。\n這可能是因為:\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo \$DOCKER_HOST` 來獲取。';
@override
String dockerImagesFmt(Object count) {
return '$count鏡像';
return '$count映像檔';
}
@override
@@ -884,19 +923,19 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
Object runningCount,
Object stoppedCount,
) {
return '$runningCount 個正在行, $stoppedCount 個已停止';
return '$runningCount 個正在行, $stoppedCount 個已停止';
}
@override
String dockerStatusRunningFmt(Object count) {
return '$count 個容器正在';
return '$count 個容器正在';
}
@override
String get doubleColumnMode => '雙列模式';
@override
String get doubleColumnTip => '此選項僅開啟功能,實際是否能開啟還取決於設備的寬';
String get doubleColumnTip => '此選項僅開啟功能,實際是否能開啟還取決於設備的';
@override
String get editVirtKeys => '編輯虛擬按鍵';
@@ -905,7 +944,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get editor => '編輯器';
@override
String get editorHighlightTip => '目前的代碼高亮性能較為糟糕,可以選擇關閉以改善。';
String get editorHighlightTip => '目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。';
@override
String get emulator => '模擬器';
@@ -914,7 +953,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get encode => '編碼';
@override
String get envVars => '環境變';
String get envVars => '環境變';
@override
String get experimentalFeature => '實驗性功能';
@@ -926,18 +965,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get fallbackSshDest => '備選 SSH 目標';
@override
String get fdroidReleaseTip => '如果你是從 F-Droid 下載的本應用,推薦關閉此選項';
String get fdroidReleaseTip => '如果你是從 F-Droid 下載的本App,推薦關閉此選項';
@override
String get fgService => '前台服務';
@override
String get fgServiceTip =>
'開啟後,可能會導致部分機型閃退。關閉可能導致部分機型無法後台保持 SSH 連。請在系統設內允許 ServerBox 通知權限、後台運行、自我喚醒。';
'開啟後,可能會導致部分機型閃退。關閉可能導致部分機型無法背景保持 SSH 連。請在系統設內允許 ServerBox 通知權限、背景執行、自我喚醒。';
@override
String fileTooLarge(Object file, Object size, Object sizeMax) {
return '文件 \'$file\' 過大 \'$size\',超過了 $sizeMax';
return '檔案 \'$file\' 過大 \'$size\',超過了 $sizeMax';
}
@override
@@ -959,10 +998,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get fullScreenJitter => '全螢幕模式抖動';
@override
String get fullScreenJitterHelp => '防止燒屏';
String get fullScreenJitterHelp => '防止烙印';
@override
String get fullScreenTip => '當設備旋轉為橫時,是否開啟全幕模式?此選項僅適用於伺服器選項卡';
String get fullScreenTip => '當設備旋轉為橫時,是否開啟全幕模式?此選項僅適用於伺服器分頁';
@override
String get goBackQ => '返回?';
@@ -974,10 +1013,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get hideTitleBar => '隱藏標題欄';
@override
String get highlight => '代碼高亮';
String get highlight => '程式碼標記';
@override
String get homeWidgetUrlConfig => '桌面部件鏈接配置';
String get homeWidgetUrlConfig => '桌面小工具連結配置';
@override
String get host => '主機';
@@ -988,13 +1027,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
}
@override
String get ignoreCert => '忽略證';
String get ignoreCert => '忽略';
@override
String get image => '鏡像';
String get image => '映像檔';
@override
String get imagesList => '鏡像列表';
String get imagesList => '映像檔列表';
@override
String get init => '初始化';
@@ -1016,7 +1055,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get jumpServer => '跳板伺服器';
@override
String get keepForeground => '請保持應用處於前';
String get keepForeground => '請保持App處於前';
@override
String get keepStatusWhenErr => '保留上次的伺服器狀態';
@@ -1025,22 +1064,22 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get keepStatusWhenErrTip => '僅在執行腳本出錯時';
@override
String get keyAuth => '鑰認證';
String get keyAuth => '鑰認證';
@override
String get letterCache => '入法字符緩存';
String get letterCache => '入法字符快取';
@override
String get letterCacheTip => '建議關閉,但關閉後將無法輸入 CJK 等文字。';
@override
String get license => '';
String get license => '';
@override
String get location => '位置';
@override
String get loss => '丟包率';
String get loss => '逾時';
@override
String madeWithLove(Object myGithub) {
@@ -1077,16 +1116,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get needHomeDir =>
'如果你是群暉用戶[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統用戶,需搜如何建家目錄home directory';
'如果你是群暉使用者[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統使用者,需搜如何建家目錄home directory';
@override
String get needRestart => '需要重 App';
String get needRestart => '需要重 App';
@override
String get net => '網路';
@override
String get netViewType => '網路視類型';
String get netViewType => '網路視類型';
@override
String get newContainer => '新建容器';
@@ -1113,7 +1152,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get onServerDetailPage => '在伺服器詳情頁';
@override
String get onlyOneLine => '僅顯示為一行(可動)';
String get onlyOneLine => '僅顯示為一行(可動)';
@override
String get onlyWhenCoreBiggerThan8 => '僅當核心數大於 8 時生效';
@@ -1125,7 +1164,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get openLastPathTip => '不同的伺服器會有不同的記錄,且記錄的是退出時的路徑';
@override
String get parseContainerStatsTip => 'Docker 解析佔用狀態較為緩慢';
String get parseContainerStatsTip => 'Docker 解析消耗狀態較為緩慢';
@override
String percentOfSize(Object percent, Object size) {
@@ -1145,7 +1184,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get pingNoServer => '沒有伺服器可用於 Ping\n請在伺服器 Tab 新增伺服器後再試';
@override
String get pkg => '管理';
String get pkg => '套件管理';
@override
String get plugInType => '插入類型';
@@ -1163,7 +1202,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get privateKey => '私鑰';
@override
String get process => '行程';
String get process => '處理程序';
@override
String get prune => '修剪';
@@ -1172,7 +1211,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get pushToken => '消息推送 Token';
@override
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的默認證書,則需要啟用此選項。';
String get pveIgnoreCertTip => '不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。';
@override
String get pveLoginFailed => '登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。';
@@ -1181,13 +1220,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。';
@override
String get pwd => '密碼';
String get read => '讀取';
@override
String get read => '';
@override
String get reboot => '重启';
String get reboot => '重開';
@override
String get rememberPwdInMem => '在記憶體中記住密碼';
@@ -1196,13 +1232,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get rememberPwdInMemTip => '用於容器、暫停等';
@override
String get rememberWindowSize => '記住窗大小';
String get rememberWindowSize => '記住窗大小';
@override
String get remotePath => '遠端路徑';
@override
String get restart => '';
String get restart => '';
@override
String get result => '結果';
@@ -1214,7 +1250,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get route => '路由';
@override
String get run => '';
String get run => '';
@override
String get running => '運作中';
@@ -1223,16 +1259,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get sameIdServerExist => '已存在相同 ID 的伺服器';
@override
String get save => '';
String get save => '';
@override
String get saved => '';
String get saved => '';
@override
String get second => '';
@override
String get sensors => '';
String get sensors => '';
@override
String get sequence => '順序';
@@ -1250,17 +1286,17 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get serverOrder => '伺服器順序';
@override
String get sftpDlPrepare => '準備連至伺服器...';
String get sftpDlPrepare => '準備連至伺服器...';
@override
String get sftpEditorTip =>
'如果為空, 使用App內置的文件編輯器。如果有值, 則使用遠伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。';
'如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。';
@override
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除文件';
String get sftpRmrDirSummary => '在 SFTP 中使用 `rm -r` 來刪除檔案';
@override
String get sftpSSHConnected => 'SFTP 已連...';
String get sftpSSHConnected => 'SFTP 已連...';
@override
String get sftpShowFoldersFirst => '資料夾顯示在前';
@@ -1296,11 +1332,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get sshTermHelp =>
'在終端可滾動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。文件圖標會打開前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端。代碼圖會貼上碼片段到終端並執行。';
'在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖會貼上程式碼片段到終端並執行。';
@override
String sshTip(Object url) {
return '該功能目前處於測試階段。\n\n請在 $url 饋問題,或者加入我們開發。';
return '該功能目前處於測試階段。\n\n請在 $url 饋問題,或者加入我們開發。';
}
@override
@@ -1328,10 +1364,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get supportFmtArgs => '支援以下格式化參數:';
@override
String get suspend => '挂起';
String get suspend => '當機';
@override
String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支';
String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支';
@override
String switchTo(Object val) {
@@ -1348,13 +1384,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get system => '系統';
@override
String get tag => '标签';
String get tag => '標籤';
@override
String get temperature => '溫度';
@override
String get termFontSizeTip => '此設將影響終端大小(寬度和高度)。您可以在終端頁面縮放,來調整前會話的字型大小。';
String get termFontSizeTip => '此設將影響終端大小(寬度和高度)。您可以在終端頁面縮放,來調整前會話的字型大小。';
@override
String get terminal => '终端機';
@@ -1399,7 +1435,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get update => '更新';
@override
String get updateIntervalEqual0 => '你設為 0伺服器狀態不會自動更新。\n且不能計算CPU使用情況。';
String get updateIntervalEqual0 => '你設為 0伺服器狀態不會自動更新。\n且不能計算CPU使用情況。';
@override
String get updateServerStatusInterval => '伺服器狀態更新間隔';
@@ -1411,40 +1447,40 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get upsideDown => '上下交換';
@override
String get uptime => '啟動時長';
String get uptime => '運作時間';
@override
String get useCdn => '使用 CDN';
@override
String get useCdnTip => '非中國大陸用戶建議使用 CDN是否使用';
String get useCdnTip => '非中國使用者建議使用 CDN是否使用';
@override
String get useNoPwd => '使用無密碼';
String get useNoPwd => '使用無密碼';
@override
String get usePodmanByDefault => '默認使用 Podman';
String get usePodmanByDefault => '預設使用 Podman';
@override
String get used => '已用';
String get used => '使';
@override
String get view => '';
String get view => '';
@override
String get viewErr => '查看錯誤';
@override
String get virtKeyHelpClipboard => '如果終端有選中字元,則復製選中字元至剪貼簿,否則貼剪貼簿內容至終端。';
String get virtKeyHelpClipboard => '如果終端有選中字元,則復製選中字元至剪貼簿,否則貼剪貼簿內容至終端';
@override
String get virtKeyHelpIME => '打開/關閉鍵盤';
@override
String get virtKeyHelpSFTP => '在 SFTP 中打開前路徑。';
String get virtKeyHelpSFTP => '在 SFTP 中打開前路徑。';
@override
String get waitConnection => '請等待連建立';
String get waitConnection => '請等待連建立';
@override
String get wakeLock => '保持喚醒';
@@ -1453,21 +1489,21 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get watchNotPaired => '沒有已配對的 Apple Watch';
@override
String get webdavSettingEmpty => 'WebDav 設項爲空';
String get webdavSettingEmpty => 'WebDav 設項爲空';
@override
String get whenOpenApp => '當打開 App 時';
@override
String get wolTip => '在配置 WOL網絡喚醒每次連伺服器都會先發送一次 WOL 請求。';
String get wolTip => '在配置 WOL網絡喚醒每次連伺服器都會先發送一次 WOL 請求。';
@override
String get write => '';
String get write => '寫入';
@override
String get writeScriptFailTip => '寫入腳本失敗,可能是沒有權限/目錄不存在等。';
@override
String get writeScriptTip =>
'到伺服器後,將會在 ~/.config/server_box 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
'到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
}

View File

@@ -254,6 +254,8 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
return VirtKey.f11;
case 43:
return VirtKey.f12;
case 44:
return VirtKey.shift;
default:
return VirtKey.esc;
}
@@ -350,6 +352,8 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
writer.writeByte(42);
case VirtKey.f12:
writer.writeByte(43);
case VirtKey.shift:
writer.writeByte(44);
}
}

View File

@@ -59,7 +59,7 @@ types:
index: 13
VirtKey:
typeId: 4
nextIndex: 44
nextIndex: 45
fields:
esc:
index: 0
@@ -149,6 +149,8 @@ types:
index: 42
f12:
index: 43
shift:
index: 44
NetViewType:
typeId: 5
nextIndex: 3

View File

@@ -11,8 +11,15 @@
"autoConnect": "Automatisch verbinden",
"autoRun": "Automatischer Start",
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
"backupTip": "Das Backup wird nur einfach verschlüsselt.\nBitte bewahre die Datei sicher auf.",
"backupTip": "Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.",
"backupVersionNotMatch": "Die Backup-Version stimmt nicht überein.",
"backupPassword": "Backup-Passwort",
"backupPasswordTip": "Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.",
"backupPasswordWrong": "Falsches Backup-Passwort",
"backupEncrypted": "Backup ist verschlüsselt",
"backupNotEncrypted": "Backup ist nicht verschlüsselt",
"backupPasswordSet": "Backup-Passwort gesetzt",
"backupPasswordRemoved": "Backup-Passwort entfernt",
"battery": "Batterie",
"bgRun": "Hintergrundaktualisierung",
"bgRunTip": "Dieser Schalter bedeutet nur, dass die App versuchen wird, im Hintergrund zu laufen. Ob sie im Hintergrund laufen kann, hängt davon ab, ob die Berechtigungen aktiviert sind oder nicht. Bei nativem Android deaktivieren Sie bitte \"Batterieoptimierung\" in dieser App, und bei miui ändern Sie bitte die Energiesparrichtlinie auf \"Unbegrenzt\".",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Nicht empfohlen, Achten Sie auf Sicherheitsrisiken! Wenn Sie das Standardzertifikat von PVE verwenden, müssen Sie diese Option aktivieren.",
"pveLoginFailed": "Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.",
"pveVersionLow": "Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.",
"pwd": "Passwort",
"read": "Lesen",
"reboot": "Neustart",
"rememberPwdInMem": "Passwort im Speicher behalten",
@@ -230,5 +236,5 @@
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
"write": "Schreiben",
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in ~/.config/server_box geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Auto connect",
"autoRun": "Auto run",
"autoUpdateHomeWidget": "Automatic home widget update",
"backupTip": "The exported data is weakly encrypted. \nPlease keep it safe.",
"backupTip": "The exported data can be encrypted with password. \nPlease keep it safe.",
"backupVersionNotMatch": "Backup version is not match.",
"backupPassword": "Backup password",
"backupPasswordTip": "Set a password to encrypt backup files. Leave empty to disable encryption.",
"backupPasswordWrong": "Incorrect backup password",
"backupEncrypted": "Backup is encrypted",
"backupNotEncrypted": "Backup is not encrypted",
"backupPasswordSet": "Backup password set",
"backupPasswordRemoved": "Backup password removed",
"battery": "Battery",
"bgRun": "Run in background",
"bgRunTip": "This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \"Battery Optimization\" in this app. For MIUI / HyperOS, please change the power saving policy to \"Unlimited\".",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.",
"pveLoginFailed": "Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.",
"pveVersionLow": "This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.",
"pwd": "Password",
"read": "Read",
"reboot": "Reboot",
"rememberPwdInMem": "Remember password in memory",
@@ -230,5 +236,5 @@
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
"write": "Write",
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
"writeScriptTip": "After connecting to the server, a script will be written to ~/.config/server_box to monitor the system status. You can review the script content."
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Conexión automática",
"autoRun": "Ejecución automática",
"autoUpdateHomeWidget": "Actualizar automáticamente el widget del escritorio",
"backupTip": "Los datos exportados solo están encriptados de manera básica, por favor guárdalos en un lugar seguro.",
"backupTip": "Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.",
"backupVersionNotMatch": "La versión de la copia de seguridad no coincide, no se puede restaurar",
"backupPassword": "Contraseña de respaldo",
"backupPasswordTip": "Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.",
"backupPasswordWrong": "Contraseña de respaldo incorrecta",
"backupEncrypted": "El respaldo está encriptado",
"backupNotEncrypted": "El respaldo no está encriptado",
"backupPasswordSet": "Contraseña de respaldo establecida",
"backupPasswordRemoved": "Contraseña de respaldo eliminada",
"battery": "Batería",
"bgRun": "Ejecución en segundo plano",
"bgRunTip": "Este interruptor solo indica que la aplicación intentará correr en segundo plano, si puede hacerlo o no depende de si tiene el permiso correspondiente. En Android puro, por favor desactiva la “optimización de batería” para esta app, en MIUI por favor cambia la estrategia de ahorro de energía a “Sin restricciones”.",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "No se recomienda activarlo, ¡tenga cuidado con los riesgos de seguridad! Si está utilizando el certificado predeterminado de PVE, debe habilitar esta opción.",
"pveLoginFailed": "Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.",
"pveVersionLow": "Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.",
"pwd": "Contraseña",
"read": "Leer",
"reboot": "Reiniciar",
"rememberPwdInMem": "Recordar contraseña en la memoria",
@@ -230,5 +236,5 @@
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
"write": "Escribir",
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en ~/.config/server_box para monitorear el estado del sistema. Puedes revisar el contenido del script."
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Connexion automatique",
"autoRun": "Exécution automatique",
"autoUpdateHomeWidget": "Mise à jour automatique du widget d'accueil",
"backupTip": "Les données exportées sont simplement chiffrées. \nVeuillez les garder en sécurité.",
"backupTip": "Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.",
"backupVersionNotMatch": "La version de sauvegarde ne correspond pas.",
"backupPassword": "Mot de passe de sauvegarde",
"backupPasswordTip": "Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.",
"backupPasswordWrong": "Mot de passe de sauvegarde incorrect",
"backupEncrypted": "La sauvegarde est chiffrée",
"backupNotEncrypted": "La sauvegarde n'est pas chiffrée",
"backupPasswordSet": "Mot de passe de sauvegarde défini",
"backupPasswordRemoved": "Mot de passe de sauvegarde supprimé",
"battery": "Batterie",
"bgRun": "Exécution en arrière-plan",
"bgRunTip": "Cette option signifie seulement que le programme essaiera de s'exécuter en arrière-plan, que cela soit possible dépend de l'autorisation activée ou non. Pour Android natif, veuillez désactiver l'« Optimisation de la batterie » dans cette application, et pour MIUI, veuillez changer la politique d'économie d'énergie en « Illimité ».",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Il n'est pas recommandé de l'activer, attention aux risques de sécurité ! Si vous utilisez le certificat par défaut de PVE, vous devez activer cette option.",
"pveLoginFailed": "Échec de la connexion. Impossible d'authentifier avec le nom d'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.",
"pveVersionLow": "Cette fonctionnalité est actuellement en phase de test et n'a été testée que sur PVE 8+. Veuillez l'utiliser avec prudence.",
"pwd": "Mot de passe",
"read": "Lire",
"reboot": "Redémarrer",
"rememberPwdInMem": "Mémoriser le mot de passe en mémoire",
@@ -230,5 +236,5 @@
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
"write": "Écrire",
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans ~/.config/server_box pour surveiller létat du système. Vous pouvez examiner le contenu du script."
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller létat du système. Vous pouvez examiner le contenu du script."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Hubungkan otomatis",
"autoRun": "Berjalan Otomatis",
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
"backupTip": "Data yang diekspor hanya dienkripsi.\nTolong jaga keamanannya.",
"backupTip": "Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.",
"backupVersionNotMatch": "Versi cadangan tidak cocok.",
"backupPassword": "Kata sandi cadangan",
"backupPasswordTip": "Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.",
"backupPasswordWrong": "Kata sandi cadangan salah",
"backupEncrypted": "Cadangan telah dienkripsi",
"backupNotEncrypted": "Cadangan tidak dienkripsi",
"backupPasswordSet": "Kata sandi cadangan ditetapkan",
"backupPasswordRemoved": "Kata sandi cadangan dihapus",
"battery": "Baterai",
"bgRun": "Jalankan di Backgroud",
"bgRunTip": "Sakelar ini hanya berarti aplikasi akan mencoba berjalan di latar belakang, apakah aplikasi dapat berjalan di latar belakang tergantung pada apakah izin diaktifkan atau tidak. Untuk Android asli, nonaktifkan \"Pengoptimalan Baterai\" di aplikasi ini, dan untuk miui, ubah kebijakan penghematan daya ke \"Tidak Terbatas\".",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Tidak disarankan untuk diaktifkan, waspadai risiko keamanan! Jika Anda menggunakan sertifikat default dari PVE, Anda perlu mengaktifkan opsi ini.",
"pveLoginFailed": "Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.",
"pveVersionLow": "Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.",
"pwd": "Kata sandi",
"read": "Baca",
"reboot": "Reboot",
"rememberPwdInMem": "Ingat kata sandi di dalam memori",
@@ -230,5 +236,5 @@
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
"write": "Tulis",
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke ~/.config/server_box untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "自動接続",
"autoRun": "自動実行",
"autoUpdateHomeWidget": "ホームウィジェットを自動更新",
"backupTip": "エクスポートされたデータは簡単に暗号化されています。適切に保管してください。",
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
"backupPassword": "バックアップパスワード",
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
"backupPasswordWrong": "バックアップパスワードが間違っています",
"backupEncrypted": "バックアップは暗号化されています",
"backupNotEncrypted": "バックアップは暗号化されていません",
"backupPasswordSet": "バックアップパスワードが設定されました",
"backupPasswordRemoved": "バックアップパスワードが削除されました",
"battery": "バッテリー",
"bgRun": "バックグラウンド実行",
"bgRunTip": "このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "オプションを有効にすることは推奨されません、セキュリティリスクに注意してくださいPVEのデフォルト証明書を使用している場合は、このオプションを有効にする必要があります。",
"pveLoginFailed": "ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。",
"pveVersionLow": "この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。",
"pwd": "パスワード",
"read": "読み取り",
"reboot": "再起動",
"rememberPwdInMem": "メモリにパスワードを記憶する",
@@ -230,5 +236,5 @@
"wolTip": "WOLWake-on-LANを設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
"write": "書き込み",
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが ~/.config/server_box に書き込まれます。スクリプトの内容を確認できます。"
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Automatisch verbinden",
"autoRun": "Automatisch uitvoeren",
"autoUpdateHomeWidget": "Automatische update van home-widget",
"backupTip": "De geëxporteerde gegevens zijn simpelweg versleuteld. \nBewaar deze aub veilig.",
"backupTip": "De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.",
"backupVersionNotMatch": "Back-upversie komt niet overeen.",
"backupPassword": "Back-up wachtwoord",
"backupPasswordTip": "Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.",
"backupPasswordWrong": "Onjuist back-up wachtwoord",
"backupEncrypted": "Back-up is versleuteld",
"backupNotEncrypted": "Back-up is niet versleuteld",
"backupPasswordSet": "Back-up wachtwoord ingesteld",
"backupPasswordRemoved": "Back-up wachtwoord verwijderd",
"battery": "Batterij",
"bgRun": "Uitvoeren op de achtergrond",
"bgRunTip": "Deze schakelaar betekent alleen dat het programma zal proberen op de achtergrond uit te voeren, of het in de achtergrond kan worden uitgevoerd, hangt af van of de toestemming is ingeschakeld of niet. Voor native Android, schakel \"Batterijoptimalisatie\" uit in deze app, en voor miui, wijzig de energiebesparingsbeleid naar \"Onbeperkt\".",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Niet aanbevolen om in te schakelen, let op beveiligingsrisico's! Als u de standaardcertificaat van PVE gebruikt, moet u deze optie inschakelen.",
"pveLoginFailed": "Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.",
"pveVersionLow": "Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.",
"pwd": "Wachtwoord",
"read": "Lezen",
"reboot": "Herstart",
"rememberPwdInMem": "Wachtwoord onthouden in geheugen",
@@ -230,5 +236,5 @@
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
"write": "Schrijven",
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar ~/.config/server_box om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Conexão automática",
"autoRun": "Execução automática",
"autoUpdateHomeWidget": "Atualização automática do widget da tela inicial",
"backupTip": "Os dados exportados são criptografados de forma simples, por favor, guarde-os com segurança.",
"backupTip": "Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.",
"backupVersionNotMatch": "Versão de backup não compatível, não é possível restaurar",
"backupPassword": "Senha de backup",
"backupPasswordTip": "Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.",
"backupPasswordWrong": "Senha de backup incorreta",
"backupEncrypted": "Backup está criptografado",
"backupNotEncrypted": "Backup não está criptografado",
"backupPasswordSet": "Senha de backup definida",
"backupPasswordRemoved": "Senha de backup removida",
"battery": "Bateria",
"bgRun": "Execução em segundo plano",
"bgRunTip": "Este interruptor indica que o programa tentará rodar em segundo plano, mas a capacidade de fazer isso depende das permissões concedidas. No Android nativo, desative a 'Otimização de bateria' para este app, no MIUI, altere a estratégia de economia de energia para 'Sem restrições'.",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Não recomendado para ativar, cuidado com os riscos de segurança! Se estiver usando o certificado padrão do PVE, você precisa habilitar esta opção.",
"pveLoginFailed": "Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.",
"pveVersionLow": "Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.",
"pwd": "Senha",
"read": "Leitura",
"reboot": "Reiniciar",
"rememberPwdInMem": "Lembrar senha na memória",
@@ -230,5 +236,5 @@
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
"write": "Escrita",
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
"writeScriptTip": "Após conectar ao servidor, um script será escrito em ~/.config/server_box para monitorar o status do sistema. Você pode revisar o conteúdo do script."
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Автоматическое подключение",
"autoRun": "Автозапуск",
"autoUpdateHomeWidget": "Автоматическое обновление виджета на главном экране",
"backupTip": "Экспортированные данные зашифрованы простым способом \nПожалуйста, храните их в безопасности.",
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
"backupPassword": "Пароль резервной копии",
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
"backupPasswordWrong": "Неверный пароль резервной копии",
"backupEncrypted": "Резервная копия зашифрована",
"backupNotEncrypted": "Резервная копия не зашифрована",
"backupPasswordSet": "Пароль резервной копии установлен",
"backupPasswordRemoved": "Пароль резервной копии удален",
"battery": "Батарея",
"bgRun": "Работа в фоновом режиме",
"bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.",
"pveLoginFailed": "Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.",
"pveVersionLow": "Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.",
"pwd": "Пароль",
"read": "Чтение",
"reboot": "Перезагрузка",
"rememberPwdInMem": "Запомнить пароль в памяти",
@@ -230,5 +236,5 @@
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
"write": "Запись",
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
"writeScriptTip": "После подключения к серверу скрипт будет записан в ~/.config/server_box для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Otomatik bağlan",
"autoRun": "Otomatik çalıştır",
"autoUpdateHomeWidget": "Ana ekran bileşenini otomatik güncelle",
"backupTip": "Dışa aktarılan veriler zayıf bir şekilde şifrelenmiştir. \nLütfen güvenli bir şekilde saklayın.",
"backupTip": "Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.",
"backupVersionNotMatch": "Yedekleme sürümü eşleşmiyor.",
"backupPassword": "Yedekleme parolası",
"backupPasswordTip": "Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.",
"backupPasswordWrong": "Yanlış yedekleme parolası",
"backupEncrypted": "Yedekleme şifrelenmiş",
"backupNotEncrypted": "Yedekleme şifreli değil",
"backupPasswordSet": "Yedekleme parolası ayarlandı",
"backupPasswordRemoved": "Yedekleme parolası kaldırıldı",
"battery": "Pil",
"bgRun": "Arka planda çalıştır",
"bgRunTip": "Bu anahtar yalnızca programın arka planda çalışmayı deneyeceği anlamına gelir. Arka planda çalışıp çalışamayacağı, iznin etkinleştirilip etkinleştirilmediğine bağlıdır. AOSP tabanlı Android ROM'lar için lütfen bu uygulamada \"Pil Optimizasyonu\"nu devre dışı bırakın. MIUI / HyperOS için lütfen güç tasarrufu politikasını \"Sınırsız\" olarak değiştirin.",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Etkinleştirilmesi önerilmez, güvenlik risklerine dikkat edin! PVE'den varsayılan sertifikayı kullanıyorsanız, bu seçeneği etkinleştirmeniz gerekir.",
"pveLoginFailed": "Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.",
"pveVersionLow": "Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.",
"pwd": "Şifre",
"read": "Oku",
"reboot": "Yeniden başlat",
"rememberPwdInMem": "Şifreyi bellekte hatırla",
@@ -230,5 +236,5 @@
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
"write": "Yaz",
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için ~/.config/server_box dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "Авто підключення",
"autoRun": "Авто запуск",
"autoUpdateHomeWidget": "Автоматичне оновлення віджетів на головному екрані",
"backupTip": "Експортовані дані слабо зашифровані. \nБудь ласка, зберігайте їх у безпеці.",
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
"backupPassword": "Пароль резервного копіювання",
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
"backupEncrypted": "Резервна копія зашифрована",
"backupNotEncrypted": "Резервна копія не зашифрована",
"backupPasswordSet": "Пароль резервного копіювання встановлено",
"backupPasswordRemoved": "Пароль резервного копіювання видалено",
"battery": "Акумулятор",
"bgRun": "Запуск у фоновому режимі",
"bgRunTip": "Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.",
"pveLoginFailed": "Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.",
"pveVersionLow": "Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.",
"pwd": "Пароль",
"read": "Читати",
"reboot": "Перезавантажити",
"rememberPwdInMem": "Запам'ятати пароль у пам'яті",
@@ -230,5 +236,5 @@
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
"write": "Записати",
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
"writeScriptTip": "Після підключення до сервера скрипт буде записано у ~/.config/server_box для моніторингу стану системи. Ви можете переглянути вміст скрипта."
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
}

View File

@@ -11,8 +11,15 @@
"autoConnect": "自动连接",
"autoRun": "自动运行",
"autoUpdateHomeWidget": "自动更新桌面小部件",
"backupTip": "导出的数据仅进行了简单加密,请妥善保管。",
"backupTip": "导出的数据可以使用密码加密,请妥善保管。",
"backupVersionNotMatch": "备份版本不匹配,无法恢复",
"backupPassword": "备份密码",
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
"backupPasswordWrong": "备份密码错误",
"backupEncrypted": "备份已加密",
"backupNotEncrypted": "备份未加密",
"backupPasswordSet": "备份密码已设置",
"backupPasswordRemoved": "备份密码已移除",
"battery": "电池",
"bgRun": "后台运行",
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”MIUI / HyperOS 请修改省电策略为“无限制”。",
@@ -137,7 +144,6 @@
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
"pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。",
"pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用",
"pwd": "密码",
"read": "读",
"reboot": "重启",
"rememberPwdInMem": "在内存中记住密码",
@@ -230,5 +236,5 @@
"wolTip": "在配置 WOL 后,每次连接服务器都会先发送一次 WOL 请求",
"write": "写",
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
"writeScriptTip": "在连接服务器后,会向 ~/.config/server_box 写入脚本来监测系统状态,你可以审查脚本内容。"
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
}

View File

@@ -2,97 +2,104 @@
"@@locale": "zh_TW",
"aboutThanks": "感謝以下參與的各位。",
"acceptBeta": "接受測試版更新推送",
"addSystemPrivateKeyTip": "前沒有任何私鑰,是否添加系統自帶的 (~/.ssh/id_rsa)",
"added2List": "已添加至任務列表",
"addSystemPrivateKeyTip": "前沒有任何私鑰,是否新增系統原有的 (~/.ssh/id_rsa)",
"added2List": "已新增至任務清單",
"addr": "位址",
"alreadyLastDir": "已經是最上層目錄了",
"authFailTip": "認證失敗,請檢查密碼/鑰/主機/用戶等是否錯誤。",
"authFailTip": "認證失敗,請檢查密碼/鑰/主機/使用者等是否錯誤。",
"autoBackupConflict": "只能同時開啓一個自動備份",
"autoConnect": "自動連",
"autoRun": "自動行",
"autoUpdateHomeWidget": "自動更新桌面小部件",
"backupTip": "匯出的資料僅進行了簡單加密,請妥善保管。",
"backupVersionNotMatch": "備份版本不匹配,無法還原",
"autoConnect": "自動連",
"autoRun": "自動行",
"autoUpdateHomeWidget": "自動更新桌面小工具",
"backupTip": "匯出的資料可以使用密碼加密。 \n請妥善保管。",
"backupVersionNotMatch": "備份版本不相符,無法還原",
"backupPassword": "備份密碼",
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
"backupPasswordWrong": "備份密碼錯誤",
"backupEncrypted": "備份已加密",
"backupNotEncrypted": "備份未加密",
"backupPasswordSet": "備份密碼已設定",
"backupPasswordRemoved": "備份密碼已移除",
"battery": "電池",
"bgRun": "後台運行",
"bgRunTip": "此開關只代表程式會嘗試在後台運行,具體能否在後臺行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池化”MIUI / HyperOS 請修改省電策略為“無限制”。",
"bgRun": "背景執行",
"bgRunTip": "此開關只代表程式會嘗試在背景執行,具體能否在後臺行取決於是否開啟了權限。 原生 Android 請關閉本 App 的“電池最佳化”MIUI / HyperOS 請修改省電策略為“無限制”。",
"closeAfterSave": "儲存後關閉",
"cmd": "令",
"cmd": "令",
"collapseUITip": "是否預設折疊 UI 中存在的長列表",
"conn": "連",
"conn": "連",
"container": "容器",
"containerTrySudoTip": "例如App 內設使用者為 aaa但是 Docker 安裝在 root 使用者,這時就需要開啟此選項",
"containerTrySudoTip": "例如App 內設使用者為 aaa但是 Docker 安裝在 root 使用者,這時就需要開啟此選項",
"convert": "轉換",
"copyPath": "複製路徑",
"cpuViewAsProgressTip": "以進度條樣式顯示每個CPU的使用率舊版樣式",
"cursorType": "游標類型",
"customCmd": "自訂令",
"customCmdDocUrl": "https://github.com/lollipopkit/flutter_server_box/wiki/主页#自定义令",
"customCmdHint": "\"令名稱\": \"令\"",
"customCmd": "自訂令",
"customCmdDocUrl": "https://github.com/lollipopkit/flutter_server_box/wiki/主页#自定义令",
"customCmdHint": "\"令名稱\": \"令\"",
"decode": "解碼",
"decompress": "解壓縮",
"deleteServers": "量刪除伺服器",
"deleteServers": "量刪除伺服器",
"desktopTerminalTip": "啟動 SSH 連線時用於打開終端機模擬器的指令。",
"dirEmpty": "請確保資料夾為空",
"disconnected": "連接斷開",
"disconnected": "連線中斷",
"disk": "磁碟",
"diskHealth": "磁碟健康",
"diskIgnorePath": "忽略的磁碟路徑",
"displayCpuIndex": "顯示 CPU 索引",
"dl2Local": "下載 {fileName} 到本地?",
"dockerEmptyRunningItems": "沒有正在行的容器。\n這可能是因為\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變 DOCKER_HOST 沒有被正確讀取。你可以通過在終端內運行 `echo $DOCKER_HOST` 來獲取。",
"dockerImagesFmt": "共 {count} 個鏡像",
"dockerEmptyRunningItems": "沒有正在行的容器。\n這可能是因為\n- Docker 安裝使用者與 App 內配置的使用者名稱不同\n- 環境變 DOCKER_HOST 沒有被正確讀取。你可以通過在終端機內執行 `echo $DOCKER_HOST` 來獲取。",
"dockerImagesFmt": "共 {count} 個映像檔",
"dockerNotInstalled": "Docker 未安裝",
"dockerStatusRunningAndStoppedFmt": "{runningCount} 個正在行, {stoppedCount} 個已停止",
"dockerStatusRunningFmt": "{count} 個容器正在行",
"dockerStatusRunningAndStoppedFmt": "{runningCount} 個正在行, {stoppedCount} 個已停止",
"dockerStatusRunningFmt": "{count} 個容器正在行",
"doubleColumnMode": "雙列模式",
"doubleColumnTip": "此選項僅開啟功能,實際是否能開啟還取決於設備的寬",
"doubleColumnTip": "此選項僅開啟功能,實際是否能開啟還取決於設備的寬",
"editVirtKeys": "編輯虛擬按鍵",
"editor": "編輯器",
"editorHighlightTip": "目前的代碼高亮性能較為糟糕,可以選擇關閉以改善。",
"editorHighlightTip": "目前的程式碼標記效能較為糟糕,可以選擇關閉以改善。",
"emulator": "模擬器",
"encode": "編碼",
"envVars": "環境變",
"envVars": "環境變",
"experimentalFeature": "實驗性功能",
"extraArgs": "額外參數",
"fallbackSshDest": "備選 SSH 目標",
"fdroidReleaseTip": "如果你是從 F-Droid 下載的本應用,推薦關閉此選項",
"fdroidReleaseTip": "如果你是從 F-Droid 下載的本App,推薦關閉此選項",
"fgService": "前台服務",
"fgServiceTip": "開啟後,可能會導致部分機型閃退。關閉可能導致部分機型無法後台保持 SSH 連。請在系統設內允許 ServerBox 通知權限、後台運行、自我喚醒。",
"fileTooLarge": "文件 '{file}' 過大 '{size}',超過了 {sizeMax}",
"fgServiceTip": "開啟後,可能會導致部分機型閃退。關閉可能導致部分機型無法背景保持 SSH 連。請在系統設內允許 ServerBox 通知權限、背景執行、自我喚醒。",
"fileTooLarge": "檔案 '{file}' 過大 '{size}',超過了 {sizeMax}",
"followSystem": "跟隨系統",
"font": "字型",
"fontSize": "字型大小",
"force": "強制",
"fullScreen": "全螢幕模式",
"fullScreenJitter": "全螢幕模式抖動",
"fullScreenJitterHelp": "防止燒屏",
"fullScreenTip": "當設備旋轉為橫時,是否開啟全幕模式?此選項僅適用於伺服器選項卡。",
"fullScreenJitterHelp": "防止烙印",
"fullScreenTip": "當設備旋轉為橫時,是否開啟全幕模式?此選項僅適用於伺服器分頁。",
"goBackQ": "返回?",
"goto": "前往",
"hideTitleBar": "隱藏標題欄",
"highlight": "代碼高亮",
"homeWidgetUrlConfig": "桌面部件鏈接配置",
"highlight": "程式碼標記",
"homeWidgetUrlConfig": "桌面小工具連結配置",
"host": "主機",
"httpFailedWithCode": "請求失敗, 狀態碼: {code}",
"ignoreCert": "忽略證",
"image": "鏡像",
"imagesList": "鏡像列表",
"ignoreCert": "忽略證",
"image": "映像檔",
"imagesList": "映像檔列表",
"init": "初始化",
"inner": "內建",
"install": "安裝",
"installDockerWithUrl": "請先 https://docs.docker.com/engine/install docker",
"invalid": "無效",
"jumpServer": "跳板伺服器",
"keepForeground": "請保持應用處於前",
"keepForeground": "請保持App處於前",
"keepStatusWhenErr": "保留上次的伺服器狀態",
"keepStatusWhenErrTip": "僅在執行腳本出錯時",
"keyAuth": "鑰認證",
"letterCache": "入法字符緩存",
"keyAuth": "鑰認證",
"letterCache": "入法字符快取",
"letterCacheTip": "建議關閉,但關閉後將無法輸入 CJK 等文字。",
"license": "證",
"license": "證",
"location": "位置",
"loss": "丟包率",
"loss": "逾時",
"madeWithLove": "用❤️製作 by {myGithub}",
"manual": "手動",
"max": "最大",
@@ -103,10 +110,10 @@
"more": "更多",
"moveOutServerFuncBtnsHelp": "開啟:可以在伺服器 Tab 頁的每個卡片下方顯示。關閉:在伺服器詳情頁頂部顯示。",
"ms": "毫秒",
"needHomeDir": "如果你是群暉用戶[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統用戶,需搜如何建家目錄home directory。",
"needRestart": "需要重 App",
"needHomeDir": "如果你是群暉使用者[看這裡](https://kb.synology.com/DSM/tutorial/user_enable_home_service)。其他系統使用者,需搜如何建家目錄home directory。",
"needRestart": "需要重 App",
"net": "網路",
"netViewType": "網路視類型",
"netViewType": "網路視類型",
"newContainer": "新建容器",
"noLineChart": "不使用折線圖",
"noLineChartForCpu": "CPU 不使用折線圖",
@@ -115,55 +122,54 @@
"node": "節點",
"notAvailable": "不可用",
"onServerDetailPage": "在伺服器詳情頁",
"onlyOneLine": "僅顯示為一行(可動)",
"onlyOneLine": "僅顯示為一行(可動)",
"onlyWhenCoreBiggerThan8": "僅當核心數大於 8 時生效",
"openLastPath": "打開上次的路徑",
"openLastPathTip": "不同的伺服器會有不同的記錄,且記錄的是退出時的路徑",
"parseContainerStatsTip": "Docker 解析佔用狀態較為緩慢",
"parseContainerStatsTip": "Docker 解析消耗狀態較為緩慢",
"percentOfSize": "{size} 的 {percent}%",
"permission": "權限",
"pingAvg": "平均:",
"pingInputIP": "請輸入目標 IP 或域名",
"pingNoServer": "沒有伺服器可用於 Ping\n請在伺服器 Tab 新增伺服器後再試",
"pkg": "管理",
"pkg": "套件管理",
"plugInType": "插入類型",
"port": "埠",
"preferDiskAmount": "優先顯示硬碟容量",
"preview": "預覽",
"privateKey": "私鑰",
"process": "行程",
"process": "處理程序",
"prune": "修剪",
"pushToken": "消息推送 Token",
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的默認證書,則需要啟用此選項。",
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。",
"pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。",
"pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。",
"pwd": "密碼",
"read": "",
"reboot": "重启",
"read": "讀取",
"reboot": "重開",
"rememberPwdInMem": "在記憶體中記住密碼",
"rememberPwdInMemTip": "用於容器、暫停等",
"rememberWindowSize": "記住窗大小",
"rememberWindowSize": "記住窗大小",
"remotePath": "遠端路徑",
"restart": "重",
"restart": "重",
"result": "結果",
"rotateAngel": "旋轉角度",
"route": "路由",
"run": "行",
"run": "行",
"running": "運作中",
"sameIdServerExist": "已存在相同 ID 的伺服器",
"save": "存",
"saved": "已存",
"save": "存",
"saved": "已存",
"second": "秒",
"sensors": "感器",
"sensors": "感器",
"sequence": "順序",
"server": "伺服器",
"serverDetailOrder": "詳情頁部件順序",
"serverFuncBtns": "伺服器功能按鈕",
"serverOrder": "伺服器順序",
"sftpDlPrepare": "準備連至伺服器...",
"sftpEditorTip": "如果為空, 使用App內置的文件編輯器。如果有值, 則使用遠伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。",
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除文件夾",
"sftpSSHConnected": "SFTP 已連...",
"sftpDlPrepare": "準備連至伺服器...",
"sftpEditorTip": "如果為空, 使用App內建的檔案編輯器。如果有值, 則使用遠伺服器的編輯器, 例如 `vim`(建議根據 `EDITOR` 自動獲取)。",
"sftpRmrDirSummary": "在 SFTP 中使用 `rm -r` 來刪除檔案夾",
"sftpSSHConnected": "SFTP 已連...",
"sftpShowFoldersFirst": "資料夾顯示在前",
"showDistLogo": "顯示發行版 Logo",
"shutdown": "關機",
@@ -174,8 +180,8 @@
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
"speed": "速度",
"spentTime": "耗時: {time}",
"sshTermHelp": "在終端可滾動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。文件圖標會打開前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端。代碼圖會貼上碼片段到終端並執行。",
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 饋問題,或者加入我們開發。",
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖會貼上程式碼片段到終端並執行。",
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 饋問題,或者加入我們開發。",
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
"start": "開始",
"stat": "統計",
@@ -184,15 +190,15 @@
"stopped": "已停止",
"storage": "存儲",
"supportFmtArgs": "支援以下格式化參數:",
"suspend": "挂起",
"suspendTip": "suspend 功能需要 root 權限及 systemd 支。",
"suspend": "當機",
"suspendTip": "suspend 功能需要 root 權限及 systemd 支。",
"switchTo": "切換到 {val}",
"sync": "同步",
"syncTip": "可能需要重新啟動,某些更改才能生效。",
"system": "系統",
"tag": "标签",
"tag": "標籤",
"temperature": "溫度",
"termFontSizeTip": "此設將影響終端大小(寬度和高度)。您可以在終端頁面縮放,來調整前會話的字型大小。",
"termFontSizeTip": "此設將影響終端大小(寬度和高度)。您可以在終端頁面縮放,來調整前會話的字型大小。",
"terminal": "终端機",
"test": "測試",
"textScaler": "字型縮放",
@@ -207,28 +213,28 @@
"unknown": "未知",
"unkownConvertMode": "未知轉換模式",
"update": "更新",
"updateIntervalEqual0": "你設為 0伺服器狀態不會自動更新。\n且不能計算CPU使用情況。",
"updateIntervalEqual0": "你設為 0伺服器狀態不會自動更新。\n且不能計算CPU使用情況。",
"updateServerStatusInterval": "伺服器狀態更新間隔",
"upload": "上傳",
"upsideDown": "上下交換",
"uptime": "啟動時長",
"uptime": "運作時間",
"useCdn": "使用 CDN",
"useCdnTip": "非中國大陸用戶建議使用 CDN是否使用",
"useNoPwd": "使用無密碼",
"usePodmanByDefault": "默認使用 Podman",
"used": "已用",
"view": "視",
"useCdnTip": "非中國使用者建議使用 CDN是否使用",
"useNoPwd": "使用無密碼",
"usePodmanByDefault": "預設使用 Podman",
"used": "已使用",
"view": "視",
"viewErr": "查看錯誤",
"virtKeyHelpClipboard": "如果終端有選中字元,則復製選中字元至剪貼簿,否則貼剪貼簿內容至終端。",
"virtKeyHelpClipboard": "如果終端有選中字元,則復製選中字元至剪貼簿,否則貼剪貼簿內容至終端。",
"virtKeyHelpIME": "打開/關閉鍵盤",
"virtKeyHelpSFTP": "在 SFTP 中打開前路徑。",
"waitConnection": "請等待連建立",
"virtKeyHelpSFTP": "在 SFTP 中打開前路徑。",
"waitConnection": "請等待連建立",
"wakeLock": "保持喚醒",
"watchNotPaired": "沒有已配對的 Apple Watch",
"webdavSettingEmpty": "WebDav 設項爲空",
"webdavSettingEmpty": "WebDav 設項爲空",
"whenOpenApp": "當打開 App 時",
"wolTip": "在配置 WOL網絡喚醒每次連伺服器都會先發送一次 WOL 請求。",
"write": "",
"wolTip": "在配置 WOL網絡喚醒每次連伺服器都會先發送一次 WOL 請求。",
"write": "寫入",
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
"writeScriptTip": "連到伺服器後,將會在 ~/.config/server_box 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
"writeScriptTip": "連到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
}

View File

@@ -8,7 +8,8 @@ import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
import 'package:server_box/data/model/app/bak/utils.dart';
import 'package:server_box/data/model/app/bak/backup_service.dart';
import 'package:server_box/data/model/app/bak/backup_source.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/provider/snippet.dart';
@@ -22,10 +23,7 @@ class BackupPage extends StatefulWidget {
@override
State<BackupPage> createState() => _BackupPageState();
static const route = AppRouteNoArg(
page: BackupPage.new,
path: '/backup',
);
static const route = AppRouteNoArg(page: BackupPage.new, path: '/backup');
}
final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveClientMixin {
@@ -40,33 +38,27 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: _buildBody(context),
);
return Scaffold(body: _buildBody);
}
Widget _buildBody(BuildContext context) {
Widget get _buildBody {
return MultiList(
widthDivider: 2,
children: [
[
CenterGreyTitle(libL10n.sync),
_buildTip(),
if (isMacOS || isIOS) _buildIcloud(context),
_buildWebdav(context),
_buildFile(context),
_buildClipboard(context),
],
[
CenterGreyTitle(libL10n.import),
_buildBulkImportServers(context),
_buildImportSnippet(context),
_buildTip,
if (isMacOS || isIOS) _buildIcloud,
_buildWebdav,
_buildFile,
_buildClipboard,
],
[CenterGreyTitle(libL10n.import), _buildBulkImportServers, _buildImportSnippet],
],
);
}
Widget _buildTip() {
Widget get _buildTip {
return CardX(
child: ListTile(
leading: const Icon(Icons.warning),
@@ -76,7 +68,7 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
);
}
Widget _buildFile(BuildContext context) {
Widget get _buildFile {
return CardX(
child: ExpandTile(
leading: const Icon(Icons.file_open),
@@ -84,24 +76,21 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
initiallyExpanded: false,
children: [
ListTile(
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () async {
final path = await BackupV2.backup();
await Pfs.sharePaths(paths: [path]);
},
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () => BackupService.backup(context, FileBackupSource())
),
ListTile(
trailing: const Icon(Icons.restore),
title: Text(libL10n.restore),
onTap: () async => _onTapFileRestore(context),
onTap: () => BackupService.restore(context, FileBackupSource()),
),
],
),
);
}
Widget _buildIcloud(BuildContext context) {
Widget get _buildIcloud {
return CardX(
child: ListTile(
leading: const Icon(Icons.cloud),
@@ -123,7 +112,7 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
);
}
Widget _buildWebdav(BuildContext context) {
Widget get _buildWebdav {
return CardX(
child: ExpandTile(
leading: const Icon(Icons.storage),
@@ -171,33 +160,25 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
),
ListTile(
title: Text(l10n.manual),
trailing: webdavLoading.listenVal(
(loading) {
if (loading) return SizedLoading.small;
trailing: webdavLoading.listenVal((loading) {
if (loading) return SizedLoading.small;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () async => _onTapWebdavDl(context),
child: Text(libL10n.restore),
),
UIs.width7,
TextButton(
onPressed: () async => _onTapWebdavUp(context),
child: Text(libL10n.backup),
),
],
);
},
),
return Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(onPressed: () async => _onTapWebdavDl(context), child: Text(libL10n.restore)),
UIs.width7,
TextButton(onPressed: () async => _onTapWebdavUp(context), child: Text(libL10n.backup)),
],
);
}),
),
],
),
);
}
Widget _buildClipboard(BuildContext context) {
Widget get _buildClipboard {
return CardX(
child: ExpandTile(
leading: const Icon(Icons.content_paste),
@@ -206,23 +187,19 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
ListTile(
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () async {
final path = await BackupV2.backup();
Pfs.copy(await File(path).readAsString());
context.showSnackBar(libL10n.success);
},
onTap: () => BackupService.backup(context, ClipboardBackupSource()),
),
ListTile(
trailing: const Icon(Icons.restore),
title: Text(libL10n.restore),
onTap: () async => _onTapClipboardRestore(context),
onTap: () => BackupService.restore(context, ClipboardBackupSource()),
),
],
),
);
}
Widget _buildBulkImportServers(BuildContext context) {
Widget get _buildBulkImportServers {
return CardX(
child: ListTile(
title: Text(l10n.server),
@@ -233,16 +210,13 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
);
}
Widget _buildImportSnippet(BuildContext context) {
Widget get _buildImportSnippet {
return ListTile(
title: Text(l10n.snippet),
leading: const Icon(MingCute.code_line),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
final data = await context.showImportDialog(
title: l10n.snippet,
modelDef: Snippet.example.toJson(),
);
final data = await context.showImportDialog(title: l10n.snippet, modelDef: Snippet.example.toJson());
if (data == null) return;
final str = String.fromCharCodes(data);
final (list, _) = await context.showLoadingDialog(
@@ -275,11 +249,7 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
final snippetNames = snippets.map((e) => e.name).join(', ');
context.showRoundDialog(
title: libL10n.attention,
child: SingleChildScrollView(
child: Text(
libL10n.askContinue('${libL10n.import} [$snippetNames]'),
),
),
child: SingleChildScrollView(child: Text(libL10n.askContinue('${libL10n.import} [$snippetNames]'))),
actions: Btn.ok(
onTap: () {
for (final snippet in snippets) {
@@ -294,33 +264,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
).cardx;
}
Future<void> _onTapFileRestore(BuildContext context) async {
final text = await Pfs.pickFileString();
if (text == null) return;
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text.trim()),
);
if (err != null || backup == null) return;
await context.showRoundDialog(
title: libL10n.restore,
child: Text(libL10n.askContinue(
'${libL10n.restore} ${libL10n.backup}(${backup.$2})',
)),
actions: Btn.ok(
onTap: () async {
await backup.$1.merge(force: true);
context.pop();
},
).toList,
);
} catch (e, s) {
Loggers.app.warning('Import backup failed', e, s);
context.showErrDialog(e, s, libL10n.restore);
}
}
Future<void> _onTapWebdavDl(BuildContext context) async {
webdavLoading.value = true;
@@ -328,16 +271,12 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
final files = await Webdav.shared.list();
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
final fileName = await context.showPickSingleDialog(
title: libL10n.restore,
items: files,
);
final fileName = await context.showPickSingleDialog(title: libL10n.restore, items: files);
if (fileName == null) return;
await Webdav.shared.download(relativePath: fileName);
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
final dlBak = await Computer.shared.start(BackupV2.fromJsonString, dlFile);
await dlBak.merge(force: true);
await BackupService.restoreFromText(context, dlFile);
} catch (e, s) {
context.showErrDialog(e, s, libL10n.restore);
Loggers.app.warning('Download webdav backup failed', e, s);
@@ -351,7 +290,8 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
final bakName = '$date-${Miscs.bakFileName}';
try {
await BackupV2.backup(bakName);
final savedPassword = await Stores.setting.backupasswd.read();
await BackupV2.backup(bakName, savedPassword);
await Webdav.shared.upload(relativePath: bakName);
Loggers.app.info('Upload webdav backup success');
} catch (e, s) {
@@ -388,7 +328,7 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
onSubmitted: (p0) => FocusScope.of(context).requestFocus(nodePwd),
),
Input(
label: l10n.pwd,
label: libL10n.pwd,
controller: pwd,
node: nodePwd,
suggestion: false,
@@ -417,42 +357,9 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
}
}
void _onTapClipboardRestore(BuildContext context) async {
final text = await Pfs.paste();
if (text == null || text.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
}
try {
final (backup, err) = await context.showLoadingDialog(
fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text.trim()),
);
if (err != null || backup == null) return;
await context.showRoundDialog(
title: libL10n.restore,
child: Text(libL10n.askContinue(
'${libL10n.restore} ${libL10n.backup}(${backup.$2})',
)),
actions: Btn.ok(
onTap: () async {
await backup.$1.merge(force: true);
context.pop();
},
).toList,
);
} catch (e, s) {
Loggers.app.warning('Import backup failed', e, s);
context.showErrDialog(e, s, libL10n.restore);
}
}
void _onBulkImportServers(BuildContext context) async {
final data = await context.showImportDialog(
title: l10n.server,
modelDef: Spix.example.toJson(),
);
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
if (data == null) return;
final text = String.fromCharCodes(data);
@@ -487,6 +394,11 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
}
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -60,7 +60,7 @@ class _ContainerPageState extends State<ContainerPage> {
builder: (_, _, _) {
return Scaffold(
appBar: _buildAppBar,
body: _buildMain,
body: SafeArea(child: _buildMain),
floatingActionButton: _container.error == null ? _buildFAB : null,
);
},
@@ -101,25 +101,23 @@ class _ContainerPageState extends State<ContainerPage> {
UIs.height13,
_buildSettingsBtns,
],
),
).paddingSymmetric(horizontal: 13),
);
}
if (_container.items == null || _container.images == null) {
return UIs.centerLoading;
}
return SafeArea(
child: AutoMultiList(
children: <Widget>[
_buildLoading(),
_buildVersion(),
_buildPs(),
_buildImage(),
_buildEmptyStateMessage(),
_buildPruneBtns,
_buildSettingsBtns,
],
),
return AutoMultiList(
children: <Widget>[
_buildLoading(),
_buildVersion(),
_buildPs(),
_buildImage(),
_buildEmptyStateMessage(),
_buildPruneBtns,
_buildSettingsBtns,
],
);
}
@@ -155,10 +153,10 @@ class _ContainerPageState extends State<ContainerPage> {
return ListTile(
title: Text(title ?? l10n.unknown, style: UIs.text15),
subtitle: Text('${reg ?? ''} - ${e.tag} - ${e.sizeMB}', style: UIs.text13Grey),
trailing: IconButton(
trailing: Btn.icon(
padding: EdgeInsets.zero,
icon: const Icon(Icons.delete),
onPressed: () => _showImageRmDialog(e),
onTap: () => _showImageRmDialog(e),
),
);
}
@@ -318,7 +316,7 @@ class _ContainerPageState extends State<ContainerPage> {
message: type.tip,
onConfirm: switch (type) {
_PruneTypes.images => _container.pruneImages,
_PruneTypes.containers => () => _container.pruneContainers(),
_PruneTypes.containers => _container.pruneContainers,
_PruneTypes.volumes => _container.pruneVolumes,
_PruneTypes.system => _container.pruneSystem,
},

View File

@@ -190,7 +190,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
type: TextInputType.text,
node: _pwdNode,
obscureText: true,
label: l10n.pwd,
label: libL10n.pwd,
icon: Icons.password,
suggestion: false,
onSubmitted: (_) => _onTapSave(),

View File

@@ -1,7 +1,7 @@
part of 'view.dart';
extension on _ServerDetailPageState {
void _onTapGpuItem(NvidiaSmiItem item) {
void _onTapNvidiaGpuItem(NvidiaSmiItem item) {
final processes = item.memory.processes;
final displayCount = processes.length > 5 ? 5 : processes.length;
final height = displayCount * 47.0;
@@ -19,6 +19,24 @@ extension on _ServerDetailPageState {
);
}
void _onTapAmdGpuItem(AmdSmiItem item) {
final processes = item.memory.processes;
final displayCount = processes.length > 5 ? 5 : processes.length;
final height = displayCount * 47.0;
context.showRoundDialog(
title: item.name,
child: SizedBox(
width: double.maxFinite,
height: height,
child: ListView.builder(
itemCount: processes.length,
itemBuilder: (_, idx) => _buildAmdGpuProcessItem(processes[idx]),
),
),
actions: Btnx.oks,
);
}
void _onTapGpuProcessItem(NvidiaSmiMemProcess process) {
context.showRoundDialog(
title: '${process.pid}',
@@ -37,6 +55,24 @@ extension on _ServerDetailPageState {
);
}
void _onTapAmdGpuProcessItem(AmdSmiMemProcess process) {
context.showRoundDialog(
title: '${process.pid}',
titleMaxLines: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UIs.height13,
Text('Memory: ${process.memory} ${process.memory > 1024 ? 'MB' : 'KB'}'),
UIs.height13,
Text('Process: ${process.name}'),
],
),
actions: [TextButton(onPressed: () => context.pop(), child: Text(libL10n.close))],
);
}
void _onTapCustomItem(MapEntry<String, String> cmd) {
context.showRoundDialog(
title: cmd.key,

View File

@@ -8,11 +8,11 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/amd.dart';
import 'package:server_box/data/model/server/battery.dart';
import 'package:server_box/data/model/server/cpu.dart';
import 'package:server_box/data/model/server/disk.dart';
import 'package:server_box/data/model/server/disk_smart.dart';
import 'package:server_box/data/model/server/dist.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/nvdia.dart';
import 'package:server_box/data/model/server/sensors.dart';
@@ -22,6 +22,8 @@ import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/view/page/pve.dart';
import 'package:server_box/view/page/server/edit.dart';
import 'package:server_box/view/page/server/logo.dart';
import 'package:server_box/view/widget/server_func_btns.dart';
part 'misc.dart';
@@ -132,20 +134,15 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
}
Widget? _buildLogo(Server si) {
var logoUrl = si.spi.custom?.logoUrl ?? _settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
if (logoUrl == null) return null;
final dist = si.status.more[StatusCmdType.sys]?.dist;
if (dist != null) {
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
}
logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
final logoUrl = si.getLogoUrl(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 13),
child: LayoutBuilder(
builder: (_, cons) {
if (logoUrl == null) return UIs.placeholder;
if (logoUrl == null) {
return UIs.placeholder;
}
return ExtendedImage.network(
logoUrl,
cache: true,
@@ -414,9 +411,23 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
Widget? _buildGpuView(Server si) {
final ss = si.status;
if (ss.nvidia == null || ss.nvidia?.isEmpty == true) return null;
final hasNvidia = ss.nvidia != null && ss.nvidia!.isNotEmpty;
final hasAmd = ss.amd != null && ss.amd!.isNotEmpty;
if (!hasNvidia && !hasAmd) return null;
final children = <Widget>[];
// Add NVIDIA GPUs
if (hasNvidia) {
children.addAll(ss.nvidia!.map((e) => _buildNvidiaGpuItem(e)));
}
// Add AMD GPUs
if (hasAmd) {
children.addAll(ss.amd!.map((e) => _buildAmdGpuItem(e)));
}
final children = ss.nvidia?.map((e) => _buildGpuItem(e)).toList() ?? [];
return ExpandTile(
title: const Text('GPU'),
leading: const Icon(Icons.memory, size: 17),
@@ -425,7 +436,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
).cardx;
}
Widget _buildGpuItem(NvidiaSmiItem item) {
Widget _buildNvidiaGpuItem(NvidiaSmiItem item) {
final mem = item.memory;
return ListTile(
title: Text(item.name, style: UIs.text13),
@@ -445,7 +456,36 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(onPressed: () => _onTapGpuItem(item), icon: const Icon(Icons.info_outline, size: 17)),
IconButton(
onPressed: () => _onTapNvidiaGpuItem(item),
icon: const Icon(Icons.info_outline, size: 17),
),
],
),
);
}
Widget _buildAmdGpuItem(AmdSmiItem item) {
final mem = item.memory;
return ListTile(
title: Text('${item.name} (AMD)', style: UIs.text13),
leading: Text(
'${item.utilization}%\n${item.temp} °C',
style: UIs.text12Grey,
textScaler: _textFactor,
textAlign: TextAlign.center,
),
subtitle: Text(
'${item.power} - FAN ${item.fanSpeed} RPM\n${item.clockSpeed} MHz\n${mem.used} / ${mem.total} ${mem.unit}',
style: UIs.text12Grey,
textScaler: _textFactor,
),
contentPadding: const EdgeInsets.only(left: 17, right: 17),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(onPressed: () => _onTapAmdGpuItem(item), icon: const Icon(Icons.info_outline, size: 17)),
],
),
);
@@ -472,6 +512,27 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
);
}
Widget _buildAmdGpuProcessItem(AmdSmiMemProcess process) {
return ListTile(
title: Text(
process.name,
style: UIs.text12,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textScaler: _textFactor,
),
subtitle: Text(
'PID: ${process.pid} - ${process.memory} MiB',
style: UIs.text12Grey,
textScaler: _textFactor,
),
trailing: InkWell(
onTap: () => _onTapAmdGpuProcessItem(process),
child: const Icon(Icons.info_outline, size: 17),
),
);
}
Widget? _buildDiskView(Server si) {
final ss = si.status;
final children = <Widget>[];
@@ -650,7 +711,9 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
if (smart.model != null) details.add('Model: ${smart.model}');
if (smart.serial != null) details.add('Serial: ${smart.serial}');
if (smart.temperature != null) details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C');
if (smart.temperature != null) {
details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C');
}
if (smart.powerOnHours != null) {
details.add('Power On: ${smart.powerOnHours} ${libL10n.hour}');
@@ -700,10 +763,9 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
child: MarkdownBody(
data: '- $markdown',
selectable: true,
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
p: UIs.text13Grey,
h2: UIs.text15,
),
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(p: UIs.text13Grey, h2: UIs.text15),
),
actions: Btnx.oks,
);

View File

@@ -208,9 +208,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: _passwordController,
obscureText: true,
type: TextInputType.text,
label: l10n.pwd,
label: libL10n.pwd,
icon: Icons.password,
hint: l10n.pwd,
suggestion: false,
onSubmitted: (_) => _onSave(),
),
@@ -427,9 +426,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
controller: _wolPwdCtrl,
type: TextInputType.text,
obscureText: true,
label: l10n.pwd,
label: libL10n.pwd,
icon: Icons.password,
hint: l10n.pwd,
suggestion: false,
),
],

View File

@@ -0,0 +1,21 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/data/model/app/shell_func.dart';
import 'package:server_box/data/model/server/dist.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/res/store.dart';
extension LogoExt on Server {
String? getLogoUrl(BuildContext context) {
var logoUrl = spi.custom?.logoUrl ?? Stores.setting.serverLogoUrl.fetch().selfNotEmptyOrNull;
if (logoUrl == null) {
return null;
}
final dist = status.more[StatusCmdType.sys]?.dist;
if (dist != null) {
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
}
logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
return logoUrl;
}
}

View File

@@ -7,14 +7,7 @@ extension on _ServerPageState {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LayoutBuilder(
builder: (_, cons) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: cons.maxWidth / 2.3),
child: Text(s.spi.name, style: UIs.text13Bold, maxLines: 1, overflow: TextOverflow.ellipsis),
);
},
),
Text(s.spi.name, style: UIs.text13Bold, maxLines: 1, overflow: TextOverflow.ellipsis),
const Icon(Icons.keyboard_arrow_right, size: 17, color: Colors.grey),
const Spacer(),
_buildTopRightText(s),

View File

@@ -114,7 +114,20 @@ class _IosSettingsPageState extends State<IosSettingsPage> {
final (_, err) = await context.showLoadingDialog(
fn: () async {
await wc.updateApplicationContext({'urls': result});
final data = {'urls': result};
// Try realtime update (app must be running foreground).
try {
if (await wc.isReachable) {
await wc.sendMessage(data);
return;
}
} catch (e) {
Loggers.app.warning('Failed to send message to watch', e);
}
// fallback
await wc.updateApplicationContext(data);
},
);
if (err == null) {

View File

@@ -4,10 +4,7 @@ extension _Init on SSHPageState {
void _initStoredCfg() {
final fontFamilly = Stores.setting.fontPath.fetch().getFileName();
final textSize = Stores.setting.termFontSize.fetch();
final textStyle = TextStyle(
fontFamily: fontFamilly,
fontSize: textSize,
);
final textStyle = TextStyle(fontFamily: fontFamilly, fontSize: textSize);
_terminalStyle = TerminalStyle.fromTextStyle(textStyle);
}
@@ -37,15 +34,12 @@ extension _Init on SSHPageState {
onStatus: (p0) {
_writeLn(p0.toString());
},
onKeyboardInteractive: _onKeyboardInteractive,
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(widget.args.spi, ctx: context),
);
_writeLn('${libL10n.execute}: Shell');
final session = await _client?.shell(
pty: SSHPtyConfig(
width: _terminal.viewWidth,
height: _terminal.viewHeight,
),
pty: SSHPtyConfig(width: _terminal.viewWidth, height: _terminal.viewHeight),
environment: widget.args.spi.envs,
);
@@ -98,30 +92,30 @@ extension _Init on SSHPageState {
return;
}
stream.cast<List<int>>().transform(const Utf8Decoder()).listen(
_terminal.write,
onError: (Object error, StackTrace stack) {
// _terminal.write('Stream error: $error\n');
Loggers.root.warning('Error in SSH stream', error, stack);
},
cancelOnError: false,
);
stream
.cast<List<int>>()
.transform(const Utf8Decoder())
.listen(
_terminal.write,
onError: (Object error, StackTrace stack) {
// _terminal.write('Stream error: $error\n');
Loggers.root.warning('Error in SSH stream', error, stack);
},
cancelOnError: false,
);
}
void _setupDiscontinuityTimer() {
_discontinuityTimer = Timer.periodic(
const Duration(seconds: 5),
(_) async {
var throwTimeout = true;
Future.delayed(const Duration(seconds: 3), () {
if (throwTimeout) {
_catchTimeout();
}
});
await _client?.ping();
throwTimeout = false;
},
);
_discontinuityTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
var throwTimeout = true;
Future.delayed(const Duration(seconds: 3), () {
if (throwTimeout) {
_catchTimeout();
}
});
await _client?.ping();
throwTimeout = false;
});
}
void _catchTimeout() {

View File

@@ -13,6 +13,13 @@ extension _Keyboard on SSHPageState {
_handleEscKeyOrBackButton();
return true; // Mark as handled so it doesn't propagate
}
if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
event.logicalKey == LogicalKeyboardKey.shiftRight) {
// Handle shift key press
_terminal.keyInput(TerminalKey.shift);
HapticFeedback.lightImpact();
return true;
}
}
return false; // Let other handlers process this event
}

View File

@@ -291,6 +291,9 @@ class SSHPageState extends State<SSHPage>
case TerminalKey.alt:
selected = _keyboard.alt;
break;
case TerminalKey.shift:
selected = _keyboard.shift;
break;
default:
break;
}

View File

@@ -26,6 +26,9 @@ extension _VirtKey on SSHPageState {
case TerminalKey.alt:
_keyboard.alt = !_keyboard.alt;
break;
case TerminalKey.shift:
_keyboard.shift = !_keyboard.shift;
break;
default:
_terminal.keyInput(key);
break;
@@ -161,8 +164,4 @@ extension _VirtKey on SSHPageState {
}
}
}
FutureOr<List<String>?> _onKeyboardInteractive(SSHUserInfoRequest req) {
return KeybordInteractive.defaultHandle(widget.args.spi, ctx: context);
}
}

View File

@@ -73,11 +73,24 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
},
icon: const Icon(Icons.add),
),
if (!isMobile)
IconButton(
icon: const Icon(Icons.refresh),
tooltip: MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
onPressed: () => setState(() {}),
),
if (!isPickFile) _buildMissionBtn(),
_buildSortBtn(),
],
),
body: _sortType.listen(_buildBody),
body: isMobile
? RefreshIndicator(
onRefresh: () async {
setState(() {});
},
child: _sortType.listen(_buildBody),
)
: _sortType.listen(_buildBody),
);
}

View File

@@ -537,7 +537,7 @@ extension _Actions on _SftpPageState {
/// Local file dir + server id + remote path
String _getLocalPath(String remotePath) {
return Paths.file.joinPath(widget.args.spi.id).joinPath(remotePath);
return Paths.file.joinPath(widget.args.spi.oldId).joinPath(remotePath);
}
/// Only return true if the path is changed

View File

@@ -53,7 +53,7 @@ final class _SystemdPageState extends State<SystemdPage> {
(isBusy) => AnimatedContainer(
duration: Durations.medium1,
curve: Curves.fastEaseInToSlowEaseOut,
height: isBusy ? 30 : 0,
height: isBusy ? SizedLoading.medium.size : 0,
child: isBusy
? SizedLoading.medium
: const SizedBox.shrink(),

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <gtk/gtk_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@@ -16,6 +17,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_linux
gtk
screen_retriever_linux
url_launcher_linux

View File

@@ -8,6 +8,7 @@ import Foundation
import app_links
import dynamic_color
import file_picker
import flutter_secure_storage_macos
import icloud_storage
import local_auth_darwin
import package_info_plus
@@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin"))
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View File

@@ -5,6 +5,8 @@ PODS:
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- flutter_secure_storage_macos (6.1.3):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- icloud_storage (0.0.1):
- FlutterMacOS
@@ -34,6 +36,7 @@ DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- icloud_storage (from `Flutter/ephemeral/.symlinks/plugins/icloud_storage/macos`)
- local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`)
@@ -53,6 +56,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral
icloud_storage:
@@ -80,6 +85,7 @@ SPEC CHECKSUMS:
app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d
dynamic_color: b820c000cc68df65e7ba7ff177cb98404ce56651
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
icloud_storage: eb5b0f20687cf5a4fabc0b541f3b079cd6df7dcb
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391

View File

@@ -471,7 +471,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
@@ -481,7 +481,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -608,7 +608,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
@@ -618,7 +618,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -638,7 +638,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1189;
CURRENT_PROJECT_VERSION = 1201;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
INFOPLIST_FILE = Runner/Info.plist;
@@ -649,7 +649,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0.1189;
MARKETING_VERSION = 1.0.1201;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "Server Box";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -26,5 +26,7 @@
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

View File

@@ -24,5 +24,7 @@
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

View File

@@ -324,7 +324,7 @@ packages:
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
@@ -497,8 +497,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "v1.0.321"
resolved-ref: e0b3338be10fa71c96d017d873f5e10bb4374709
ref: "v1.0.327"
resolved-ref: "5075a679b814b10742f967066858ba4df92ea4ae"
url: "https://github.com/lppcg/fl_lib"
source: git
version: "0.0.1"
@@ -584,6 +584,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_secure_storage:
dependency: transitive
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_svg:
dependency: transitive
description:
@@ -790,10 +838,10 @@ packages:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.6.7"
json_annotation:
dependency: "direct main"
description:
@@ -1159,18 +1207,18 @@ packages:
dependency: transitive
description:
name: qr_code_dart_decoder
sha256: "6da7eda27726d504bed3c30eabf78ddca3eb9265e1c8dc49b30ef5974b9c267f"
sha256: "4044f13a071da6102f7e9bc44a6b1ce577604d7846bcbeb1be412a137b825017"
url: "https://pub.dev"
source: hosted
version: "0.0.5"
version: "0.1.2"
qr_code_dart_scan:
dependency: transitive
description:
name: qr_code_dart_scan
sha256: "6e1aab64b8f5f768416b471dbc3fb0fc94969c3e236157a96b52a70f9fe12ebb"
sha256: "8c9a63dac44ea51c82e72c0fed28b850d22be26348c582f77486f128cf1c2760"
url: "https://pub.dev"
source: hosted
version: "0.10.1"
version: "0.11.1"
quiver:
dependency: transitive
description:
@@ -1806,10 +1854,10 @@ packages:
dependency: transitive
description:
name: zxing_lib
sha256: d5d81917be2e18b06a2cf4ca12927f3ab957dfbd25bd7b8175b3e9a0ce5c2e2b
sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.1.4"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.1"

View File

@@ -1,7 +1,7 @@
name: server_box
description: server status & toolbox app.
publish_to: "none"
version: 1.0.1189+1189
version: 1.0.1201+1201
environment:
sdk: ">=3.8.0"
@@ -12,29 +12,30 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
hive_ce_flutter: ^2.3.1
choice: ^2.3.2
crypto: ^3.0.6
dio: ^5.2.1
easy_isolate: ^1.3.0
intl: ^0.20.2
highlight: ^0.7.0
flutter_highlight: ^0.7.0
re_editor: ^0.7.0
shared_preferences: ^2.1.1
dynamic_color: ^1.6.6
xml: ^6.4.2 # for parsing nvidia-smi
flutter_displaymode: ^0.6.0
fl_chart: ^1.0.0
wakelock_plus: ^1.2.4
wake_on_lan: ^4.1.1+3
easy_isolate: ^1.3.0
extended_image: ^10.0.0
file_picker: ^10.1.9
json_annotation: ^4.9.0
choice: ^2.3.2
webdav_client_plus: ^1.0.2
freezed_annotation: ^3.0.0
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
flutter_highlight: ^0.7.0
flutter_displaymode: ^0.6.0
fl_chart: ^1.0.0
freezed_annotation: ^3.0.0
highlight: ^0.7.0
hive_ce_flutter: ^2.3.1
intl: ^0.20.2
json_annotation: ^4.9.0
responsive_framework: ^1.5.1
re_editor: ^0.7.0
riverpod_annotation: ^2.6.1
shared_preferences: ^2.1.1
wakelock_plus: ^1.2.4
wake_on_lan: ^4.1.1+3
webdav_client_plus: ^1.0.2
xml: ^6.4.2 # for parsing nvidia-smi
dartssh2:
git:
url: https://github.com/lollipopkit/dartssh2
@@ -62,7 +63,7 @@ dependencies:
fl_lib:
git:
url: https://github.com/lppcg/fl_lib
ref: v1.0.321
ref: v1.0.327
dependency_overrides:
# webdav_client_plus:

412
test/amd_smi_test.dart Normal file
View File

@@ -0,0 +1,412 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/data/model/server/amd.dart';
const _amdSmiRaw = '''
[
{
"name": "AMD Radeon RX 7900 XTX",
"device_id": "0",
"temp": 45,
"power_draw": 120,
"power_cap": 355,
"memory": {
"total": 24576,
"used": 1024,
"unit": "MB",
"processes": [
{
"pid": 2456,
"name": "firefox",
"memory": 512
},
{
"pid": 3784,
"name": "blender",
"memory": 256
}
]
},
"utilization": 75,
"fan_speed": 1200,
"clock_speed": 2400
},
{
"name": "AMD Radeon RX 6800 XT",
"device_id": "1",
"temp": 38,
"power_draw": 85,
"power_cap": 300,
"memory": {
"total": 16384,
"used": 512,
"unit": "MB",
"processes": []
},
"utilization": 25,
"fan_speed": 800,
"clock_speed": 2100
}
]
''';
const _amdSmiRocmRaw = '''
[
{
"card_model": "AMD Radeon RX 6700 XT",
"gpu_id": "card0",
"temperature": "42°C",
"power_draw": "95",
"power_cap": "230",
"vram": {
"total_memory": 12288,
"used_memory": 768,
"unit": "MiB",
"processes": [
{
"pid": 1234,
"process_name": "game.exe",
"used_memory": 512
}
]
},
"gpu_util": "60%",
"fan_rpm": "950 RPM",
"sclk": "1800MHz"
}
]
''';
const _amdSmiAlternativeRaw = '''
[
{
"device_name": "Radeon RX 580",
"gpu_temp": 55,
"current_power": 150,
"power_limit": 185,
"memory": {
"total": 8192,
"used": 2048,
"unit": "MB"
},
"activity": 90,
"fan_speed": 1500,
"gpu_clock": 1366
}
]
''';
const _amdSmiEdgeCasesRaw = '''
[
{
"name": "Unknown AMD GPU",
"device_id": "",
"temp": null,
"power": null,
"memory": {},
"utilization": null,
"fan_speed": null,
"clock_speed": null
},
{
"name": "AMD Test GPU",
"device_id": "test",
"temp": "50°C",
"power_draw": 100,
"memory": {
"total": "16384MB",
"used": "2048MB"
},
"utilization": "80%",
"fan_speed": "1100 RPM",
"clock_speed": "2000 MHz"
}
]
''';
const _invalidJson = '''
{
"invalid": "not an array"
}
''';
const _emptyArray = '[]';
const _malformedJson = '''
[
{
"name": "Test GPU"
// missing closing brace
''';
void main() {
group('AmdSmi JSON parsing', () {
test('parse standard AMD SMI output', () {
final gpus = AmdSmi.fromJson(_amdSmiRaw);
expect(gpus.length, 2);
final gpu1 = gpus[0];
expect(gpu1.name, 'AMD Radeon RX 7900 XTX');
expect(gpu1.deviceId, '0');
expect(gpu1.temp, 45);
expect(gpu1.power, '120W / 355W');
expect(gpu1.memory.total, 24576);
expect(gpu1.memory.used, 1024);
expect(gpu1.memory.unit, 'MB');
expect(gpu1.memory.processes.length, 2);
expect(gpu1.memory.processes[0].pid, 2456);
expect(gpu1.memory.processes[0].name, 'firefox');
expect(gpu1.memory.processes[0].memory, 512);
expect(gpu1.memory.processes[1].pid, 3784);
expect(gpu1.memory.processes[1].name, 'blender');
expect(gpu1.memory.processes[1].memory, 256);
expect(gpu1.utilization, 75);
expect(gpu1.fanSpeed, 1200);
expect(gpu1.clockSpeed, 2400);
final gpu2 = gpus[1];
expect(gpu2.name, 'AMD Radeon RX 6800 XT');
expect(gpu2.deviceId, '1');
expect(gpu2.temp, 38);
expect(gpu2.power, '85W / 300W');
expect(gpu2.memory.total, 16384);
expect(gpu2.memory.used, 512);
expect(gpu2.memory.unit, 'MB');
expect(gpu2.memory.processes.length, 0);
expect(gpu2.utilization, 25);
expect(gpu2.fanSpeed, 800);
expect(gpu2.clockSpeed, 2100);
});
test('parse ROCm SMI output with different field names', () {
final gpus = AmdSmi.fromJson(_amdSmiRocmRaw);
expect(gpus.length, 1);
final gpu = gpus[0];
expect(gpu.name, 'AMD Radeon RX 6700 XT');
expect(gpu.deviceId, 'card0');
expect(gpu.temp, 42);
expect(gpu.power, '95W / 230W');
expect(gpu.memory.total, 12288);
expect(gpu.memory.used, 768);
expect(gpu.memory.unit, 'MiB');
expect(gpu.memory.processes.length, 1);
expect(gpu.memory.processes[0].pid, 1234);
expect(gpu.memory.processes[0].name, 'game.exe');
expect(gpu.memory.processes[0].memory, 512);
expect(gpu.utilization, 60);
expect(gpu.fanSpeed, 950);
expect(gpu.clockSpeed, 1800);
});
test('parse alternative field names', () {
final gpus = AmdSmi.fromJson(_amdSmiAlternativeRaw);
expect(gpus.length, 1);
final gpu = gpus[0];
expect(gpu.name, 'Radeon RX 580');
expect(gpu.deviceId, '0');
expect(gpu.temp, 55);
expect(gpu.power, '150W / 185W');
expect(gpu.memory.total, 8192);
expect(gpu.memory.used, 2048);
expect(gpu.memory.unit, 'MB');
expect(gpu.memory.processes.length, 0);
expect(gpu.utilization, 90);
expect(gpu.fanSpeed, 1500);
expect(gpu.clockSpeed, 1366);
});
test('handle edge cases and string parsing', () {
final gpus = AmdSmi.fromJson(_amdSmiEdgeCasesRaw);
expect(gpus.length, 2);
final gpu1 = gpus[0];
expect(gpu1.name, 'Unknown AMD GPU');
expect(gpu1.deviceId, '');
expect(gpu1.temp, 0);
expect(gpu1.power, 'N/A');
expect(gpu1.memory.total, 0);
expect(gpu1.memory.used, 0);
expect(gpu1.memory.unit, 'MB');
expect(gpu1.memory.processes.length, 0);
expect(gpu1.utilization, 0);
expect(gpu1.fanSpeed, 0);
expect(gpu1.clockSpeed, 0);
final gpu2 = gpus[1];
expect(gpu2.name, 'AMD Test GPU');
expect(gpu2.deviceId, 'test');
expect(gpu2.temp, 50);
expect(gpu2.power, '100W');
expect(gpu2.memory.total, 16384);
expect(gpu2.memory.used, 2048);
expect(gpu2.memory.unit, 'MB');
expect(gpu2.utilization, 80);
expect(gpu2.fanSpeed, 1100);
expect(gpu2.clockSpeed, 2000);
});
test('handle invalid JSON gracefully', () {
final gpus1 = AmdSmi.fromJson(_invalidJson);
expect(gpus1.length, 0);
final gpus2 = AmdSmi.fromJson(_malformedJson);
expect(gpus2.length, 0);
final gpus3 = AmdSmi.fromJson('invalid json');
expect(gpus3.length, 0);
});
test('handle empty array', () {
final gpus = AmdSmi.fromJson(_emptyArray);
expect(gpus.length, 0);
});
});
group('AmdSmi helper methods', () {
test('_parseIntValue handles various input types', () {
expect(AmdSmi.fromJson('[{"name":"test","temp":42}]')[0].temp, 42);
expect(AmdSmi.fromJson('[{"name":"test","temp":"45°C"}]')[0].temp, 45);
expect(AmdSmi.fromJson('[{"name":"test","temp":"1200 RPM"}]')[0].temp, 1200);
expect(AmdSmi.fromJson('[{"name":"test","temp":"N/A"}]')[0].temp, 0);
expect(AmdSmi.fromJson('[{"name":"test","temp":null}]')[0].temp, 0);
});
test('_formatPower handles different power scenarios', () {
final gpu1 = AmdSmi.fromJson('[{"name":"test","power_draw":100,"power_cap":200}]')[0];
expect(gpu1.power, '100W / 200W');
final gpu2 = AmdSmi.fromJson('[{"name":"test","power_draw":50}]')[0];
expect(gpu2.power, '50W');
final gpu3 = AmdSmi.fromJson('[{"name":"test"}]')[0];
expect(gpu3.power, 'N/A');
});
test('_parseMemory handles missing memory data', () {
final gpu = AmdSmi.fromJson('[{"name":"test"}]')[0];
expect(gpu.memory.total, 0);
expect(gpu.memory.used, 0);
expect(gpu.memory.unit, 'MB');
expect(gpu.memory.processes.length, 0);
});
test('_parseProcess filters invalid processes', () {
const jsonWithInvalidProcess = '''
[
{
"name": "Test GPU",
"memory": {
"processes": [
{
"pid": 0,
"name": "invalid",
"memory": 100
},
{
"pid": 1234,
"name": "valid",
"memory": 200
}
]
}
}
]
''';
final gpu = AmdSmi.fromJson(jsonWithInvalidProcess)[0];
expect(gpu.memory.processes.length, 1);
expect(gpu.memory.processes[0].pid, 1234);
expect(gpu.memory.processes[0].name, 'valid');
});
});
group('AmdSmi data classes', () {
test('AmdSmiItem toString', () {
final memory = AmdSmiMem(8192, 2048, 'MB', []);
final item = AmdSmiItem(
deviceId: '0',
name: 'Test GPU',
temp: 45,
power: '100W / 200W',
memory: memory,
utilization: 75,
fanSpeed: 1200,
clockSpeed: 2400,
);
final toString = item.toString();
expect(toString, contains('Test GPU'));
expect(toString, contains('45'));
expect(toString, contains('100W / 200W'));
expect(toString, contains('75%'));
});
test('AmdSmiMem toString', () {
final process = AmdSmiMemProcess(1234, 'test', 512);
final memory = AmdSmiMem(8192, 2048, 'MB', [process]);
final toString = memory.toString();
expect(toString, contains('8192'));
expect(toString, contains('2048'));
expect(toString, contains('MB'));
expect(toString, contains('1'));
});
test('AmdSmiMemProcess toString', () {
final process = AmdSmiMemProcess(1234, 'firefox', 512);
final toString = process.toString();
expect(toString, contains('1234'));
expect(toString, contains('firefox'));
expect(toString, contains('512'));
});
});
group('AmdSmi robustness', () {
test('handles malformed GPU objects gracefully', () {
const malformedGpuJson = '''
[
{
"name": "Valid GPU",
"temp": 45
},
{
"malformed": true
},
{
"name": "Another Valid GPU",
"temp": 50
}
]
''';
final gpus = AmdSmi.fromJson(malformedGpuJson);
expect(gpus.length, 3);
expect(gpus[0].name, 'Valid GPU');
expect(gpus[0].temp, 45);
expect(gpus[1].name, 'Unknown AMD GPU');
expect(gpus[1].temp, 0);
expect(gpus[2].name, 'Another Valid GPU');
expect(gpus[2].temp, 50);
});
test('handles missing required fields with defaults', () {
const minimalGpuJson = '''
[
{}
]
''';
final gpus = AmdSmi.fromJson(minimalGpuJson);
expect(gpus.length, 1);
expect(gpus[0].name, 'Unknown AMD GPU');
expect(gpus[0].deviceId, '0');
expect(gpus[0].temp, 0);
expect(gpus[0].power, 'N/A');
expect(gpus[0].utilization, 0);
expect(gpus[0].fanSpeed, 0);
expect(gpus[0].clockSpeed, 0);
});
});
}

View File

@@ -8,6 +8,7 @@
#include <app_links/app_links_plugin_c_api.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <local_auth_windows/local_auth_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
@@ -19,6 +20,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
dynamic_color
flutter_secure_storage_windows
local_auth_windows
screen_retriever_windows
share_plus