From 3c592baf2c1b6c241b2d4ae3952f4fb2d6a66c0c Mon Sep 17 00:00:00 2001 From: GT610 <79314033+GT-610@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:27:58 +0800 Subject: [PATCH] fix: Use latest `dartssh2` and add a switch for temperature between celsius and millicelsius (#1095) * refactor(sftp): Optimize file download logic and SSH command execution handling Replace manual chunked downloads with a more concise `sftp.download` method Consistently use `utf8.decode` to process SSH command output Remove redundant code and comments, and simplify the logic * chore: Update `dartssh2` submodule * feat (Temperature Display): Added an option to switch between degrees Celsius and millicelsius Allows users to switch temperature units in server settings, resolving the issue of incorrect temperature display on some devices * chore: Add a participnt * fix(sftp): Fixed a resource leak issue during file downloads and SSH command execution Ensured that remote and local file handles are properly closed during file downloads to prevent resource leaks. Additionally, improved error handling during SSH command execution to ensure that all streams are either successfully completed or properly handled in the event of an error. --- lib/core/extension/ssh_client.dart | 114 ++++++++++-------- lib/data/model/server/custom.dart | 5 + lib/data/model/server/custom.g.dart | 2 + .../server/server_status_update_req.dart | 9 +- lib/data/model/server/temp.dart | 4 +- lib/data/model/sftp/worker.dart | 60 ++++----- lib/data/provider/private_key.g.dart | 2 +- lib/data/provider/server/all.g.dart | 2 +- lib/data/provider/server/single.dart | 1 + lib/data/provider/server/single.g.dart | 2 +- lib/data/provider/snippet.g.dart | 2 +- lib/data/res/github_id.dart | 3 +- lib/generated/l10n/l10n.dart | 6 + lib/generated/l10n/l10n_de.dart | 4 + lib/generated/l10n/l10n_en.dart | 4 + lib/generated/l10n/l10n_es.dart | 4 + lib/generated/l10n/l10n_fr.dart | 4 + lib/generated/l10n/l10n_id.dart | 4 + lib/generated/l10n/l10n_it.dart | 4 + lib/generated/l10n/l10n_ja.dart | 4 + lib/generated/l10n/l10n_ko.dart | 4 + lib/generated/l10n/l10n_nl.dart | 4 + lib/generated/l10n/l10n_pt.dart | 4 + lib/generated/l10n/l10n_ru.dart | 4 + lib/generated/l10n/l10n_tr.dart | 4 + lib/generated/l10n/l10n_uk.dart | 4 + lib/generated/l10n/l10n_zh.dart | 4 + lib/hive/hive_adapters.g.dart | 7 +- lib/hive/hive_adapters.g.yaml | 4 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/view/page/server/edit/actions.dart | 2 + lib/view/page/server/edit/edit.dart | 2 + lib/view/page/server/edit/widget.dart | 12 ++ packages/dartssh2 | 2 +- pubspec.lock | 14 +-- 36 files changed, 211 insertions(+), 102 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 09bc2bff..b74bcf0c 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -1,22 +1,22 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'package:dartssh2/dartssh2.dart'; -import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/widgets.dart'; import 'package:server_box/data/helper/ssh_decoder.dart'; import 'package:server_box/data/model/server/system.dart'; typedef OnStdout = void Function(String data, SSHSession session); +typedef OnStderr = void Function(String data, SSHSession session); typedef OnStdin = void Function(SSHSession session); extension SSHClientX on SSHClient { - /// Create a persistent PowerShell session for Windows commands Future<(SSHSession, String)> execPowerShell( OnStdin onStdin, { SSHPtyConfig? pty, OnStdout? onStdout, - OnStdout? onStderr, + void Function(String data)? onStderr, bool stdout = true, bool stderr = true, Map? env, @@ -33,20 +33,24 @@ extension SSHClientX on SSHClient { session.stdout.listen( (e) { - onStdout?.call(e.string, session); + onStdout?.call(utf8.decode(e), session); if (stdout) result.add(e); }, onDone: stdoutDone.complete, - onError: stderrDone.completeError, + onError: (e) { + if (!stdoutDone.isCompleted) stdoutDone.completeError(e); + }, ); session.stderr.listen( (e) { - onStderr?.call(e.string, session); - // Don't add stderr to result, only stdout + onStderr?.call(utf8.decode(e)); + if (stderr) result.add(e); }, onDone: stderrDone.complete, - onError: stderrDone.completeError, + onError: (e) { + if (!stderrDone.isCompleted) stderrDone.completeError(e); + }, ); onStdin(session); @@ -54,7 +58,7 @@ extension SSHClientX on SSHClient { await stdoutDone.future; await stderrDone.future; - return (session, result.takeBytes().string); + return (session, utf8.decode(result.takeBytes())); } Future<(SSHSession, String)> exec( @@ -62,7 +66,7 @@ extension SSHClientX on SSHClient { String? entry, SSHPtyConfig? pty, OnStdout? onStdout, - OnStdout? onStderr, + OnStderr? onStderr, bool stdout = true, bool stderr = true, Map? env, @@ -84,16 +88,16 @@ extension SSHClientX on SSHClient { session.stdout.listen( (e) { - onStdout?.call(e.string, session); + onStdout?.call(utf8.decode(e), session); if (stdout) result.add(e); }, onDone: stdoutDone.complete, - onError: stderrDone.completeError, + onError: stdoutDone.completeError, ); session.stderr.listen( (e) { - onStderr?.call(e.string, session); + onStderr?.call(utf8.decode(e), session); if (stderr) result.add(e); }, onDone: stderrDone.complete, @@ -105,39 +109,26 @@ extension SSHClientX on SSHClient { await stdoutDone.future; await stderrDone.future; - return (session, result.takeBytes().string); + return (session, utf8.decode(result.takeBytes())); } - /// Executes a command with password error detection. - /// - /// This method is used for executing commands where password has already been - /// handled beforehand (e.g., via base64 pipe in container commands). - /// It captures stderr via [onStderr] callback to detect sudo password errors - /// (e.g., "Sorry, try again.", "incorrect password attempt", or - /// "a password is required"), while excluding stderr from the returned - /// output via [stderr: false]. - /// - /// Returns exitCode: - /// - 0: success - /// - 1: general error - /// - 2: sudo password error Future<(int?, String)> execWithPwd( String script, { String? entry, BuildContext? context, OnStdout? onStdout, - OnStdout? onStderr, + OnStderr? onStderr, required String id, }) async { var hasPasswordError = false; final (session, output) = await exec( (sess) { - sess.stdin.add('$script\n'.uint8List); + sess.stdin.add(Uint8List.fromList(utf8.encode('$script\n'))); sess.stdin.close(); }, - onStderr: (data, session) async { - onStderr?.call(data, session); + onStderr: (data, sess) { + onStderr?.call(data, sess); if (data.contains('Sorry, try again.') || data.contains('incorrect password attempt') || data.contains('a password is required')) { @@ -163,33 +154,52 @@ extension SSHClientX on SSHClient { String? entry, Map? env, }) async { - final ret = await exec( - (session) { - session.stdin.add('$script\n'.uint8List); - session.stdin.close(); - }, + final session = await execute( + entry ?? 'cat | sh', pty: pty, - env: env, - stdout: stdout, - stderr: stderr, - entry: entry, + environment: env, ); - return ret.$2; + + final result = BytesBuilder(copy: false); + final stdoutDone = Completer(); + final stderrDone = Completer(); + + session.stdout.listen( + (e) { + if (stdout) result.add(e); + }, + onDone: stdoutDone.complete, + onError: (e) { + if (!stdoutDone.isCompleted) stdoutDone.completeError(e); + }, + ); + + session.stderr.listen( + (e) { + if (stderr) result.add(e); + }, + onDone: stderrDone.complete, + onError: (e) { + if (!stderrDone.isCompleted) stderrDone.completeError(e); + }, + ); + + session.stdin.add(Uint8List.fromList(utf8.encode('$script\n'))); + session.stdin.close(); + + await stdoutDone.future; + await stderrDone.future; + + return utf8.decode(result.takeBytes()); } - /// Runs a command and decodes output safely with encoding fallback - /// - /// [systemType] - The system type (affects encoding choice) - /// Runs a command and safely decodes the result Future runSafe( String command, { SystemType? systemType, String? context, }) async { - // Let SSH errors propagate with their original type (e.g., SSHError subclasses) final result = await run(command); - - // Only catch decoding failures and add context + try { return SSHDecoder.decode( result, @@ -198,12 +208,11 @@ extension SSHClientX on SSHClient { ); } on FormatException catch (e) { throw Exception( - 'Failed to decode command output${context != null ? " [$context]" : ""}: $e', + 'Failed to decode command output${context != null ? ' [$context]' : ''}: $e', ); } } - /// Executes a command with stdin and safely decodes stdout/stderr Future<(String stdout, String stderr)> execSafe( void Function(SSHSession session) callback, { required String entry, @@ -241,7 +250,6 @@ extension SSHClientX on SSHClient { final stdoutBytes = stdoutBuilder.takeBytes(); final stderrBytes = stderrBuilder.takeBytes(); - // Only catch decoding failures, let other errors propagate String stdout; try { stdout = SSHDecoder.decode( @@ -251,7 +259,7 @@ extension SSHClientX on SSHClient { ); } on FormatException catch (e) { throw Exception( - 'Failed to decode stdout${context != null ? " [$context]" : ""}: $e', + 'Failed to decode stdout${context != null ? ' [$context]' : ''}: $e', ); } @@ -264,7 +272,7 @@ extension SSHClientX on SSHClient { ); } on FormatException catch (e) { throw Exception( - 'Failed to decode stderr${context != null ? " [$context]" : ""}: $e', + 'Failed to decode stderr${context != null ? ' [$context]' : ''}: $e', ); } diff --git a/lib/data/model/server/custom.dart b/lib/data/model/server/custom.dart index 10ec85ae..a0eb46a4 100644 --- a/lib/data/model/server/custom.dart +++ b/lib/data/model/server/custom.dart @@ -18,6 +18,8 @@ final class ServerCustom { final String? preferTempDev; + final bool tempIsCelsius; + final String? logoUrl; /// The device name of the network interface displayed in the home server card. @@ -33,6 +35,7 @@ final class ServerCustom { this.pvePwd, this.cmds, this.preferTempDev, + this.tempIsCelsius = false, this.logoUrl, this.netDev, this.scriptDir, @@ -51,6 +54,7 @@ final class ServerCustom { other.pvePwd == pvePwd && other.cmds == cmds && other.preferTempDev == preferTempDev && + other.tempIsCelsius == tempIsCelsius && other.logoUrl == logoUrl && other.netDev == netDev && other.scriptDir == scriptDir; @@ -64,6 +68,7 @@ final class ServerCustom { pvePwd.hashCode ^ cmds.hashCode ^ preferTempDev.hashCode ^ + tempIsCelsius.hashCode ^ logoUrl.hashCode ^ netDev.hashCode ^ scriptDir.hashCode; diff --git a/lib/data/model/server/custom.g.dart b/lib/data/model/server/custom.g.dart index 1619d62d..144796ea 100644 --- a/lib/data/model/server/custom.g.dart +++ b/lib/data/model/server/custom.g.dart @@ -14,6 +14,7 @@ ServerCustom _$ServerCustomFromJson(Map json) => ServerCustom( (k, e) => MapEntry(k, e as String), ), preferTempDev: json['preferTempDev'] as String?, + tempIsCelsius: json['tempIsCelsius'] as bool? ?? false, logoUrl: json['logoUrl'] as String?, netDev: json['netDev'] as String?, scriptDir: json['scriptDir'] as String?, @@ -26,6 +27,7 @@ Map _$ServerCustomToJson(ServerCustom instance) => 'pvePwd': ?instance.pvePwd, 'cmds': ?instance.cmds, 'preferTempDev': ?instance.preferTempDev, + 'tempIsCelsius': instance.tempIsCelsius, 'logoUrl': ?instance.logoUrl, 'netDev': ?instance.netDev, 'scriptDir': ?instance.scriptDir, diff --git a/lib/data/model/server/server_status_update_req.dart b/lib/data/model/server/server_status_update_req.dart index 367feca9..a74cc6e2 100644 --- a/lib/data/model/server/server_status_update_req.dart +++ b/lib/data/model/server/server_status_update_req.dart @@ -23,12 +23,14 @@ class ServerStatusUpdateReq { final Map parsedOutput; final SystemType system; final Map customCmds; + final double tempDivisor; const ServerStatusUpdateReq({ required this.system, required this.ss, required this.parsedOutput, required this.customCmds, + this.tempDivisor = 1000.0, }); } @@ -88,6 +90,7 @@ Future _getLinuxStatus(ServerStatusUpdateReq req) async { req.ss.temps.parse( StatusCmdType.tempType.findInMap(parsedOutput), StatusCmdType.tempVal.findInMap(parsedOutput), + divisor: req.tempDivisor, ); } catch (e, s) { Loggers.app.warning(e, s); @@ -495,7 +498,7 @@ void _parseWindowsTemperatureData(ServerStatusUpdateReq req, Map try { final tempRaw = WindowsStatusCmdType.temp.findInMap(parsedOutput); if (tempRaw.isNotEmpty && tempRaw != 'null') { - _parseWindowsTemperatures(req.ss.temps, tempRaw); + _parseWindowsTemperatures(req.ss.temps, tempRaw, divisor: req.tempDivisor); } } catch (e, s) { Loggers.app.warning('Windows temperature parsing failed: $e', s); @@ -648,7 +651,7 @@ List _parseWindowsDiskIO(String raw, int currentTime) { } } -void _parseWindowsTemperatures(Temperatures temps, String raw) { +void _parseWindowsTemperatures(Temperatures temps, String raw, {double divisor = 1000.0}) { try { // Handle error output if (raw.contains('Error') || raw.contains('Exception') || raw.contains('The term')) { @@ -677,7 +680,7 @@ void _parseWindowsTemperatures(Temperatures temps, String raw) { } if (typeLines.isNotEmpty && valueLines.isNotEmpty) { - temps.parse(typeLines.join('\n'), valueLines.join('\n')); + temps.parse(typeLines.join('\n'), valueLines.join('\n'), divisor: divisor); } } catch (e, s) { Loggers.app.warning('Failed to parse Windows temperature data', e, s); diff --git a/lib/data/model/server/temp.dart b/lib/data/model/server/temp.dart index c31b3b5b..b956ac5e 100644 --- a/lib/data/model/server/temp.dart +++ b/lib/data/model/server/temp.dart @@ -1,7 +1,7 @@ class Temperatures { final Map _map = {}; - void parse(String type, String value) { + void parse(String type, String value, {double divisor = 1000.0}) { final typeSplited = type.split('\n'); final valueSplited = value.split('\n'); for (var i = 0; i < typeSplited.length && i < valueSplited.length; i++) { @@ -15,7 +15,7 @@ class Temperatures { if (temp == null) { continue; } - _map[name] = temp / 1000; + _map[name] = temp / divisor; } } diff --git a/lib/data/model/sftp/worker.dart b/lib/data/model/sftp/worker.dart index fe2a88c3..62e3651c 100644 --- a/lib/data/model/sftp/worker.dart +++ b/lib/data/model/sftp/worker.dart @@ -84,48 +84,52 @@ Future _download( ); mainSendPort.send(SftpWorkerStatus.sshConnectted); - /// Create the directory if not exists final dirPath = req.localPath.substring( 0, req.localPath.lastIndexOf(Pfs.seperator), ); await Directory(dirPath).create(recursive: true); - /// Use [FileMode.write] to overwrite the file - final localFile = File(req.localPath).openWrite(mode: FileMode.write); - final file = await (await client.sftp()).open(req.remotePath); - final size = (await file.stat()).size; - if (size == null) { - mainSendPort.send(Exception('can\'t get file size: ${req.remotePath}')); - return; + final sftp = await client.sftp(); + + final remoteFile = await sftp.open(req.remotePath); + int? size; + try { + size = (await remoteFile.stat()).size; + if (size == null) { + mainSendPort.send(Exception('can\'t get file size: ${req.remotePath}')); + return; + } + } finally { + await remoteFile.close(); } mainSendPort.send(size); mainSendPort.send(SftpWorkerStatus.loading); - // Issue #161 - // Due to single core performance, limit the chunk size - const defaultChunkSize = 1024 * 1024 * 5; - var totalRead = 0; + const chunkSize = 1024 * 1024 * 5; + var lastProgress = 0; + final localFile = File(req.localPath).openWrite(mode: FileMode.write); - 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); - } + try { + await sftp.download( + req.remotePath, + localFile, + onProgress: (bytesRead) { + final s = size; + if (s == null || s == 0) return; + final progress = (bytesRead / s * 100).round(); + if (progress != lastProgress) { + lastProgress = progress; + mainSendPort.send(progress); + } + }, + chunkSize: chunkSize, + ); + } finally { + await localFile.close(); } - await localFile.close(); - await file.close(); - mainSendPort.send(watch.elapsed); mainSendPort.send(SftpWorkerStatus.finished); } catch (e) { diff --git a/lib/data/provider/private_key.g.dart b/lib/data/provider/private_key.g.dart index 3670a00d..13cadb22 100644 --- a/lib/data/provider/private_key.g.dart +++ b/lib/data/provider/private_key.g.dart @@ -42,7 +42,7 @@ final class PrivateKeyNotifierProvider } String _$privateKeyNotifierHash() => - r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7'; + r'79d02e116fe665df1ccb0719590947e109a5a736'; abstract class _$PrivateKeyNotifier extends $Notifier { PrivateKeyState build(); diff --git a/lib/data/provider/server/all.g.dart b/lib/data/provider/server/all.g.dart index 75189bbf..33e02178 100644 --- a/lib/data/provider/server/all.g.dart +++ b/lib/data/provider/server/all.g.dart @@ -41,7 +41,7 @@ final class ServersNotifierProvider } } -String _$serversNotifierHash() => r'c90c2d8ce73a63f926bcf9679a84ae150c9d4808'; +String _$serversNotifierHash() => r'29a4cb286b7032e5e74841f4eba66941b0dec34e'; abstract class _$ServersNotifier extends $Notifier { ServersState build(); diff --git a/lib/data/provider/server/single.dart b/lib/data/provider/server/single.dart index f96641f8..30b83f41 100644 --- a/lib/data/provider/server/single.dart +++ b/lib/data/provider/server/single.dart @@ -361,6 +361,7 @@ class ServerNotifier extends _$ServerNotifier { parsedOutput: parsedOutput, system: state.status.system, customCmds: spi.custom?.cmds ?? {}, + tempDivisor: spi.custom?.tempIsCelsius == true ? 1.0 : 1000.0, ); final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>'); updateStatus(newStatus); diff --git a/lib/data/provider/server/single.g.dart b/lib/data/provider/server/single.g.dart index da9e2a75..1f2b64ed 100644 --- a/lib/data/provider/server/single.g.dart +++ b/lib/data/provider/server/single.g.dart @@ -58,7 +58,7 @@ final class ServerNotifierProvider } } -String _$serverNotifierHash() => r'04b1beef4d96242fd10d5b523c6f5f17eb774bae'; +String _$serverNotifierHash() => r'bf724a58c5d0ec99f4e00ebe7cea47a7b6b2cc64'; final class ServerNotifierFamily extends $Family with diff --git a/lib/data/provider/snippet.g.dart b/lib/data/provider/snippet.g.dart index 965fe314..97a91798 100644 --- a/lib/data/provider/snippet.g.dart +++ b/lib/data/provider/snippet.g.dart @@ -41,7 +41,7 @@ final class SnippetNotifierProvider } } -String _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9'; +String _$snippetNotifierHash() => r'46297b84ec6497e5c454be6ffe32330b37c6a465'; abstract class _$SnippetNotifier extends $Notifier { SnippetState build(); diff --git a/lib/data/res/github_id.dart b/lib/data/res/github_id.dart index f1fb3ae9..f2824a1b 100644 --- a/lib/data/res/github_id.dart +++ b/lib/data/res/github_id.dart @@ -156,7 +156,8 @@ abstract final class GithubIds { 'xxnuo', 'sunnysu0608', 'Staten-Wang', - 'alterkeyy' + 'alterkeyy', + 'zhbyu', }; } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 80075265..ddb44eb1 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1248,6 +1248,12 @@ abstract class AppLocalizations { /// **'For example, network traffic statistics are by default for all devices. You can specify a particular device here.'** String get specifyDevTip; + /// No description provided for @tempIsCelsiusTip. + /// + /// In en, this message translates to: + /// **'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'** + String get tempIsCelsiusTip; + /// No description provided for @speed. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index cac1e44c..d28619c8 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -644,6 +644,10 @@ class AppLocalizationsDe extends AppLocalizations { String get specifyDevTip => 'Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Tempo'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 397a2581..fa33cc96 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -639,6 +639,10 @@ class AppLocalizationsEn extends AppLocalizations { String get specifyDevTip => 'For example, network traffic statistics are by default for all devices. You can specify a particular device here.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Speed'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index e2e84e16..e2ab6e49 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -648,6 +648,10 @@ class AppLocalizationsEs extends AppLocalizations { String get specifyDevTip => 'Por ejemplo, las estadísticas de tráfico de red son por defecto para todos los dispositivos. Aquí puede especificar un dispositivo en particular.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Velocidad'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 2ed01fd6..04c9b4d1 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -649,6 +649,10 @@ class AppLocalizationsFr extends AppLocalizations { String get specifyDevTip => 'Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Vitesse'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index a93d3d1f..74b49291 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -640,6 +640,10 @@ class AppLocalizationsId extends AppLocalizations { String get specifyDevTip => 'Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Kecepatan'; diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index 76fa583a..738d970a 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -642,6 +642,10 @@ class AppLocalizationsIt extends AppLocalizations { String get specifyDevTip => 'Ad esempio, le statistiche del traffico di rete sono per impostazione predefinita per tutti i dispositivi. Puoi specificare un dispositivo particolare qui.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Velocità'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index 19153d0b..3d539ec0 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -620,6 +620,10 @@ class AppLocalizationsJa extends AppLocalizations { String get specifyDevTip => '例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => '速度'; diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index 17bb1895..3a1058d6 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -618,6 +618,10 @@ class AppLocalizationsKo extends AppLocalizations { String get specifyDevTip => '예를 들어, 네트워크 트래픽 통계는 기본적으로 모든 장치를 대상으로 합니다. 여기서 특정 장치를 지정할 수 있습니다.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => '속도'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 784b273c..c3012296 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -643,6 +643,10 @@ class AppLocalizationsNl extends AppLocalizations { String get specifyDevTip => 'Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Snelheid'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 42351aed..50d0184e 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -640,6 +640,10 @@ class AppLocalizationsPt extends AppLocalizations { String get specifyDevTip => 'Por exemplo, as estatísticas de tráfego de rede são por padrão para todos os dispositivos. Você pode especificar um dispositivo específico aqui.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Velocidade'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 29b17f05..2d539cea 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -645,6 +645,10 @@ class AppLocalizationsRu extends AppLocalizations { String get specifyDevTip => 'Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Скорость'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index a7ceffa6..201d294b 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -641,6 +641,10 @@ class AppLocalizationsTr extends AppLocalizations { String get specifyDevTip => 'Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihaz belirtebilirsiniz.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Hız'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 98ada076..898a32c9 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -645,6 +645,10 @@ class AppLocalizationsUk extends AppLocalizations { String get specifyDevTip => 'Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.'; + @override + String get tempIsCelsiusTip => + 'When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).'; + @override String get speed => 'Швидкість'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 01fbd566..456b40db 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -606,6 +606,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get specifyDevTip => '例如网络流量统计默认是所有设备,你可以在这里指定特定的设备'; + @override + String get tempIsCelsiusTip => + '开启后,温度值将被视为摄氏度而非毫摄氏度。仅在温度显示不正确时开启(例如显示0.1°C而非58°C)。'; + @override String get speed => '速度'; diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index f3663333..2f11e5f6 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -492,6 +492,7 @@ class ServerCustomAdapter extends TypeAdapter { pvePwd: fields[8] as String?, cmds: (fields[3] as Map?)?.cast(), preferTempDev: fields[4] as String?, + tempIsCelsius: fields[9] == null ? false : fields[9] as bool, logoUrl: fields[5] as String?, netDev: fields[6] as String?, scriptDir: fields[7] as String?, @@ -501,7 +502,7 @@ class ServerCustomAdapter extends TypeAdapter { @override void write(BinaryWriter writer, ServerCustom obj) { writer - ..writeByte(8) + ..writeByte(9) ..writeByte(1) ..write(obj.pveAddr) ..writeByte(2) @@ -517,7 +518,9 @@ class ServerCustomAdapter extends TypeAdapter { ..writeByte(7) ..write(obj.scriptDir) ..writeByte(8) - ..write(obj.pvePwd); + ..write(obj.pvePwd) + ..writeByte(9) + ..write(obj.tempIsCelsius); } @override diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index e4abd0b5..3072a248 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -187,7 +187,7 @@ types: index: 9 ServerCustom: typeId: 7 - nextIndex: 9 + nextIndex: 10 fields: pveAddr: index: 1 @@ -205,6 +205,8 @@ types: index: 7 pvePwd: index: 8 + tempIsCelsius: + index: 9 WakeOnLanCfg: typeId: 8 nextIndex: 3 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 901088a6..8cd63e8a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -187,6 +187,7 @@ "softWrap": "Soft wrap", "specifyDev": "Specify device", "specifyDevTip": "For example, network traffic statistics are by default for all devices. You can specify a particular device here.", + "tempIsCelsiusTip": "When enabled, the temperature value will be treated as Celsius instead of millicelsius. Turn on only if the temperature displays incorrectly (e.g., showing 0.1°C instead of 58°C).", "speed": "Speed", "spentTime": "Spent time: {time}", "sshConfigAllExist": "All servers already exist ({duplicateCount} duplicates found)", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 7e141b5d..e59df0e0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -187,6 +187,7 @@ "softWrap": "自动换行", "specifyDev": "指定设备", "specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备", + "tempIsCelsiusTip": "开启后,温度值将被视为摄氏度而非毫摄氏度。仅在温度显示不正确时开启(例如显示0.1°C而非58°C)。", "speed": "速度", "spentTime": "耗时:{time}", "sshConfigAllExist": "所有服务器已存在(发现 {duplicateCount} 个重复项)", diff --git a/lib/view/page/server/edit/actions.dart b/lib/view/page/server/edit/actions.dart index 2a838c87..4bc6dce2 100644 --- a/lib/view/page/server/edit/actions.dart +++ b/lib/view/page/server/edit/actions.dart @@ -74,6 +74,7 @@ extension _Actions on _ServerEditPageState { pvePwd: _pvePwdCtrl.text.selfNotEmptyOrNull, cmds: customCmds.isEmpty ? null : customCmds, preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull, + tempIsCelsius: _tempIsCelsius.value, logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull, netDev: _netDevCtrl.text.selfNotEmptyOrNull, scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull, @@ -270,6 +271,7 @@ extension _Utils on _ServerEditPageState { _pvePwdCtrl.text = custom.pvePwd ?? ''; _customCmds.value = custom.cmds ?? {}; _preferTempDevCtrl.text = custom.preferTempDev ?? ''; + _tempIsCelsius.value = custom.tempIsCelsius; _logoUrlCtrl.text = custom.logoUrl ?? ''; } diff --git a/lib/view/page/server/edit/edit.dart b/lib/view/page/server/edit/edit.dart index aab97500..10571131 100644 --- a/lib/view/page/server/edit/edit.dart +++ b/lib/view/page/server/edit/edit.dart @@ -70,6 +70,7 @@ class _ServerEditPageState extends ConsumerState final _autoConnect = ValueNotifier(true); final _jumpServer = nvn(); final _pveIgnoreCert = ValueNotifier(false); + final _tempIsCelsius = ValueNotifier(false); final _env = {}.vn; final _customCmds = {}.vn; final _tags = {}.vn; @@ -105,6 +106,7 @@ class _ServerEditPageState extends ConsumerState _autoConnect.dispose(); _jumpServer.dispose(); _pveIgnoreCert.dispose(); + _tempIsCelsius.dispose(); _env.dispose(); _customCmds.dispose(); _tags.dispose(); diff --git a/lib/view/page/server/edit/widget.dart b/lib/view/page/server/edit/widget.dart index 91ee8ab8..935f09cc 100644 --- a/lib/view/page/server/edit/widget.dart +++ b/lib/view/page/server/edit/widget.dart @@ -181,6 +181,18 @@ extension _Widgets on _ServerEditPageState { hint: 'nvme-pci-0400', suggestion: false, ), + ListTile( + leading: const Icon(MingCute.question_line), + title: TipText('${libL10n.temperature} (°C)', l10n.tempIsCelsiusTip), + trailing: _tempIsCelsius.listenVal( + (v) => Switch( + value: v, + onChanged: (val) { + _tempIsCelsius.value = val; + }, + ), + ), + ).cardx, Input( controller: _netDevCtrl, type: TextInputType.text, diff --git a/packages/dartssh2 b/packages/dartssh2 index 3dddce99..99f2b100 160000 --- a/packages/dartssh2 +++ b/packages/dartssh2 @@ -1 +1 @@ -Subproject commit 3dddce998d6f10ef46f6a4331be59c711ff2560a +Subproject commit 99f2b100de97973f2dda9e944c8bfbd2f93adcfa diff --git a/pubspec.lock b/pubspec.lock index ba00145d..a2e2d02e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -303,7 +303,7 @@ packages: path: "packages/dartssh2" relative: true source: path - version: "2.13.0" + version: "2.17.0" dbus: dependency: transitive description: @@ -990,10 +990,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1293,10 +1293,10 @@ packages: dependency: transitive description: name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" url: "https://pub.dev" source: hosted - version: "12.0.1" + version: "12.0.2" share_plus_platform_interface: dependency: transitive description: @@ -1610,10 +1610,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1" + sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" url: "https://pub.dev" source: hosted - version: "1.1.20" + version: "1.1.21" vector_graphics_codec: dependency: transitive description: