mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 04:34:34 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a12bc844 | ||
|
|
8d597294a4 | ||
|
|
682a6e4f2d | ||
|
|
8c3302cf0d | ||
|
|
ec4bf3df24 | ||
|
|
263d4eabb4 | ||
|
|
c6439673b8 | ||
|
|
a35d21981b | ||
|
|
dbc873c0c0 | ||
|
|
e69808a2f6 | ||
|
|
55b3ba63ec | ||
|
|
006e66d825 |
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
|
||||
</full-backup-content>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
198
lib/data/model/app/bak/backup_service.dart
Normal file
198
lib/data/model/app/bak/backup_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
62
lib/data/model/app/bak/backup_source.dart
Normal file
62
lib/data/model/app/bak/backup_source.dart
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
188
lib/data/model/server/amd.dart
Normal file
188
lib/data/model/server/amd.dart
Normal 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}';
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ abstract final class GithubIds {
|
||||
'rhwong',
|
||||
'AstroEngineeer',
|
||||
'mochasweet',
|
||||
'back-lacking',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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` に書き込まれます。スクリプトの内容を確認できます。';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||
}
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||
}
|
||||
|
||||
@@ -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` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
||||
"write": "書き込み",
|
||||
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
||||
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが ~/.config/server_box に書き込まれます。スクリプトの内容を確認できます。"
|
||||
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
|
||||
}
|
||||
@@ -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` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
||||
}
|
||||
@@ -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` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
21
lib/view/page/server/logo.dart
Normal file
21
lib/view/page/server/logo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_linux
|
||||
gtk
|
||||
screen_retriever_linux
|
||||
url_launcher_linux
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -26,5 +26,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -24,5 +24,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
70
pubspec.lock
70
pubspec.lock
@@ -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"
|
||||
|
||||
39
pubspec.yaml
39
pubspec.yaml
@@ -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
412
test/amd_smi_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user