mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
feat: import servers from ~/.ssh/config (#873)
This commit is contained in:
@@ -78,7 +78,7 @@ Future<SSHClient> genClient(
|
||||
Loggers.app.warning('genClient', e);
|
||||
if (spi.alterUrl == null) rethrow;
|
||||
try {
|
||||
final res = spi.fromStringUrl();
|
||||
final res = spi.parseAlterUrl();
|
||||
alterUser = res.$2;
|
||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||
} catch (e) {
|
||||
|
||||
84
lib/core/utils/server_dedup.dart
Normal file
84
lib/core/utils/server_dedup.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
|
||||
class ServerDeduplication {
|
||||
/// Remove duplicate servers from the import list based on existing servers
|
||||
/// Returns the deduplicated list
|
||||
static List<Spi> deduplicateServers(List<Spi> importedServers) {
|
||||
final existingServers = ServerStore.instance.fetch();
|
||||
final deduplicated = <Spi>[];
|
||||
|
||||
for (final imported in importedServers) {
|
||||
if (!_isDuplicate(imported, existingServers)) {
|
||||
deduplicated.add(imported);
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicated;
|
||||
}
|
||||
|
||||
/// Check if an imported server is a duplicate of an existing server
|
||||
static bool _isDuplicate(Spi imported, List<Spi> existing) {
|
||||
for (final existingSpi in existing) {
|
||||
if (imported.isSameAs(existingSpi)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Resolve name conflicts by appending suffixes
|
||||
static List<Spi> resolveNameConflicts(List<Spi> importedServers) {
|
||||
final existingServers = ServerStore.instance.fetch();
|
||||
final existingNames = existingServers.map((s) => s.name).toSet();
|
||||
final processedNames = <String>{};
|
||||
final result = <Spi>[];
|
||||
|
||||
for (final server in importedServers) {
|
||||
String newName = server.name;
|
||||
int suffix = 1;
|
||||
|
||||
// Check against both existing servers and already processed servers
|
||||
while (existingNames.contains(newName) || processedNames.contains(newName)) {
|
||||
newName = '${server.name} ($suffix)';
|
||||
suffix++;
|
||||
}
|
||||
|
||||
processedNames.add(newName);
|
||||
|
||||
if (newName != server.name) {
|
||||
result.add(server.copyWith(name: newName));
|
||||
} else {
|
||||
result.add(server);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get summary of import operation
|
||||
static ImportSummary getImportSummary(List<Spi> originalList, List<Spi> deduplicatedList) {
|
||||
final duplicateCount = originalList.length - deduplicatedList.length;
|
||||
return ImportSummary(
|
||||
total: originalList.length,
|
||||
duplicates: duplicateCount,
|
||||
toImport: deduplicatedList.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImportSummary {
|
||||
final int total;
|
||||
final int duplicates;
|
||||
final int toImport;
|
||||
|
||||
const ImportSummary({
|
||||
required this.total,
|
||||
required this.duplicates,
|
||||
required this.toImport,
|
||||
});
|
||||
|
||||
bool get hasDuplicates => duplicates > 0;
|
||||
bool get hasItemsToImport => toImport > 0;
|
||||
}
|
||||
187
lib/core/utils/ssh_config.dart
Normal file
187
lib/core/utils/ssh_config.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
import 'dart:io';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
|
||||
/// Utility class to parse SSH config files under `~/.ssh/config`
|
||||
abstract final class SSHConfig {
|
||||
static const String _defaultPath = '~/.ssh/config';
|
||||
|
||||
static String? get _homePath {
|
||||
final homePath = isWindows ? Platform.environment['USERPROFILE'] : Platform.environment['HOME'];
|
||||
if (homePath == null || homePath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return homePath;
|
||||
}
|
||||
|
||||
/// Get possible SSH config file paths, with macOS-specific handling
|
||||
static List<String> get _possibleConfigPaths {
|
||||
final paths = <String>[];
|
||||
final homePath = _homePath;
|
||||
|
||||
if (homePath != null) {
|
||||
// Standard path
|
||||
paths.add('$homePath/.ssh/config');
|
||||
|
||||
// On macOS, also try the actual user home directory
|
||||
if (isMacOS) {
|
||||
// Try to get the real user home directory
|
||||
final username = Platform.environment['USER'];
|
||||
if (username != null) {
|
||||
paths.add('/Users/$username/.ssh/config');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/// Parse SSH config file and return a list of Spi objects
|
||||
static Future<List<Spi>> parseConfig([String? configPath]) async {
|
||||
final (file, exists) = configExists(configPath);
|
||||
if (!exists || file == null) {
|
||||
Loggers.app.info('SSH config file does not exist at path: ${configPath ?? _defaultPath}');
|
||||
return [];
|
||||
}
|
||||
|
||||
final content = await file.readAsString();
|
||||
return _parseSSHConfig(content);
|
||||
}
|
||||
|
||||
/// Parse SSH config content
|
||||
static List<Spi> _parseSSHConfig(String content) {
|
||||
final servers = <Spi>[];
|
||||
final lines = content.split('\n');
|
||||
|
||||
String? currentHost;
|
||||
String? hostname;
|
||||
String? user;
|
||||
int port = 22;
|
||||
String? identityFile;
|
||||
String? jumpHost;
|
||||
|
||||
void addServer() {
|
||||
if (currentHost != null && currentHost != '*' && hostname != null) {
|
||||
final spi = Spi(
|
||||
name: currentHost,
|
||||
ip: hostname,
|
||||
port: port,
|
||||
user: user ?? 'root', // Default user is 'root'
|
||||
keyId: identityFile,
|
||||
jumpId: jumpHost,
|
||||
);
|
||||
servers.add(spi);
|
||||
}
|
||||
}
|
||||
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty || trimmed.startsWith('#')) continue;
|
||||
|
||||
// Handle inline comments
|
||||
final commentIndex = trimmed.indexOf('#');
|
||||
final cleanLine = commentIndex != -1 ? trimmed.substring(0, commentIndex).trim() : trimmed;
|
||||
if (cleanLine.isEmpty) continue;
|
||||
|
||||
final parts = cleanLine.split(RegExp(r'\s+'));
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
final key = parts[0].toLowerCase();
|
||||
var value = parts.sublist(1).join(' ');
|
||||
|
||||
// Remove quotes from values
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.substring(1, value.length - 1);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'host':
|
||||
// Save previous host config
|
||||
addServer();
|
||||
|
||||
// Reset for new host
|
||||
final originalValue = parts.sublist(1).join(' ');
|
||||
final isQuoted =
|
||||
(originalValue.startsWith('"') && originalValue.endsWith('"')) ||
|
||||
(originalValue.startsWith("'") && originalValue.endsWith("'"));
|
||||
|
||||
currentHost = value;
|
||||
// Skip hosts with multiple patterns (contains spaces but not quoted)
|
||||
if (currentHost.contains(' ') && !isQuoted) {
|
||||
currentHost = null; // Mark as invalid to skip
|
||||
}
|
||||
hostname = null;
|
||||
user = null;
|
||||
port = 22;
|
||||
identityFile = null;
|
||||
jumpHost = null;
|
||||
break;
|
||||
|
||||
case 'hostname':
|
||||
hostname = value;
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user = value;
|
||||
break;
|
||||
|
||||
case 'port':
|
||||
port = int.tryParse(value) ?? 22;
|
||||
break;
|
||||
|
||||
case 'identityfile':
|
||||
identityFile = value; // Store the path directly
|
||||
break;
|
||||
|
||||
case 'proxyjump':
|
||||
case 'proxycommand':
|
||||
jumpHost = _extractJumpHost(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last server
|
||||
addServer();
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/// Extract jump host from ProxyJump or ProxyCommand
|
||||
static String? _extractJumpHost(String value) {
|
||||
// For ProxyJump, the format is usually: user@host:port
|
||||
// For ProxyCommand, it's more complex and might need custom parsing
|
||||
if (value.contains('@')) {
|
||||
return value.split(' ').first;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if SSH config file exists, trying multiple possible paths
|
||||
static (File?, bool) configExists([String? configPath]) {
|
||||
if (configPath != null) {
|
||||
// If specific path is provided, use it directly
|
||||
final homePath = _homePath;
|
||||
if (homePath == null) {
|
||||
Loggers.app.warning('Cannot determine home directory for SSH config parsing.');
|
||||
return (null, false);
|
||||
}
|
||||
final expandedPath = configPath.replaceFirst('~', homePath);
|
||||
dprint('Checking SSH config at path: $expandedPath');
|
||||
final file = File(expandedPath);
|
||||
return (file, file.existsSync());
|
||||
}
|
||||
|
||||
// Try multiple possible paths
|
||||
for (final path in _possibleConfigPaths) {
|
||||
dprint('Checking SSH config at path: $path');
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
dprint('Found SSH config at: $path');
|
||||
return (file, true);
|
||||
}
|
||||
}
|
||||
|
||||
dprint('SSH config file not found in any of the expected locations');
|
||||
return (null, false);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ abstract class Spi with _$Spi {
|
||||
@override
|
||||
String toString() => 'Spi<$oldId>';
|
||||
|
||||
/// Parse the [id], if it's null or empty, generate a new one.
|
||||
static String parseId(Object? id) {
|
||||
if (id == null || id is! String || id.isEmpty) return ShortId.generate();
|
||||
return id;
|
||||
@@ -83,20 +84,26 @@ extension Spix on Spi {
|
||||
return newSpi.id;
|
||||
}
|
||||
|
||||
/// Json encode to string.
|
||||
String toJsonString() => json.encode(toJson());
|
||||
|
||||
bool shouldReconnect(Spi old) {
|
||||
return user != old.user ||
|
||||
ip != old.ip ||
|
||||
port != old.port ||
|
||||
pwd != old.pwd ||
|
||||
keyId != old.keyId ||
|
||||
alterUrl != old.alterUrl ||
|
||||
jumpId != old.jumpId ||
|
||||
custom?.cmds != old.custom?.cmds;
|
||||
/// Returns true if the connection info is the same as [other].
|
||||
bool isSameAs(Spi other) {
|
||||
return user == other.user &&
|
||||
ip == other.ip &&
|
||||
port == other.port &&
|
||||
pwd == other.pwd &&
|
||||
keyId == other.keyId &&
|
||||
jumpId == other.jumpId;
|
||||
}
|
||||
|
||||
(String ip, String usr, int port) fromStringUrl() {
|
||||
/// Returns true if the connection should be re-established.
|
||||
bool shouldReconnect(Spi old) {
|
||||
return !isSameAs(old) || alterUrl != old.alterUrl || custom?.cmds != old.custom?.cmds;
|
||||
}
|
||||
|
||||
/// Parse the [alterUrl] to (ip, user, port).
|
||||
(String ip, String usr, int port) parseAlterUrl() {
|
||||
if (alterUrl == null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
|
||||
}
|
||||
@@ -141,5 +148,6 @@ extension Spix on Spi {
|
||||
id: 'id',
|
||||
);
|
||||
|
||||
/// Returns true if the user is 'root'.
|
||||
bool get isRoot => user == 'root';
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ abstract final class GithubIds {
|
||||
'zxf945',
|
||||
'cnen2018',
|
||||
'xiaomeng9597',
|
||||
'mingzhao2019',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -277,4 +277,7 @@ class SettingStore extends HiveStore {
|
||||
|
||||
/// The backup password
|
||||
late final backupasswd = SecureProp('bakPasswd');
|
||||
|
||||
/// Whether to read SSH config from ~/.ssh/config on first time
|
||||
late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);
|
||||
}
|
||||
|
||||
@@ -185,17 +185,17 @@ abstract class AppLocalizations {
|
||||
/// **'Automatic home widget update'**
|
||||
String get autoUpdateHomeWidget;
|
||||
|
||||
/// No description provided for @backupTip.
|
||||
/// No description provided for @backupEncrypted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
|
||||
String get backupTip;
|
||||
/// **'Backup is encrypted'**
|
||||
String get backupEncrypted;
|
||||
|
||||
/// No description provided for @backupVersionNotMatch.
|
||||
/// No description provided for @backupNotEncrypted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup version is not match.'**
|
||||
String get backupVersionNotMatch;
|
||||
/// **'Backup is not encrypted'**
|
||||
String get backupNotEncrypted;
|
||||
|
||||
/// No description provided for @backupPassword.
|
||||
///
|
||||
@@ -203,6 +203,18 @@ abstract class AppLocalizations {
|
||||
/// **'Backup password'**
|
||||
String get backupPassword;
|
||||
|
||||
/// No description provided for @backupPasswordRemoved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password removed'**
|
||||
String get backupPasswordRemoved;
|
||||
|
||||
/// No description provided for @backupPasswordSet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password set'**
|
||||
String get backupPasswordSet;
|
||||
|
||||
/// No description provided for @backupPasswordTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -215,29 +227,17 @@ abstract class AppLocalizations {
|
||||
/// **'Incorrect backup password'**
|
||||
String get backupPasswordWrong;
|
||||
|
||||
/// No description provided for @backupEncrypted.
|
||||
/// No description provided for @backupTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup is encrypted'**
|
||||
String get backupEncrypted;
|
||||
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
|
||||
String get backupTip;
|
||||
|
||||
/// No description provided for @backupNotEncrypted.
|
||||
/// No description provided for @backupVersionNotMatch.
|
||||
///
|
||||
/// 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;
|
||||
/// **'Backup version is not match.'**
|
||||
String get backupVersionNotMatch;
|
||||
|
||||
/// No description provided for @battery.
|
||||
///
|
||||
@@ -1202,6 +1202,84 @@ abstract class AppLocalizations {
|
||||
/// **'Spent time: {time}'**
|
||||
String spentTime(Object time);
|
||||
|
||||
/// No description provided for @sshConfigAllExist.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All servers already exist ({duplicateCount} duplicates found)'**
|
||||
String sshConfigAllExist(Object duplicateCount);
|
||||
|
||||
/// No description provided for @sshConfigDuplicatesSkipped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{duplicateCount} duplicates will be skipped'**
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount);
|
||||
|
||||
/// No description provided for @sshConfigFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'We found SSH configuration on your system.'**
|
||||
String get sshConfigFound;
|
||||
|
||||
/// No description provided for @sshConfigFoundServers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Found {totalCount} servers'**
|
||||
String sshConfigFoundServers(Object totalCount);
|
||||
|
||||
/// No description provided for @sshConfigImport.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SSH Config Import'**
|
||||
String get sshConfigImport;
|
||||
|
||||
/// No description provided for @sshConfigImportHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Only basic information can be imported, for example: IP/Port.'**
|
||||
String get sshConfigImportHelp;
|
||||
|
||||
/// No description provided for @sshConfigImportPermission.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Would you like to give permission to read ~/.ssh/config and automatically import server settings?'**
|
||||
String get sshConfigImportPermission;
|
||||
|
||||
/// No description provided for @sshConfigImportTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Prompt to read ~/.ssh/config on first server creation'**
|
||||
String get sshConfigImportTip;
|
||||
|
||||
/// No description provided for @sshConfigImported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Imported {count} servers from SSH config'**
|
||||
String sshConfigImported(Object count);
|
||||
|
||||
/// No description provided for @sshConfigManualSelect.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Would you like to select the SSH config file manually?'**
|
||||
String get sshConfigManualSelect;
|
||||
|
||||
/// No description provided for @sshConfigNoServers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No servers found in SSH config'**
|
||||
String get sshConfigNoServers;
|
||||
|
||||
/// No description provided for @sshConfigPermissionDenied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cannot access SSH config file due to macOS permissions.'**
|
||||
String get sshConfigPermissionDenied;
|
||||
|
||||
/// No description provided for @sshConfigServersToImport.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{importCount} servers will be imported'**
|
||||
String sshConfigServersToImport(Object importCount);
|
||||
|
||||
/// No description provided for @sshTermHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -46,16 +46,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.';
|
||||
String get backupEncrypted => 'Backup ist verschlüsselt';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Die Backup-Version stimmt nicht überein.';
|
||||
String get backupNotEncrypted => 'Backup ist nicht verschlüsselt';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Backup-Passwort';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Backup-Passwort entfernt';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Backup-Passwort gesetzt';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.';
|
||||
@@ -64,16 +68,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Falsches Backup-Passwort';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Backup ist verschlüsselt';
|
||||
String get backupTip =>
|
||||
'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Backup ist nicht verschlüsselt';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Backup-Passwort gesetzt';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Backup-Passwort entfernt';
|
||||
String get backupVersionNotMatch =>
|
||||
'Die Backup-Version stimmt nicht überein.';
|
||||
|
||||
@override
|
||||
String get battery => 'Batterie';
|
||||
@@ -606,6 +606,62 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Benötigte Zeit: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Alle Server existieren bereits ($duplicateCount Duplikate gefunden)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount Duplikate werden übersprungen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound =>
|
||||
'Wir haben SSH-Konfiguration auf Ihrem System gefunden.';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '$totalCount Server gefunden';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'SSH-Konfiguration importieren';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Es können nur Basisinformationen importiert werden, zum Beispiel: IP/Port.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return '$count Server aus SSH-Konfiguration importiert';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers =>
|
||||
'Keine Server in der SSH-Konfiguration gefunden';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount Server werden importiert';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.';
|
||||
|
||||
@@ -46,15 +46,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'Automatic home widget update';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'The exported data can be encrypted with password. \nPlease keep it safe.';
|
||||
String get backupEncrypted => 'Backup is encrypted';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Backup version is not match.';
|
||||
String get backupNotEncrypted => 'Backup is not encrypted';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Backup password';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Backup password removed';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Backup password set';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Set a password to encrypt backup files. Leave empty to disable encryption.';
|
||||
@@ -63,16 +68,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Incorrect backup password';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Backup is encrypted';
|
||||
String get backupTip =>
|
||||
'The exported data can be encrypted with password. \nPlease keep it safe.';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Backup is not encrypted';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Backup password set';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Backup password removed';
|
||||
String get backupVersionNotMatch => 'Backup version is not match.';
|
||||
|
||||
@override
|
||||
String get battery => 'Battery';
|
||||
@@ -602,6 +602,60 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Spent time: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'All servers already exist ($duplicateCount duplicates found)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount duplicates will be skipped';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'We found SSH configuration on your system.';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return 'Found $totalCount servers';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'SSH Config Import';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Only basic information can be imported, for example: IP/Port.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Would you like to give permission to read ~/.ssh/config and automatically import server settings?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Prompt to read ~/.ssh/config on first server creation';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'Imported $count servers from SSH config';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Would you like to select the SSH config file manually?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'No servers found in SSH config';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Cannot access SSH config file due to macOS permissions.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount servers will be imported';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.';
|
||||
|
||||
@@ -46,16 +46,20 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'Actualizar automáticamente el widget del escritorio';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.';
|
||||
String get backupEncrypted => 'El respaldo está encriptado';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'La versión de la copia de seguridad no coincide, no se puede restaurar';
|
||||
String get backupNotEncrypted => 'El respaldo no está encriptado';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Contraseña de respaldo';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Contraseña de respaldo eliminada';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Contraseña de respaldo establecida';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.';
|
||||
@@ -64,16 +68,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Contraseña de respaldo incorrecta';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'El respaldo está encriptado';
|
||||
String get backupTip =>
|
||||
'Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.';
|
||||
|
||||
@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';
|
||||
String get backupVersionNotMatch =>
|
||||
'La versión de la copia de seguridad no coincide, no se puede restaurar';
|
||||
|
||||
@override
|
||||
String get battery => 'Batería';
|
||||
@@ -609,6 +609,61 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Tiempo gastado: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Todos los servidores ya existen (se encontraron $duplicateCount duplicados)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return 'Se omitirán $duplicateCount duplicados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'Encontramos configuración SSH en tu sistema';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return 'Se encontraron $totalCount servidores';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'Importar Configuración SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Solo se pueden importar datos básicos, por ejemplo: IP/Puerto.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Sugerencia para leer ~/.ssh/config al crear el primer servidor';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'Se importaron $count servidores desde la configuración SSH';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'¿Te gustaría seleccionar manualmente el archivo de configuración SSH?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers =>
|
||||
'No se encontraron servidores en la configuración SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return 'Se importarán $importCount servidores';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.';
|
||||
|
||||
@@ -46,16 +46,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Mise à jour automatique du widget d\'accueil';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.';
|
||||
String get backupEncrypted => 'La sauvegarde est chiffrée';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'La version de sauvegarde ne correspond pas.';
|
||||
String get backupNotEncrypted => 'La sauvegarde n\'est pas chiffrée';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Mot de passe de sauvegarde';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Mot de passe de sauvegarde supprimé';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Mot de passe de sauvegarde défini';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.';
|
||||
@@ -64,16 +68,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Mot de passe de sauvegarde incorrect';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'La sauvegarde est chiffrée';
|
||||
String get backupTip =>
|
||||
'Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.';
|
||||
|
||||
@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é';
|
||||
String get backupVersionNotMatch =>
|
||||
'La version de sauvegarde ne correspond pas.';
|
||||
|
||||
@override
|
||||
String get battery => 'Batterie';
|
||||
@@ -610,6 +610,62 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Temps écoulé : $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Tous les serveurs existent déjà ($duplicateCount doublons trouvés)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount doublons seront ignorés';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound =>
|
||||
'Nous avons trouvé une configuration SSH sur votre système.';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '$totalCount serveurs trouvés';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'Importation de configuration SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Seules les informations de base peuvent être importées, par exemple : IP/Port.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Souhaitez-vous donner la permission de lire ~/.ssh/config et d\'importer automatiquement les paramètres du serveur ?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Proposer de lire ~/.ssh/config lors de la première création de serveur';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return '$count serveurs importés depuis la configuration SSH';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers =>
|
||||
'Aucun serveur trouvé dans la configuration SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Impossible d\'accéder au fichier de configuration SSH en raison des permissions macOS.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount serveurs seront importés';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L\'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu\'aucun texte n\'est sélectionné et qu\'il y a du contenu dans le presse-papiers. L\'icône de code colle des extraits de code dans le terminal et les exécute.';
|
||||
@@ -798,5 +854,5 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get 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.';
|
||||
'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.';
|
||||
}
|
||||
|
||||
@@ -46,15 +46,20 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'Widget Rumah Pembaruan Otomatis';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.';
|
||||
String get backupEncrypted => 'Cadangan telah dienkripsi';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';
|
||||
String get backupNotEncrypted => 'Cadangan tidak dienkripsi';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Kata sandi cadangan';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.';
|
||||
@@ -63,16 +68,11 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Kata sandi cadangan salah';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Cadangan telah dienkripsi';
|
||||
String get backupTip =>
|
||||
'Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Cadangan tidak dienkripsi';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';
|
||||
String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';
|
||||
|
||||
@override
|
||||
String get battery => 'Baterai';
|
||||
@@ -603,6 +603,61 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
return 'Menghabiskan waktu: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Semua server sudah ada (ditemukan $duplicateCount duplikat)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount duplikat akan dilewati';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'Kami menemukan konfigurasi SSH di sistem Anda';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return 'Ditemukan $totalCount server';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'Impor Konfigurasi SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Hanya informasi dasar yang dapat diimpor, misalnya: IP/Port.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'Berhasil mengimpor $count server dari konfigurasi SSH';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Apakah Anda ingin memilih file konfigurasi SSH secara manual?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers =>
|
||||
'Tidak ada server yang ditemukan dalam konfigurasi SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Tidak dapat mengakses file konfigurasi SSH karena izin macOS.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount server akan diimpor';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.';
|
||||
|
||||
@@ -43,14 +43,20 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新';
|
||||
|
||||
@override
|
||||
String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。';
|
||||
String get backupEncrypted => 'バックアップは暗号化されています';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';
|
||||
String get backupNotEncrypted => 'バックアップは暗号化されていません';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'バックアップパスワード';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'バックアップパスワードが設定されました';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。';
|
||||
@@ -59,16 +65,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'バックアップパスワードが間違っています';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'バックアップは暗号化されています';
|
||||
String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'バックアップは暗号化されていません';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'バックアップパスワードが設定されました';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
|
||||
String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';
|
||||
|
||||
@override
|
||||
String get battery => 'バッテリー';
|
||||
@@ -587,6 +587,56 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '費した時間: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'すべてのサーバーがすでに存在します($duplicateCount個の重複が見つかりました)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount個の重複がスキップされます';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'システムにSSH設定が見つかりました。';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '$totalCount個のサーバーが見つかりました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'SSH設定のインポート';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp => 'インポートできるのは基本情報のみです。例:IP/ポート。';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip => '初回サーバー作成時に~/.ssh/configの読み取りを促す';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'SSH設定から$count個のサーバーをインポートしました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect => 'SSH設定ファイルを手動で選択しますか?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'SSH設定でサーバーが見つかりませんでした';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied => 'macOSの権限により、SSH設定ファイルにアクセスできません。';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount個のサーバーがインポートされます';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。';
|
||||
|
||||
@@ -46,15 +46,20 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'Automatische update van home-widget';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.';
|
||||
String get backupEncrypted => 'Back-up is versleuteld';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';
|
||||
String get backupNotEncrypted => 'Back-up is niet versleuteld';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Back-up wachtwoord';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Back-up wachtwoord verwijderd';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Back-up wachtwoord ingesteld';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.';
|
||||
@@ -63,16 +68,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Onjuist back-up wachtwoord';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Back-up is versleuteld';
|
||||
String get backupTip =>
|
||||
'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.';
|
||||
|
||||
@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';
|
||||
String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';
|
||||
|
||||
@override
|
||||
String get battery => 'Batterij';
|
||||
@@ -605,6 +605,61 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Gebruikte tijd: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Alle servers bestaan al ($duplicateCount duplicaten gevonden)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount duplicaten worden overgeslagen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound =>
|
||||
'We hebben SSH-configuratie op uw systeem gevonden';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '$totalCount servers gevonden';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'SSH Configuratie Importeren';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Alleen basisinformatie kan worden geïmporteerd, bijvoorbeeld: IP/Poort.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return '$count servers geïmporteerd uit SSH-configuratie';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Wilt u het SSH-configuratiebestand handmatig selecteren?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'Geen servers gevonden in SSH-configuratie';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount servers worden geïmporteerd';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.';
|
||||
|
||||
@@ -46,16 +46,20 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'Atualização automática do widget da tela inicial';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.';
|
||||
String get backupEncrypted => 'Backup está criptografado';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Versão de backup não compatível, não é possível restaurar';
|
||||
String get backupNotEncrypted => 'Backup não está criptografado';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Senha de backup';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Senha de backup removida';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Senha de backup definida';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.';
|
||||
@@ -64,16 +68,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Senha de backup incorreta';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Backup está criptografado';
|
||||
String get backupTip =>
|
||||
'Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.';
|
||||
|
||||
@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';
|
||||
String get backupVersionNotMatch =>
|
||||
'Versão de backup não compatível, não é possível restaurar';
|
||||
|
||||
@override
|
||||
String get battery => 'Bateria';
|
||||
@@ -604,6 +604,61 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Tempo gasto: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Todos os servidores já existem (encontradas $duplicateCount duplicatas)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount duplicatas serão ignoradas';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'Encontramos configuração SSH no seu sistema';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return 'Encontrados $totalCount servidores';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'Importar Configuração SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Só é possível importar informações básicas, por exemplo: IP/Porta.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Sugestão para ler ~/.ssh/config na criação do primeiro servidor';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'Importados $count servidores da configuração SSH';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Gostaria de selecionar manualmente o arquivo de configuração SSH?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers =>
|
||||
'Nenhum servidor encontrado na configuração SSH';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount servidores serão importados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.';
|
||||
|
||||
@@ -46,16 +46,20 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Автоматическое обновление виджета на главном экране';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
|
||||
String get backupEncrypted => 'Резервная копия зашифрована';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Версия резервной копии не совпадает, восстановление невозможно';
|
||||
String get backupNotEncrypted => 'Резервная копия не зашифрована';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Пароль резервной копии';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Пароль резервной копии удален';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Пароль резервной копии установлен';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.';
|
||||
@@ -64,16 +68,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Неверный пароль резервной копии';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Резервная копия зашифрована';
|
||||
String get backupTip =>
|
||||
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Резервная копия не зашифрована';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Пароль резервной копии установлен';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Пароль резервной копии удален';
|
||||
String get backupVersionNotMatch =>
|
||||
'Версия резервной копии не совпадает, восстановление невозможно';
|
||||
|
||||
@override
|
||||
String get battery => 'Батарея';
|
||||
@@ -607,6 +607,60 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Затрачено времени: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Все серверы уже существуют (найдено $duplicateCount дубликатов)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount дубликатов будут пропущены';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'Мы нашли SSH-конфигурацию в вашей системе';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return 'Найдено $totalCount серверов';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'Импорт SSH Конфигурации';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Можно импортировать только базовую информацию, например: IP/порт.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Предложение прочитать ~/.ssh/config при создании первого сервера';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'Импортировано $count серверов из SSH-конфигурации';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Хотели бы вы вручную выбрать файл конфигурации SSH?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'Серверы не найдены в SSH-конфигурации';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount серверов будут импортированы';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.';
|
||||
|
||||
@@ -45,15 +45,20 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => 'Ana ekran bileşenini otomatik güncelle';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.';
|
||||
String get backupEncrypted => 'Yedekleme şifrelenmiş';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';
|
||||
String get backupNotEncrypted => 'Yedekleme şifreli değil';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Yedekleme parolası';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Yedekleme parolası ayarlandı';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.';
|
||||
@@ -62,16 +67,11 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Yanlış yedekleme parolası';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Yedekleme şifrelenmiş';
|
||||
String get backupTip =>
|
||||
'Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Yedekleme şifreli değil';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Yedekleme parolası ayarlandı';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';
|
||||
String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';
|
||||
|
||||
@override
|
||||
String get battery => 'Pil';
|
||||
@@ -603,6 +603,60 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
return 'Harcanan süre: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Tüm sunucular zaten mevcut ($duplicateCount kopya bulundu)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount kopya atlanacak';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'Sisteminizde SSH yapılandırması bulduk';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '$totalCount sunucu bulundu';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'SSH Yapılandırma İçe Aktarma';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Yalnızca temel bilgiler içe aktarılabilir, örneğin: IP/Port.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'İlk sunucu oluşturulurken ~/.ssh/config okuma istemi';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'SSH yapılandırmasından $count sunucu içe aktarıldı';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'SSH yapılandırmasında sunucu bulunamadı';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount sunucu içe aktarılacak';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP\'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.';
|
||||
|
||||
@@ -46,16 +46,20 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
'Автоматичне оновлення віджетів на головному екрані';
|
||||
|
||||
@override
|
||||
String get backupTip =>
|
||||
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
|
||||
String get backupEncrypted => 'Резервна копія зашифрована';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch =>
|
||||
'Версія резервного копіювання не збіглася.';
|
||||
String get backupNotEncrypted => 'Резервна копія не зашифрована';
|
||||
|
||||
@override
|
||||
String get backupPassword => 'Пароль резервного копіювання';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip =>
|
||||
'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.';
|
||||
@@ -64,16 +68,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get backupPasswordWrong => 'Неправильний пароль резервного копіювання';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => 'Резервна копія зашифрована';
|
||||
String get backupTip =>
|
||||
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => 'Резервна копія не зашифрована';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
|
||||
String get backupVersionNotMatch =>
|
||||
'Версія резервного копіювання не збіглася.';
|
||||
|
||||
@override
|
||||
String get battery => 'Акумулятор';
|
||||
@@ -608,6 +608,60 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return 'Витрачений час: $time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return 'Всі сервери вже існують (знайдено $duplicateCount дублікатів)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount дублікатів буде пропущено';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => 'Ми знайшли SSH-конфігурацію у вашій системі';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return 'Знайдено $totalCount серверів';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'Імпорт SSH Конфігурації';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp =>
|
||||
'Можна імпортувати лише базову інформацію, наприклад: IP/порт.';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission =>
|
||||
'Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip =>
|
||||
'Пропозиція прочитати ~/.ssh/config при створенні першого сервера';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return 'Імпортовано $count серверів з SSH-конфігурації';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect =>
|
||||
'Чи хочете ви вручну вибрати файл конфігурації SSH?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'Сервери не знайдені в SSH-конфігурації';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied =>
|
||||
'Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount серверів буде імпортовано';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.';
|
||||
|
||||
@@ -42,14 +42,20 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get autoUpdateHomeWidget => '自动更新桌面小部件';
|
||||
|
||||
@override
|
||||
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
|
||||
String get backupEncrypted => '备份已加密';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
|
||||
String get backupNotEncrypted => '备份未加密';
|
||||
|
||||
@override
|
||||
String get backupPassword => '备份密码';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => '备份密码已移除';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => '备份密码已设置';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';
|
||||
|
||||
@@ -57,16 +63,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get backupPasswordWrong => '备份密码错误';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => '备份已加密';
|
||||
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => '备份未加密';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => '备份密码已设置';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => '备份密码已移除';
|
||||
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
|
||||
|
||||
@override
|
||||
String get battery => '电池';
|
||||
@@ -578,6 +578,55 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '耗时:$time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return '所有服务器已存在(发现 $duplicateCount 个重复项)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '$duplicateCount 个重复项将被跳过';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => '我们在您的系统中发现了 SSH 配置。';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '发现 $totalCount 个服务器';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => 'SSH 配置导入';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp => '只能导入基础信息,例如:IP/端口';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission => '是否允许读取 ~/.ssh/config 并自动导入服务器设置?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip => '首次创建服务器时提示读取 ~/.ssh/config';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return '从 SSH 配置导入了 $count 个服务器';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect => '是否要手动选择 SSH 配置文件?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'SSH 配置中未找到服务器';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied => '由于 macOS 权限限制,无法访问 SSH 配置文件。';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '$importCount 个服务器将被导入';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容,在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。';
|
||||
@@ -794,14 +843,20 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get autoUpdateHomeWidget => '自動更新桌面小工具';
|
||||
|
||||
@override
|
||||
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
|
||||
String get backupEncrypted => '備份已加密';
|
||||
|
||||
@override
|
||||
String get backupVersionNotMatch => '備份版本不相容,無法還原';
|
||||
String get backupNotEncrypted => '備份未加密';
|
||||
|
||||
@override
|
||||
String get backupPassword => '備份密碼';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => '備份密碼已移除';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => '備份密碼已設定';
|
||||
|
||||
@override
|
||||
String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';
|
||||
|
||||
@@ -809,16 +864,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
String get backupPasswordWrong => '備份密碼錯誤';
|
||||
|
||||
@override
|
||||
String get backupEncrypted => '備份已加密';
|
||||
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
|
||||
|
||||
@override
|
||||
String get backupNotEncrypted => '備份未加密';
|
||||
|
||||
@override
|
||||
String get backupPasswordSet => '備份密碼已設定';
|
||||
|
||||
@override
|
||||
String get backupPasswordRemoved => '備份密碼已移除';
|
||||
String get backupVersionNotMatch => '備份版本不相容,無法還原';
|
||||
|
||||
@override
|
||||
String get battery => '電池';
|
||||
@@ -1330,6 +1379,55 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
return '耗時:$time';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigAllExist(Object duplicateCount) {
|
||||
return '所有伺服器均已存在(發現$duplicateCount個重複項)';
|
||||
}
|
||||
|
||||
@override
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount) {
|
||||
return '將跳過$duplicateCount個重複項';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigFound => '我們在您的系統中發現了SSH設定';
|
||||
|
||||
@override
|
||||
String sshConfigFoundServers(Object totalCount) {
|
||||
return '發現$totalCount個伺服器';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigImport => '匯入SSH設定';
|
||||
|
||||
@override
|
||||
String get sshConfigImportHelp => '只能匯入基礎資訊,例如:IP/端口。';
|
||||
|
||||
@override
|
||||
String get sshConfigImportPermission => '您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定?';
|
||||
|
||||
@override
|
||||
String get sshConfigImportTip => '在建立第一個伺服器時提示讀取 ~/.ssh/config';
|
||||
|
||||
@override
|
||||
String sshConfigImported(Object count) {
|
||||
return '已從SSH設定匯入$count個伺服器';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshConfigManualSelect => '是否要手動選擇 SSH 設定檔案?';
|
||||
|
||||
@override
|
||||
String get sshConfigNoServers => 'SSH設定中未找到伺服器';
|
||||
|
||||
@override
|
||||
String get sshConfigPermissionDenied => '由於 macOS 權限限制,無法存取 SSH 設定檔案。';
|
||||
|
||||
@override
|
||||
String sshConfigServersToImport(Object importCount) {
|
||||
return '將匯入$importCount個伺服器';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sshTermHelp =>
|
||||
'在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。';
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Automatisch verbinden",
|
||||
"autoRun": "Automatischer Start",
|
||||
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
|
||||
"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",
|
||||
"backupPassword": "Backup-Passwort",
|
||||
"backupPasswordRemoved": "Backup-Passwort entfernt",
|
||||
"backupPasswordSet": "Backup-Passwort gesetzt",
|
||||
"backupPasswordTip": "Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.",
|
||||
"backupPasswordWrong": "Falsches Backup-Passwort",
|
||||
"backupTip": "Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.",
|
||||
"backupVersionNotMatch": "Die Backup-Version stimmt nicht überein.",
|
||||
"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\".",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.",
|
||||
"speed": "Tempo",
|
||||
"spentTime": "Benötigte Zeit: {time}",
|
||||
"sshConfigAllExist": "Alle Server existieren bereits ({duplicateCount} Duplikate gefunden)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} Duplikate werden übersprungen",
|
||||
"sshConfigFound": "Wir haben SSH-Konfiguration auf Ihrem System gefunden.",
|
||||
"sshConfigFoundServers": "{totalCount} Server gefunden",
|
||||
"sshConfigImport": "SSH-Konfiguration importieren",
|
||||
"sshConfigImportHelp": "Es können nur Basisinformationen importiert werden, zum Beispiel: IP/Port.",
|
||||
"sshConfigImportPermission": "Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?",
|
||||
"sshConfigImportTip": "Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern",
|
||||
"sshConfigImported": "{count} Server aus SSH-Konfiguration importiert",
|
||||
"sshConfigManualSelect": "Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?",
|
||||
"sshConfigNoServers": "Keine Server in der SSH-Konfiguration gefunden",
|
||||
"sshConfigPermissionDenied": "Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.",
|
||||
"sshConfigServersToImport": "{importCount} Server werden importiert",
|
||||
"sshTermHelp": "Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.",
|
||||
"sshTip": "Diese Funktion befindet sich jetzt in der Experimentierphase.\n\nBitte melde Bugs auf {url} oder mach mit bei der Entwicklung.",
|
||||
"sshVirtualKeyAutoOff": "Automatische Umschaltung der virtuellen Tasten",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Auto connect",
|
||||
"autoRun": "Auto run",
|
||||
"autoUpdateHomeWidget": "Automatic home widget update",
|
||||
"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",
|
||||
"backupPassword": "Backup password",
|
||||
"backupPasswordRemoved": "Backup password removed",
|
||||
"backupPasswordSet": "Backup password set",
|
||||
"backupPasswordTip": "Set a password to encrypt backup files. Leave empty to disable encryption.",
|
||||
"backupPasswordWrong": "Incorrect backup password",
|
||||
"backupTip": "The exported data can be encrypted with password. \nPlease keep it safe.",
|
||||
"backupVersionNotMatch": "Backup version is not match.",
|
||||
"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\".",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "For example, network traffic statistics are by default for all devices. You can specify a particular device here.",
|
||||
"speed": "Speed",
|
||||
"spentTime": "Spent time: {time}",
|
||||
"sshConfigAllExist": "All servers already exist ({duplicateCount} duplicates found)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} duplicates will be skipped",
|
||||
"sshConfigFound": "We found SSH configuration on your system.",
|
||||
"sshConfigFoundServers": "Found {totalCount} servers",
|
||||
"sshConfigImport": "SSH Config Import",
|
||||
"sshConfigImportHelp": "Only basic information can be imported, for example: IP/Port.",
|
||||
"sshConfigImportPermission": "Would you like to give permission to read ~/.ssh/config and automatically import server settings?",
|
||||
"sshConfigImportTip": "Prompt to read ~/.ssh/config on first server creation",
|
||||
"sshConfigImported": "Imported {count} servers from SSH config",
|
||||
"sshConfigManualSelect": "Would you like to select the SSH config file manually?",
|
||||
"sshConfigNoServers": "No servers found in SSH config",
|
||||
"sshConfigPermissionDenied": "Cannot access SSH config file due to macOS permissions.",
|
||||
"sshConfigServersToImport": "{importCount} servers will be imported",
|
||||
"sshTermHelp": "When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.",
|
||||
"sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.",
|
||||
"sshVirtualKeyAutoOff": "Auto switching of virtual keys",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Conexión automática",
|
||||
"autoRun": "Ejecución automática",
|
||||
"autoUpdateHomeWidget": "Actualizar automáticamente el widget del escritorio",
|
||||
"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",
|
||||
"backupPassword": "Contraseña de respaldo",
|
||||
"backupPasswordRemoved": "Contraseña de respaldo eliminada",
|
||||
"backupPasswordSet": "Contraseña de respaldo establecida",
|
||||
"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",
|
||||
"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",
|
||||
"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”.",
|
||||
@@ -180,6 +180,19 @@
|
||||
"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.",
|
||||
"speed": "Velocidad",
|
||||
"spentTime": "Tiempo gastado: {time}",
|
||||
"sshConfigAllExist": "Todos los servidores ya existen (se encontraron {duplicateCount} duplicados)",
|
||||
"sshConfigDuplicatesSkipped": "Se omitirán {duplicateCount} duplicados",
|
||||
"sshConfigFound": "Encontramos configuración SSH en tu sistema",
|
||||
"sshConfigFoundServers": "Se encontraron {totalCount} servidores",
|
||||
"sshConfigImport": "Importar Configuración SSH",
|
||||
"sshConfigImportHelp": "Solo se pueden importar datos básicos, por ejemplo: IP/Puerto.",
|
||||
"sshConfigImportPermission": "¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?",
|
||||
"sshConfigImportTip": "Sugerencia para leer ~/.ssh/config al crear el primer servidor",
|
||||
"sshConfigImported": "Se importaron {count} servidores desde la configuración SSH",
|
||||
"sshConfigManualSelect": "¿Te gustaría seleccionar manualmente el archivo de configuración SSH?",
|
||||
"sshConfigNoServers": "No se encontraron servidores en la configuración SSH",
|
||||
"sshConfigPermissionDenied": "No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.",
|
||||
"sshConfigServersToImport": "Se importarán {importCount} servidores",
|
||||
"sshTermHelp": "Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.",
|
||||
"sshTip": "Esta función está en fase de pruebas.\n\nPor favor, informa los problemas en {url}, o únete a nuestro desarrollo.",
|
||||
"sshVirtualKeyAutoOff": "Desactivación automática de teclas virtuales",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Connexion automatique",
|
||||
"autoRun": "Exécution automatique",
|
||||
"autoUpdateHomeWidget": "Mise à jour automatique du widget d'accueil",
|
||||
"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",
|
||||
"backupPassword": "Mot de passe de sauvegarde",
|
||||
"backupPasswordRemoved": "Mot de passe de sauvegarde supprimé",
|
||||
"backupPasswordSet": "Mot de passe de sauvegarde défini",
|
||||
"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",
|
||||
"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.",
|
||||
"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é ».",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.",
|
||||
"speed": "Vitesse",
|
||||
"spentTime": "Temps écoulé : {time}",
|
||||
"sshConfigAllExist": "Tous les serveurs existent déjà ({duplicateCount} doublons trouvés)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} doublons seront ignorés",
|
||||
"sshConfigFound": "Nous avons trouvé une configuration SSH sur votre système.",
|
||||
"sshConfigFoundServers": "{totalCount} serveurs trouvés",
|
||||
"sshConfigImport": "Importation de configuration SSH",
|
||||
"sshConfigImportHelp": "Seules les informations de base peuvent être importées, par exemple : IP/Port.",
|
||||
"sshConfigImportPermission": "Souhaitez-vous donner la permission de lire ~/.ssh/config et d'importer automatiquement les paramètres du serveur ?",
|
||||
"sshConfigImportTip": "Proposer de lire ~/.ssh/config lors de la première création de serveur",
|
||||
"sshConfigImported": "{count} serveurs importés depuis la configuration SSH",
|
||||
"sshConfigManualSelect": "Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?",
|
||||
"sshConfigNoServers": "Aucun serveur trouvé dans la configuration SSH",
|
||||
"sshConfigPermissionDenied": "Impossible d'accéder au fichier de configuration SSH en raison des permissions macOS.",
|
||||
"sshConfigServersToImport": "{importCount} serveurs seront importés",
|
||||
"sshTermHelp": "Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu'aucun texte n'est sélectionné et qu'il y a du contenu dans le presse-papiers. L'icône de code colle des extraits de code dans le terminal et les exécute.",
|
||||
"sshTip": "Cette fonctionnalité est actuellement à l'étape expérimentale.\n\nVeuillez signaler les bugs sur {url} ou rejoindre notre développement.",
|
||||
"sshVirtualKeyAutoOff": "Activation automatique des touches virtuelles",
|
||||
@@ -236,5 +249,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` \n | `/tmp/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,15 +11,15 @@
|
||||
"autoConnect": "Hubungkan otomatis",
|
||||
"autoRun": "Berjalan Otomatis",
|
||||
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
|
||||
"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",
|
||||
"backupPassword": "Kata sandi cadangan",
|
||||
"backupPasswordRemoved": "Kata sandi cadangan dihapus",
|
||||
"backupPasswordSet": "Kata sandi cadangan ditetapkan",
|
||||
"backupPasswordTip": "Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.",
|
||||
"backupPasswordWrong": "Kata sandi cadangan salah",
|
||||
"backupTip": "Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.",
|
||||
"backupVersionNotMatch": "Versi cadangan tidak cocok.",
|
||||
"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\".",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.",
|
||||
"speed": "Kecepatan",
|
||||
"spentTime": "Menghabiskan waktu: {time}",
|
||||
"sshConfigAllExist": "Semua server sudah ada (ditemukan {duplicateCount} duplikat)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} duplikat akan dilewati",
|
||||
"sshConfigFound": "Kami menemukan konfigurasi SSH di sistem Anda",
|
||||
"sshConfigFoundServers": "Ditemukan {totalCount} server",
|
||||
"sshConfigImport": "Impor Konfigurasi SSH",
|
||||
"sshConfigImportHelp": "Hanya informasi dasar yang dapat diimpor, misalnya: IP/Port.",
|
||||
"sshConfigImportPermission": "Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?",
|
||||
"sshConfigImportTip": "Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama",
|
||||
"sshConfigImported": "Berhasil mengimpor {count} server dari konfigurasi SSH",
|
||||
"sshConfigManualSelect": "Apakah Anda ingin memilih file konfigurasi SSH secara manual?",
|
||||
"sshConfigNoServers": "Tidak ada server yang ditemukan dalam konfigurasi SSH",
|
||||
"sshConfigPermissionDenied": "Tidak dapat mengakses file konfigurasi SSH karena izin macOS.",
|
||||
"sshConfigServersToImport": "{importCount} server akan diimpor",
|
||||
"sshTermHelp": "Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.",
|
||||
"sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.",
|
||||
"sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "自動接続",
|
||||
"autoRun": "自動実行",
|
||||
"autoUpdateHomeWidget": "ホームウィジェットを自動更新",
|
||||
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
|
||||
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
|
||||
"backupPassword": "バックアップパスワード",
|
||||
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
|
||||
"backupPasswordWrong": "バックアップパスワードが間違っています",
|
||||
"backupEncrypted": "バックアップは暗号化されています",
|
||||
"backupNotEncrypted": "バックアップは暗号化されていません",
|
||||
"backupPasswordSet": "バックアップパスワードが設定されました",
|
||||
"backupPassword": "バックアップパスワード",
|
||||
"backupPasswordRemoved": "バックアップパスワードが削除されました",
|
||||
"backupPasswordSet": "バックアップパスワードが設定されました",
|
||||
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
|
||||
"backupPasswordWrong": "バックアップパスワードが間違っています",
|
||||
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
|
||||
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
|
||||
"battery": "バッテリー",
|
||||
"bgRun": "バックグラウンド実行",
|
||||
"bgRunTip": "このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。",
|
||||
"speed": "速度",
|
||||
"spentTime": "費した時間: {time}",
|
||||
"sshConfigAllExist": "すべてのサーバーがすでに存在します({duplicateCount}個の重複が見つかりました)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount}個の重複がスキップされます",
|
||||
"sshConfigFound": "システムにSSH設定が見つかりました。",
|
||||
"sshConfigFoundServers": "{totalCount}個のサーバーが見つかりました",
|
||||
"sshConfigImport": "SSH設定のインポート",
|
||||
"sshConfigImportHelp": "インポートできるのは基本情報のみです。例:IP/ポート。",
|
||||
"sshConfigImportPermission": "~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか?",
|
||||
"sshConfigImportTip": "初回サーバー作成時に~/.ssh/configの読み取りを促す",
|
||||
"sshConfigImported": "SSH設定から{count}個のサーバーをインポートしました",
|
||||
"sshConfigManualSelect": "SSH設定ファイルを手動で選択しますか?",
|
||||
"sshConfigNoServers": "SSH設定でサーバーが見つかりませんでした",
|
||||
"sshConfigPermissionDenied": "macOSの権限により、SSH設定ファイルにアクセスできません。",
|
||||
"sshConfigServersToImport": "{importCount}個のサーバーがインポートされます",
|
||||
"sshTermHelp": "ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。",
|
||||
"sshTip": "この機能は現在テスト段階にあります。\n\n問題がある場合は、{url}でフィードバックしてください。",
|
||||
"sshVirtualKeyAutoOff": "仮想キーの自動オフ",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Automatisch verbinden",
|
||||
"autoRun": "Automatisch uitvoeren",
|
||||
"autoUpdateHomeWidget": "Automatische update van home-widget",
|
||||
"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",
|
||||
"backupPassword": "Back-up wachtwoord",
|
||||
"backupPasswordRemoved": "Back-up wachtwoord verwijderd",
|
||||
"backupPasswordSet": "Back-up wachtwoord ingesteld",
|
||||
"backupPasswordTip": "Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.",
|
||||
"backupPasswordWrong": "Onjuist back-up wachtwoord",
|
||||
"backupTip": "De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.",
|
||||
"backupVersionNotMatch": "Back-upversie komt niet overeen.",
|
||||
"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\".",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.",
|
||||
"speed": "Snelheid",
|
||||
"spentTime": "Gebruikte tijd: {time}",
|
||||
"sshConfigAllExist": "Alle servers bestaan al ({duplicateCount} duplicaten gevonden)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} duplicaten worden overgeslagen",
|
||||
"sshConfigFound": "We hebben SSH-configuratie op uw systeem gevonden",
|
||||
"sshConfigFoundServers": "{totalCount} servers gevonden",
|
||||
"sshConfigImport": "SSH Configuratie Importeren",
|
||||
"sshConfigImportHelp": "Alleen basisinformatie kan worden geïmporteerd, bijvoorbeeld: IP/Poort.",
|
||||
"sshConfigImportPermission": "Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?",
|
||||
"sshConfigImportTip": "Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server",
|
||||
"sshConfigImported": "{count} servers geïmporteerd uit SSH-configuratie",
|
||||
"sshConfigManualSelect": "Wilt u het SSH-configuratiebestand handmatig selecteren?",
|
||||
"sshConfigNoServers": "Geen servers gevonden in SSH-configuratie",
|
||||
"sshConfigPermissionDenied": "Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.",
|
||||
"sshConfigServersToImport": "{importCount} servers worden geïmporteerd",
|
||||
"sshTermHelp": "Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.",
|
||||
"sshTip": "Deze functie bevindt zich momenteel in de experimentele fase.\n\nMeld alstublieft bugs op {url} of sluit je aan bij onze ontwikkeling.",
|
||||
"sshVirtualKeyAutoOff": "Automatisch schakelen van virtuele toetsen",
|
||||
|
||||
@@ -11,15 +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 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",
|
||||
"backupPassword": "Senha de backup",
|
||||
"backupPasswordRemoved": "Senha de backup removida",
|
||||
"backupPasswordSet": "Senha de backup definida",
|
||||
"backupPasswordTip": "Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.",
|
||||
"backupPasswordWrong": "Senha de backup incorreta",
|
||||
"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",
|
||||
"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'.",
|
||||
@@ -180,6 +180,19 @@
|
||||
"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.",
|
||||
"speed": "Velocidade",
|
||||
"spentTime": "Tempo gasto: {time}",
|
||||
"sshConfigAllExist": "Todos os servidores já existem (encontradas {duplicateCount} duplicatas)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} duplicatas serão ignoradas",
|
||||
"sshConfigFound": "Encontramos configuração SSH no seu sistema",
|
||||
"sshConfigFoundServers": "Encontrados {totalCount} servidores",
|
||||
"sshConfigImport": "Importar Configuração SSH",
|
||||
"sshConfigImportHelp": "Só é possível importar informações básicas, por exemplo: IP/Porta.",
|
||||
"sshConfigImportPermission": "Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?",
|
||||
"sshConfigImportTip": "Sugestão para ler ~/.ssh/config na criação do primeiro servidor",
|
||||
"sshConfigImported": "Importados {count} servidores da configuração SSH",
|
||||
"sshConfigManualSelect": "Gostaria de selecionar manualmente o arquivo de configuração SSH?",
|
||||
"sshConfigNoServers": "Nenhum servidor encontrado na configuração SSH",
|
||||
"sshConfigPermissionDenied": "Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.",
|
||||
"sshConfigServersToImport": "{importCount} servidores serão importados",
|
||||
"sshTermHelp": "Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.",
|
||||
"sshTip": "Esta funcionalidade está em fase de teste.\n\nPor favor, reporte problemas em {url} ou junte-se a nós no desenvolvimento.",
|
||||
"sshVirtualKeyAutoOff": "Desativação automática das teclas virtuais",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Автоматическое подключение",
|
||||
"autoRun": "Автозапуск",
|
||||
"autoUpdateHomeWidget": "Автоматическое обновление виджета на главном экране",
|
||||
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
|
||||
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
|
||||
"backupPassword": "Пароль резервной копии",
|
||||
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
|
||||
"backupPasswordWrong": "Неверный пароль резервной копии",
|
||||
"backupEncrypted": "Резервная копия зашифрована",
|
||||
"backupNotEncrypted": "Резервная копия не зашифрована",
|
||||
"backupPasswordSet": "Пароль резервной копии установлен",
|
||||
"backupPassword": "Пароль резервной копии",
|
||||
"backupPasswordRemoved": "Пароль резервной копии удален",
|
||||
"backupPasswordSet": "Пароль резервной копии установлен",
|
||||
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
|
||||
"backupPasswordWrong": "Неверный пароль резервной копии",
|
||||
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
|
||||
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
|
||||
"battery": "Батарея",
|
||||
"bgRun": "Работа в фоновом режиме",
|
||||
"bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.",
|
||||
"speed": "Скорость",
|
||||
"spentTime": "Затрачено времени: {time}",
|
||||
"sshConfigAllExist": "Все серверы уже существуют (найдено {duplicateCount} дубликатов)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} дубликатов будут пропущены",
|
||||
"sshConfigFound": "Мы нашли SSH-конфигурацию в вашей системе",
|
||||
"sshConfigFoundServers": "Найдено {totalCount} серверов",
|
||||
"sshConfigImport": "Импорт SSH Конфигурации",
|
||||
"sshConfigImportHelp": "Можно импортировать только базовую информацию, например: IP/порт.",
|
||||
"sshConfigImportPermission": "Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?",
|
||||
"sshConfigImportTip": "Предложение прочитать ~/.ssh/config при создании первого сервера",
|
||||
"sshConfigImported": "Импортировано {count} серверов из SSH-конфигурации",
|
||||
"sshConfigManualSelect": "Хотели бы вы вручную выбрать файл конфигурации SSH?",
|
||||
"sshConfigNoServers": "Серверы не найдены в SSH-конфигурации",
|
||||
"sshConfigPermissionDenied": "Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.",
|
||||
"sshConfigServersToImport": "{importCount} серверов будут импортированы",
|
||||
"sshTermHelp": "Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.",
|
||||
"sshTip": "Эта функция находится в стадии тестирования.\n\nПожалуйста, отправляйте отчеты о проблемах на {url} или присоединяйтесь к нашей разработке.",
|
||||
"sshVirtualKeyAutoOff": "Автоматическое переключение виртуальных клавиш",
|
||||
|
||||
@@ -11,15 +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 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ı",
|
||||
"backupPassword": "Yedekleme parolası",
|
||||
"backupPasswordRemoved": "Yedekleme parolası kaldırıldı",
|
||||
"backupPasswordSet": "Yedekleme parolası ayarlandı",
|
||||
"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ı",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihaz belirtebilirsiniz.",
|
||||
"speed": "Hız",
|
||||
"spentTime": "Harcanan süre: {time}",
|
||||
"sshConfigAllExist": "Tüm sunucular zaten mevcut ({duplicateCount} kopya bulundu)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} kopya atlanacak",
|
||||
"sshConfigFound": "Sisteminizde SSH yapılandırması bulduk",
|
||||
"sshConfigFoundServers": "{totalCount} sunucu bulundu",
|
||||
"sshConfigImport": "SSH Yapılandırma İçe Aktarma",
|
||||
"sshConfigImportHelp": "Yalnızca temel bilgiler içe aktarılabilir, örneğin: IP/Port.",
|
||||
"sshConfigImportPermission": "~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?",
|
||||
"sshConfigImportTip": "İlk sunucu oluşturulurken ~/.ssh/config okuma istemi",
|
||||
"sshConfigImported": "SSH yapılandırmasından {count} sunucu içe aktarıldı",
|
||||
"sshConfigManualSelect": "SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?",
|
||||
"sshConfigNoServers": "SSH yapılandırmasında sunucu bulunamadı",
|
||||
"sshConfigPermissionDenied": "macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.",
|
||||
"sshConfigServersToImport": "{importCount} sunucu içe aktarılacak",
|
||||
"sshTermHelp": "Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.",
|
||||
"sshTip": "Bu işlev şu anda deneysel aşamada.\n\nLütfen hataları {url} adresinde bildirin veya geliştirmemize katılın.",
|
||||
"sshVirtualKeyAutoOff": "Sanal tuşların otomatik geçişi",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "Авто підключення",
|
||||
"autoRun": "Авто запуск",
|
||||
"autoUpdateHomeWidget": "Автоматичне оновлення віджетів на головному екрані",
|
||||
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
|
||||
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
|
||||
"backupPassword": "Пароль резервного копіювання",
|
||||
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
|
||||
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
|
||||
"backupEncrypted": "Резервна копія зашифрована",
|
||||
"backupNotEncrypted": "Резервна копія не зашифрована",
|
||||
"backupPasswordSet": "Пароль резервного копіювання встановлено",
|
||||
"backupPassword": "Пароль резервного копіювання",
|
||||
"backupPasswordRemoved": "Пароль резервного копіювання видалено",
|
||||
"backupPasswordSet": "Пароль резервного копіювання встановлено",
|
||||
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
|
||||
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
|
||||
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
|
||||
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
|
||||
"battery": "Акумулятор",
|
||||
"bgRun": "Запуск у фоновому режимі",
|
||||
"bgRunTip": "Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.",
|
||||
"speed": "Швидкість",
|
||||
"spentTime": "Витрачений час: {time}",
|
||||
"sshConfigAllExist": "Всі сервери вже існують (знайдено {duplicateCount} дублікатів)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} дублікатів буде пропущено",
|
||||
"sshConfigFound": "Ми знайшли SSH-конфігурацію у вашій системі",
|
||||
"sshConfigFoundServers": "Знайдено {totalCount} серверів",
|
||||
"sshConfigImport": "Імпорт SSH Конфігурації",
|
||||
"sshConfigImportHelp": "Можна імпортувати лише базову інформацію, наприклад: IP/порт.",
|
||||
"sshConfigImportPermission": "Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?",
|
||||
"sshConfigImportTip": "Пропозиція прочитати ~/.ssh/config при створенні першого сервера",
|
||||
"sshConfigImported": "Імпортовано {count} серверів з SSH-конфігурації",
|
||||
"sshConfigManualSelect": "Чи хочете ви вручну вибрати файл конфігурації SSH?",
|
||||
"sshConfigNoServers": "Сервери не знайдені в SSH-конфігурації",
|
||||
"sshConfigPermissionDenied": "Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.",
|
||||
"sshConfigServersToImport": "{importCount} серверів буде імпортовано",
|
||||
"sshTermHelp": "Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.",
|
||||
"sshTip": "Ця функція наразі в експериментальній стадії. Будь ласка, повідомте про помилки за адресою {url} або приєднуйтеся до нашої розробки.",
|
||||
"sshVirtualKeyAutoOff": "Автоматичне переключення віртуальних клавіш",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "自动连接",
|
||||
"autoRun": "自动运行",
|
||||
"autoUpdateHomeWidget": "自动更新桌面小部件",
|
||||
"backupTip": "导出数据可通过密码加密,请妥善保管。",
|
||||
"backupVersionNotMatch": "备份版本不兼容,无法恢复",
|
||||
"backupPassword": "备份密码",
|
||||
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
|
||||
"backupPasswordWrong": "备份密码错误",
|
||||
"backupEncrypted": "备份已加密",
|
||||
"backupNotEncrypted": "备份未加密",
|
||||
"backupPasswordSet": "备份密码已设置",
|
||||
"backupPassword": "备份密码",
|
||||
"backupPasswordRemoved": "备份密码已移除",
|
||||
"backupPasswordSet": "备份密码已设置",
|
||||
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
|
||||
"backupPasswordWrong": "备份密码错误",
|
||||
"backupTip": "导出数据可通过密码加密,请妥善保管。",
|
||||
"backupVersionNotMatch": "备份版本不兼容,无法恢复",
|
||||
"battery": "电池",
|
||||
"bgRun": "后台运行",
|
||||
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”,MIUI / HyperOS 请将省电策略改为“无限制”。",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
|
||||
"speed": "速度",
|
||||
"spentTime": "耗时:{time}",
|
||||
"sshConfigAllExist": "所有服务器已存在(发现 {duplicateCount} 个重复项)",
|
||||
"sshConfigDuplicatesSkipped": "{duplicateCount} 个重复项将被跳过",
|
||||
"sshConfigFound": "我们在您的系统中发现了 SSH 配置。",
|
||||
"sshConfigFoundServers": "发现 {totalCount} 个服务器",
|
||||
"sshConfigImport": "SSH 配置导入",
|
||||
"sshConfigImportHelp": "只能导入基础信息,例如:IP/端口",
|
||||
"sshConfigImportPermission": "是否允许读取 ~/.ssh/config 并自动导入服务器设置?",
|
||||
"sshConfigImportTip": "首次创建服务器时提示读取 ~/.ssh/config",
|
||||
"sshConfigImported": "从 SSH 配置导入了 {count} 个服务器",
|
||||
"sshConfigManualSelect": "是否要手动选择 SSH 配置文件?",
|
||||
"sshConfigNoServers": "SSH 配置中未找到服务器",
|
||||
"sshConfigPermissionDenied": "由于 macOS 权限限制,无法访问 SSH 配置文件。",
|
||||
"sshConfigServersToImport": "{importCount} 个服务器将被导入",
|
||||
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容,在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",
|
||||
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
|
||||
"sshVirtualKeyAutoOff": "虚拟按键自动切换",
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"autoConnect": "自動連線",
|
||||
"autoRun": "自動執行",
|
||||
"autoUpdateHomeWidget": "自動更新桌面小工具",
|
||||
"backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
|
||||
"backupVersionNotMatch": "備份版本不相容,無法還原",
|
||||
"backupPassword": "備份密碼",
|
||||
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
|
||||
"backupPasswordWrong": "備份密碼錯誤",
|
||||
"backupEncrypted": "備份已加密",
|
||||
"backupNotEncrypted": "備份未加密",
|
||||
"backupPasswordSet": "備份密碼已設定",
|
||||
"backupPassword": "備份密碼",
|
||||
"backupPasswordRemoved": "備份密碼已移除",
|
||||
"backupPasswordSet": "備份密碼已設定",
|
||||
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
|
||||
"backupPasswordWrong": "備份密碼錯誤",
|
||||
"backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
|
||||
"backupVersionNotMatch": "備份版本不相容,無法還原",
|
||||
"battery": "電池",
|
||||
"bgRun": "背景執行",
|
||||
"bgRunTip": "此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。",
|
||||
@@ -180,6 +180,19 @@
|
||||
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
|
||||
"speed": "速度",
|
||||
"spentTime": "耗時:{time}",
|
||||
"sshConfigAllExist": "所有伺服器均已存在(發現{duplicateCount}個重複項)",
|
||||
"sshConfigDuplicatesSkipped": "將跳過{duplicateCount}個重複項",
|
||||
"sshConfigFound": "我們在您的系統中發現了SSH設定",
|
||||
"sshConfigFoundServers": "發現{totalCount}個伺服器",
|
||||
"sshConfigImport": "匯入SSH設定",
|
||||
"sshConfigImportHelp": "只能匯入基礎資訊,例如:IP/端口。",
|
||||
"sshConfigImportPermission": "您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定?",
|
||||
"sshConfigImportTip": "在建立第一個伺服器時提示讀取 ~/.ssh/config",
|
||||
"sshConfigImported": "已從SSH設定匯入{count}個伺服器",
|
||||
"sshConfigManualSelect": "是否要手動選擇 SSH 設定檔案?",
|
||||
"sshConfigNoServers": "SSH設定中未找到伺服器",
|
||||
"sshConfigPermissionDenied": "由於 macOS 權限限制,無法存取 SSH 設定檔案。",
|
||||
"sshConfigServersToImport": "將匯入{importCount}個伺服器",
|
||||
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容,在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。",
|
||||
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。",
|
||||
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換",
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:choice/choice.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/core/utils/server_dedup.dart';
|
||||
import 'package:server_box/core/utils/ssh_config.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
@@ -14,6 +18,7 @@ import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/view/page/private_key/edit.dart';
|
||||
|
||||
@@ -121,7 +126,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
}
|
||||
|
||||
Widget _buildForm() {
|
||||
final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan()];
|
||||
final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan(), if (isDesktop) _buildSSHImport()];
|
||||
final children = [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: topItems.joinWith(UIs.width13).toList()),
|
||||
Input(
|
||||
@@ -235,27 +240,27 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 10, right: 15),
|
||||
leading: Radio<int>(value: index),
|
||||
title: Text(e.id, textAlign: TextAlign.start),
|
||||
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
|
||||
trailing: Btn.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)),
|
||||
),
|
||||
onTap: () => _keyIdx.value = index,
|
||||
);
|
||||
});
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(libL10n.add),
|
||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
||||
trailing: const Icon(Icons.add),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||
title: Text(e.id, textAlign: TextAlign.start),
|
||||
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
|
||||
trailing: Btn.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)),
|
||||
),
|
||||
onTap: () => _keyIdx.value = index,
|
||||
);
|
||||
return RadioGroup<int>(
|
||||
onChanged: (val) => _keyIdx.value = val,
|
||||
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
|
||||
);
|
||||
});
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(libL10n.add),
|
||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
||||
trailing: const Icon(Icons.add),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||
),
|
||||
);
|
||||
return RadioGroup<int>(
|
||||
onChanged: (val) => _keyIdx.value = val,
|
||||
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEnvs() {
|
||||
@@ -486,7 +491,10 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
|
||||
Widget _buildJumpServer() {
|
||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||
final srvs = ref.watch(serverNotifierProvider).servers.values
|
||||
final srvs = ref
|
||||
.watch(serverNotifierProvider)
|
||||
.servers
|
||||
.values
|
||||
.where((e) => e.jumpId == null)
|
||||
.where((e) => e.id != spi?.id)
|
||||
.toList();
|
||||
@@ -560,6 +568,16 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSSHImport() {
|
||||
return Btn.tile(
|
||||
text: l10n.sshConfigImport,
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
onTap: _onTapSSHImport,
|
||||
textStyle: UIs.textGrey,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelBtn() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
@@ -584,11 +602,115 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (spi != null) {
|
||||
_initWithSpi(spi!);
|
||||
} else {
|
||||
// Only for new servers, check SSH config import on first time
|
||||
_checkSSHConfigImport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on _ServerEditPageState {
|
||||
extension _Actions on _ServerEditPageState {
|
||||
void _onTapSSHImport() async {
|
||||
try {
|
||||
final servers = await SSHConfig.parseConfig();
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
dprint('Parsed ${servers.length} servers from SSH config');
|
||||
await _processSSHServers(servers);
|
||||
dprint('Finished processing SSH config servers');
|
||||
} catch (e, s) {
|
||||
_handleImportSSHCfgPermissionIssue(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
||||
dprint('Error importing SSH config: $e');
|
||||
// Check if it's a permission error and offer file picker as fallback
|
||||
if (e is PathAccessException || e.toString().contains('Operation not permitted')) {
|
||||
final useFilePicker = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigPermissionDenied),
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.sshConfigManualSelect),
|
||||
],
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
if (useFilePicker == true) {
|
||||
await _onTapSSHImportWithFilePicker();
|
||||
}
|
||||
} else {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processSSHServers(List<Spi> servers) async {
|
||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||
|
||||
if (!summary.hasItemsToImport) {
|
||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldImport = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
||||
if (summary.hasDuplicates)
|
||||
Text(l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'), style: UIs.textGrey),
|
||||
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
||||
const SizedBox(height: 16),
|
||||
...resolved.map((s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})')),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
if (shouldImport == true) {
|
||||
for (final server in resolved) {
|
||||
ref.read(serverNotifierProvider.notifier).addServer(server);
|
||||
}
|
||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTapSSHImportWithFilePicker() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
allowMultiple: false,
|
||||
dialogTitle: 'SSH ${libL10n.select}',
|
||||
);
|
||||
|
||||
if (result?.files.single.path case final path?) {
|
||||
final servers = await SSHConfig.parseConfig(path);
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
await _processSSHServers(servers);
|
||||
}
|
||||
} catch (e, s) {
|
||||
context.showErrDialog(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCustomItem() async {
|
||||
final res = await KvEditor.route.go(context, KvEditorArgs(data: _customCmds.value));
|
||||
if (res == null) return;
|
||||
@@ -604,52 +726,6 @@ extension on _ServerEditPageState {
|
||||
await _showCmdTypesDialog(allCmdTypes);
|
||||
}
|
||||
|
||||
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
||||
return context.showRoundDialog(
|
||||
title: '${libL10n.disabled} ${l10n.cmd}',
|
||||
child: SizedBox(
|
||||
width: 270,
|
||||
child: _disabledCmdTypes.listenVal((disabled) {
|
||||
return ListView.builder(
|
||||
itemCount: allCmdTypes.length,
|
||||
itemExtent: 50,
|
||||
itemBuilder: (context, index) {
|
||||
final cmdType = allCmdTypes.elementAtOrNull(index);
|
||||
if (cmdType == null) return UIs.placeholder;
|
||||
final display = cmdType.displayName;
|
||||
return ListTile(
|
||||
leading: Icon(cmdType.sysType.icon, size: 20),
|
||||
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
|
||||
trailing: Checkbox(
|
||||
value: disabled.contains(display),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
final isDisabled = disabled.contains(display);
|
||||
if (isDisabled) {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSave() async {
|
||||
if (_ipController.text.isEmpty) {
|
||||
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
||||
@@ -705,7 +781,9 @@ extension on _ServerEditPageState {
|
||||
port: int.parse(_portController.text),
|
||||
user: _usernameController.text,
|
||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||
keyId: _keyIdx.value != null ? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id : null,
|
||||
keyId: _keyIdx.value != null
|
||||
? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id
|
||||
: null,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
@@ -731,6 +809,111 @@ extension on _ServerEditPageState {
|
||||
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
extension _Utils on _ServerEditPageState {
|
||||
void _checkSSHConfigImport() async {
|
||||
final prop = Stores.setting.firstTimeReadSSHCfg;
|
||||
// Only check if it's first time and user hasn't disabled it
|
||||
if (!prop.fetch()) return;
|
||||
|
||||
try {
|
||||
// Check if SSH config exists
|
||||
final (_, configExists) = SSHConfig.configExists();
|
||||
if (!configExists) return;
|
||||
|
||||
// Ask for permission
|
||||
final hasPermission = await context.showRoundDialog<bool>(
|
||||
title: l10n.sshConfigImport,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.sshConfigFound),
|
||||
UIs.height7,
|
||||
Text(l10n.sshConfigImportPermission),
|
||||
UIs.height7,
|
||||
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
||||
],
|
||||
),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
|
||||
prop.put(false);
|
||||
|
||||
if (hasPermission == true) {
|
||||
// Parse and import SSH config
|
||||
final servers = await SSHConfig.parseConfig();
|
||||
if (servers.isEmpty) {
|
||||
context.showSnackBar(l10n.sshConfigNoServers);
|
||||
return;
|
||||
}
|
||||
|
||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||
|
||||
if (!summary.hasItemsToImport) {
|
||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Import without asking again since user already gave permission
|
||||
for (final server in resolved) {
|
||||
ref.read(serverNotifierProvider.notifier).addServer(server);
|
||||
}
|
||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||
}
|
||||
} catch (e, s) {
|
||||
_handleImportSSHCfgPermissionIssue(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
||||
return context.showRoundDialog(
|
||||
title: '${libL10n.disabled} ${l10n.cmd}',
|
||||
child: SizedBox(
|
||||
width: 270,
|
||||
child: _disabledCmdTypes.listenVal((disabled) {
|
||||
return ListView.builder(
|
||||
itemCount: allCmdTypes.length,
|
||||
itemExtent: 50,
|
||||
itemBuilder: (context, index) {
|
||||
final cmdType = allCmdTypes.elementAtOrNull(index);
|
||||
if (cmdType == null) return UIs.placeholder;
|
||||
final display = cmdType.displayName;
|
||||
return ListTile(
|
||||
leading: Icon(cmdType.sysType.icon, size: 20),
|
||||
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
|
||||
trailing: Checkbox(
|
||||
value: disabled.contains(display),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
final isDisabled = disabled.contains(display);
|
||||
if (isDisabled) {
|
||||
_disabledCmdTypes.value.remove(display);
|
||||
} else {
|
||||
_disabledCmdTypes.value.add(display);
|
||||
}
|
||||
_disabledCmdTypes.notify();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
}
|
||||
|
||||
void _initWithSpi(Spi spi) {
|
||||
_nameController.text = spi.name;
|
||||
|
||||
@@ -180,6 +180,7 @@ extension _Server on _AppSettingsPageState {
|
||||
_buildDoubleColumnServersPage(),
|
||||
_buildUpdateInterval(),
|
||||
_buildMaxRetry(),
|
||||
_buildSSHConfigImport(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -306,4 +307,12 @@ extension _Server on _AppSettingsPageState {
|
||||
trailing: StoreSwitch(prop: Stores.setting.serverTabPreferDiskAmount),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSSHConfigImport() {
|
||||
return ListTile(
|
||||
title: Text(l10n.sshConfigImport),
|
||||
subtitle: Text(l10n.sshConfigImportTip, style: UIs.textGrey),
|
||||
trailing: StoreSwitch(prop: _setting.firstTimeReadSSHCfg),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
|
||||
<array>
|
||||
<string>.ssh/</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
|
||||
@@ -32,5 +32,9 @@
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSFileProviderExtensionAccessControlPolicy</key>
|
||||
<string>NSFileProviderExtensionAccessControlPolicyRequiresExplicitAccess</string>
|
||||
<key>NSFileProviderExtensionUsageDescription</key>
|
||||
<string>This app needs to access SSH configuration files to import server connection settings from ~/.ssh/config. This allows users to quickly add their existing SSH servers without manual configuration.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
|
||||
<array>
|
||||
<string>.ssh/</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
|
||||
@@ -497,8 +497,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v1.0.343"
|
||||
resolved-ref: a65b7447ac2cc5c25e5a96dc559b1b67a40b2c82
|
||||
ref: "v1.0.344"
|
||||
resolved-ref: "9c7dd603b125fa3ca7a65d466c8fb41383997bd3"
|
||||
url: "https://github.com/lppcg/fl_lib"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
||||
@@ -63,7 +63,7 @@ dependencies:
|
||||
fl_lib:
|
||||
git:
|
||||
url: https://github.com/lppcg/fl_lib
|
||||
ref: v1.0.343
|
||||
ref: v1.0.344
|
||||
flutter_gbk2utf8: ^1.0.1
|
||||
|
||||
dependency_overrides:
|
||||
|
||||
414
test/server_dedup_test.dart
Normal file
414
test/server_dedup_test.dart
Normal file
@@ -0,0 +1,414 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:server_box/core/utils/server_dedup.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
|
||||
// Mock functions to test the deduplication logic without relying on ServerStore
|
||||
List<Spi> _mockDeduplicateServers(List<Spi> importedServers, List<Spi> existingServers) {
|
||||
final deduplicated = <Spi>[];
|
||||
|
||||
for (final imported in importedServers) {
|
||||
// Check against existing servers
|
||||
if (!_mockIsDuplicate(imported, existingServers)) {
|
||||
// Also check against already processed imported servers
|
||||
if (!_mockIsDuplicate(imported, deduplicated)) {
|
||||
deduplicated.add(imported);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicated;
|
||||
}
|
||||
|
||||
bool _mockIsDuplicate(Spi imported, List<Spi> existing) {
|
||||
for (final existingSpi in existing) {
|
||||
// Check for exact match on ip:port@user combination
|
||||
if (existingSpi.ip == imported.ip &&
|
||||
existingSpi.port == imported.port &&
|
||||
existingSpi.user == imported.user) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Spi> _mockResolveNameConflicts(List<Spi> importedServers, List<String> existingNames) {
|
||||
final existingNamesSet = existingNames.toSet();
|
||||
final processedNames = <String>{};
|
||||
final result = <Spi>[];
|
||||
|
||||
for (final server in importedServers) {
|
||||
String newName = server.name;
|
||||
int suffix = 2;
|
||||
|
||||
// Check against both existing servers and already processed servers
|
||||
while (existingNamesSet.contains(newName) || processedNames.contains(newName)) {
|
||||
newName = '${server.name} ($suffix)';
|
||||
suffix++;
|
||||
}
|
||||
|
||||
processedNames.add(newName);
|
||||
|
||||
if (newName != server.name) {
|
||||
result.add(server.copyWith(name: newName));
|
||||
} else {
|
||||
result.add(server);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('ServerDeduplication Tests', () {
|
||||
late List<Spi> existingServers;
|
||||
late List<Spi> importedServers;
|
||||
|
||||
setUp(() {
|
||||
// Set up some existing servers for testing
|
||||
existingServers = [
|
||||
const Spi(
|
||||
name: 'production-web',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 'existing1',
|
||||
),
|
||||
const Spi(
|
||||
name: 'staging-db',
|
||||
ip: '192.168.1.200',
|
||||
port: 22,
|
||||
user: 'postgres',
|
||||
id: 'existing2',
|
||||
),
|
||||
const Spi(
|
||||
name: 'dev-server',
|
||||
ip: '192.168.1.50',
|
||||
port: 2222,
|
||||
user: 'developer',
|
||||
id: 'existing3',
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
test('deduplicateServers removes exact duplicates', () {
|
||||
importedServers = [
|
||||
const Spi(
|
||||
name: 'new-server-1',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root', // Same as existing1
|
||||
),
|
||||
const Spi(
|
||||
name: 'new-server-2',
|
||||
ip: '192.168.1.300',
|
||||
port: 22,
|
||||
user: 'admin', // New server
|
||||
),
|
||||
const Spi(
|
||||
name: 'new-server-3',
|
||||
ip: '192.168.1.200',
|
||||
port: 22,
|
||||
user: 'postgres', // Same as existing2
|
||||
),
|
||||
];
|
||||
|
||||
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
|
||||
|
||||
expect(deduplicated, hasLength(1));
|
||||
expect(deduplicated.first.name, 'new-server-2');
|
||||
expect(deduplicated.first.ip, '192.168.1.300');
|
||||
});
|
||||
|
||||
test('deduplicateServers considers port and user in deduplication', () {
|
||||
importedServers = [
|
||||
const Spi(
|
||||
name: 'same-ip-diff-port',
|
||||
ip: '192.168.1.100',
|
||||
port: 2222, // Different port
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'same-ip-diff-user',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'admin', // Different user
|
||||
),
|
||||
const Spi(
|
||||
name: 'exact-duplicate',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root', // Exact duplicate
|
||||
),
|
||||
];
|
||||
|
||||
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
|
||||
|
||||
expect(deduplicated, hasLength(2));
|
||||
expect(deduplicated.any((s) => s.name == 'same-ip-diff-port'), isTrue);
|
||||
expect(deduplicated.any((s) => s.name == 'same-ip-diff-user'), isTrue);
|
||||
expect(deduplicated.any((s) => s.name == 'exact-duplicate'), isFalse);
|
||||
});
|
||||
|
||||
test('deduplicateServers handles empty existing servers list', () {
|
||||
importedServers = [
|
||||
const Spi(
|
||||
name: 'server1',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'server2',
|
||||
ip: '192.168.1.200',
|
||||
port: 22,
|
||||
user: 'admin',
|
||||
),
|
||||
];
|
||||
|
||||
final deduplicated = _mockDeduplicateServers(importedServers, []);
|
||||
|
||||
expect(deduplicated, hasLength(2));
|
||||
expect(deduplicated, equals(importedServers));
|
||||
});
|
||||
|
||||
test('deduplicateServers handles empty imported servers list', () {
|
||||
final deduplicated = _mockDeduplicateServers([], existingServers);
|
||||
|
||||
expect(deduplicated, isEmpty);
|
||||
});
|
||||
|
||||
test('resolveNameConflicts appends numbers to conflicting names', () {
|
||||
importedServers = [
|
||||
const Spi(
|
||||
name: 'production-web', // Conflicts with existing
|
||||
ip: '192.168.1.300',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'dev-server', // Conflicts with existing
|
||||
ip: '192.168.1.400',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'unique-name', // No conflict
|
||||
ip: '192.168.1.500',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
];
|
||||
|
||||
final resolved = _mockResolveNameConflicts(
|
||||
importedServers,
|
||||
existingServers.map((s) => s.name).toList(),
|
||||
);
|
||||
|
||||
expect(resolved, hasLength(3));
|
||||
expect(resolved[0].name, 'production-web (2)');
|
||||
expect(resolved[1].name, 'dev-server (2)');
|
||||
expect(resolved[2].name, 'unique-name');
|
||||
});
|
||||
|
||||
test('resolveNameConflicts handles multiple conflicts with same base name', () {
|
||||
importedServers = [
|
||||
const Spi(
|
||||
name: 'server',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'server',
|
||||
ip: '192.168.1.200',
|
||||
port: 22,
|
||||
user: 'admin',
|
||||
),
|
||||
const Spi(
|
||||
name: 'server',
|
||||
ip: '192.168.1.300',
|
||||
port: 2222,
|
||||
user: 'root',
|
||||
),
|
||||
];
|
||||
|
||||
final existingNames = ['server', 'server (2)'];
|
||||
final resolved = _mockResolveNameConflicts(importedServers, existingNames);
|
||||
|
||||
expect(resolved, hasLength(3));
|
||||
expect(resolved[0].name, 'server (3)');
|
||||
expect(resolved[1].name, 'server (4)');
|
||||
expect(resolved[2].name, 'server (5)');
|
||||
});
|
||||
|
||||
test('resolveNameConflicts handles empty input', () {
|
||||
final resolved = _mockResolveNameConflicts([], ['existing1', 'existing2']);
|
||||
|
||||
expect(resolved, isEmpty);
|
||||
});
|
||||
|
||||
test('getImportSummary calculates correct statistics', () {
|
||||
final originalList = [
|
||||
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
|
||||
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
|
||||
const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),
|
||||
const Spi(name: 'duplicate', ip: '192.168.1.100', port: 22, user: 'root'), // Duplicate of server1
|
||||
];
|
||||
|
||||
final deduplicatedList = [
|
||||
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
|
||||
const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),
|
||||
];
|
||||
|
||||
final summary = ServerDeduplication.getImportSummary(
|
||||
originalList,
|
||||
deduplicatedList,
|
||||
);
|
||||
|
||||
expect(summary.total, 4);
|
||||
expect(summary.duplicates, 2); // server1 and duplicate
|
||||
expect(summary.toImport, 2);
|
||||
});
|
||||
|
||||
test('getImportSummary handles case with no duplicates', () {
|
||||
final originalList = [
|
||||
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
|
||||
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
|
||||
];
|
||||
|
||||
final summary = ServerDeduplication.getImportSummary(
|
||||
originalList,
|
||||
originalList,
|
||||
);
|
||||
|
||||
expect(summary.total, 2);
|
||||
expect(summary.duplicates, 0);
|
||||
expect(summary.toImport, 2);
|
||||
});
|
||||
|
||||
test('getImportSummary handles case with all duplicates', () {
|
||||
final originalList = [
|
||||
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
|
||||
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
|
||||
];
|
||||
|
||||
final summary = ServerDeduplication.getImportSummary(
|
||||
originalList,
|
||||
[],
|
||||
);
|
||||
|
||||
expect(summary.total, 2);
|
||||
expect(summary.duplicates, 2);
|
||||
expect(summary.toImport, 0);
|
||||
});
|
||||
|
||||
test('getImportSummary handles empty lists', () {
|
||||
final summary = ServerDeduplication.getImportSummary([], []);
|
||||
|
||||
expect(summary.total, 0);
|
||||
expect(summary.duplicates, 0);
|
||||
expect(summary.toImport, 0);
|
||||
});
|
||||
|
||||
test('complete deduplication workflow', () {
|
||||
// Simulate a complete import workflow
|
||||
importedServers = [
|
||||
const Spi(
|
||||
name: 'production-web', // Name conflicts with existing
|
||||
ip: '192.168.1.400', // Different IP, so not a duplicate
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'new-staging',
|
||||
ip: '192.168.1.100', // Same as existing1, should be removed
|
||||
port: 22,
|
||||
user: 'root',
|
||||
),
|
||||
const Spi(
|
||||
name: 'unique-server',
|
||||
ip: '192.168.1.500', // Unique server
|
||||
port: 22,
|
||||
user: 'admin',
|
||||
),
|
||||
];
|
||||
|
||||
// Step 1: Remove duplicates
|
||||
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
|
||||
|
||||
expect(deduplicated, hasLength(2)); // new-staging should be removed
|
||||
|
||||
// Step 2: Resolve name conflicts
|
||||
final resolved = _mockResolveNameConflicts(
|
||||
deduplicated,
|
||||
existingServers.map((s) => s.name).toList(),
|
||||
);
|
||||
|
||||
expect(resolved, hasLength(2));
|
||||
expect(resolved.any((s) => s.name == 'production-web (2)'), isTrue);
|
||||
expect(resolved.any((s) => s.name == 'unique-server'), isTrue);
|
||||
|
||||
// Step 3: Get summary
|
||||
final summary = ServerDeduplication.getImportSummary(
|
||||
importedServers,
|
||||
resolved,
|
||||
);
|
||||
|
||||
expect(summary.total, 3);
|
||||
expect(summary.duplicates, 1);
|
||||
expect(summary.toImport, 2);
|
||||
});
|
||||
|
||||
test('deduplication key generation is consistent', () {
|
||||
const server1 = Spi(
|
||||
name: 'test1',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
);
|
||||
|
||||
const server2 = Spi(
|
||||
name: 'test2', // Different name
|
||||
ip: '192.168.1.100', // Same IP
|
||||
port: 22, // Same port
|
||||
user: 'root', // Same user
|
||||
);
|
||||
|
||||
final servers = [server1, server2];
|
||||
final deduplicated = _mockDeduplicateServers(servers, []);
|
||||
|
||||
// First server should be kept, second should be removed since it has same ip:port@user
|
||||
expect(deduplicated, hasLength(1));
|
||||
expect(deduplicated.first.name, 'test1');
|
||||
});
|
||||
|
||||
test('ImportSummary properties work correctly', () {
|
||||
final summary = ServerDeduplication.getImportSummary(
|
||||
[
|
||||
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
|
||||
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
|
||||
],
|
||||
[
|
||||
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
|
||||
],
|
||||
);
|
||||
|
||||
expect(summary.total, 2);
|
||||
expect(summary.duplicates, 1);
|
||||
expect(summary.toImport, 1);
|
||||
expect(summary.hasDuplicates, isTrue);
|
||||
expect(summary.hasItemsToImport, isTrue);
|
||||
});
|
||||
|
||||
test('ImportSummary with no duplicates or imports', () {
|
||||
final summary = ServerDeduplication.getImportSummary([], []);
|
||||
|
||||
expect(summary.total, 0);
|
||||
expect(summary.duplicates, 0);
|
||||
expect(summary.toImport, 0);
|
||||
expect(summary.hasDuplicates, isFalse);
|
||||
expect(summary.hasItemsToImport, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
317
test/server_edit_logic_test.dart
Normal file
317
test/server_edit_logic_test.dart
Normal file
@@ -0,0 +1,317 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
|
||||
void main() {
|
||||
group('Server Edit Page Logic Tests', () {
|
||||
test('SSH import should only be available on desktop platforms', () {
|
||||
final desktopPlatforms = ['linux', 'macos', 'windows'];
|
||||
final mobilePlatforms = ['android', 'ios', 'fuchsia'];
|
||||
|
||||
for (final platform in desktopPlatforms) {
|
||||
final isDesktop = desktopPlatforms.contains(platform);
|
||||
expect(isDesktop, isTrue, reason: '$platform should support SSH import');
|
||||
}
|
||||
|
||||
for (final platform in mobilePlatforms) {
|
||||
final isDesktop = desktopPlatforms.contains(platform);
|
||||
expect(isDesktop, isFalse, reason: '$platform should not support SSH import');
|
||||
}
|
||||
});
|
||||
|
||||
test('permission prompt conditions are correct', () {
|
||||
// Test the conditions for showing permission prompt
|
||||
|
||||
// Should prompt when: firstTimeReadSSHCfg=true, sshConfigExists=true, isNewServer=true
|
||||
bool shouldPrompt(bool firstTime, bool configExists, bool isNew) {
|
||||
return firstTime && configExists && isNew;
|
||||
}
|
||||
|
||||
expect(shouldPrompt(true, true, true), isTrue); // All conditions met
|
||||
expect(shouldPrompt(false, true, true), isFalse); // Setting disabled
|
||||
expect(shouldPrompt(true, false, true), isFalse); // No config file
|
||||
expect(shouldPrompt(true, true, false), isFalse); // Editing existing server
|
||||
expect(shouldPrompt(false, false, false), isFalse); // No conditions met
|
||||
});
|
||||
|
||||
test('server validation logic works correctly', () {
|
||||
// Test server validation without actual form widgets
|
||||
|
||||
// Valid server
|
||||
const validServer = Spi(
|
||||
name: 'test-server',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
);
|
||||
|
||||
expect(validServer.name.isNotEmpty, isTrue);
|
||||
expect(validServer.ip.isNotEmpty, isTrue);
|
||||
expect(validServer.port > 0 && validServer.port <= 65535, isTrue);
|
||||
expect(validServer.user.isNotEmpty, isTrue);
|
||||
|
||||
// Invalid cases
|
||||
expect(''.isNotEmpty, isFalse); // Empty name
|
||||
expect(0 > 0, isFalse); // Invalid port
|
||||
expect(65536 <= 65535, isFalse); // Port too high
|
||||
});
|
||||
|
||||
test('server form data processing is correct', () {
|
||||
// Test data processing logic
|
||||
|
||||
final Map<String, dynamic> formData = {
|
||||
'name': 'my-server',
|
||||
'ip': '192.168.1.100',
|
||||
'port': '2222',
|
||||
'user': 'admin',
|
||||
};
|
||||
|
||||
// Process form data into server object
|
||||
final server = Spi(
|
||||
name: formData['name'] as String,
|
||||
ip: formData['ip'] as String,
|
||||
port: int.parse(formData['port'] as String),
|
||||
user: formData['user'] as String,
|
||||
);
|
||||
|
||||
expect(server.name, 'my-server');
|
||||
expect(server.ip, '192.168.1.100');
|
||||
expect(server.port, 2222);
|
||||
expect(server.user, 'admin');
|
||||
});
|
||||
|
||||
test('SSH key handling is correct', () {
|
||||
// Test SSH key field handling
|
||||
|
||||
const serverWithKey = Spi(
|
||||
name: 'key-server',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
keyId: '~/.ssh/id_rsa',
|
||||
);
|
||||
|
||||
expect(serverWithKey.keyId, '~/.ssh/id_rsa');
|
||||
expect(serverWithKey.keyId?.isNotEmpty, isTrue);
|
||||
|
||||
const serverWithoutKey = Spi(
|
||||
name: 'pwd-server',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
pwd: 'password123',
|
||||
);
|
||||
|
||||
expect(serverWithoutKey.keyId, isNull);
|
||||
expect(serverWithoutKey.pwd, 'password123');
|
||||
});
|
||||
|
||||
test('server editing vs creation logic', () {
|
||||
// Test logic for distinguishing between editing and creating servers
|
||||
|
||||
const existingServer = Spi(
|
||||
name: 'existing',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: 'server123',
|
||||
);
|
||||
|
||||
// Existing server has non-empty ID
|
||||
final isEditing = existingServer.id.isNotEmpty;
|
||||
final isCreating = !isEditing;
|
||||
|
||||
expect(isEditing, isTrue);
|
||||
expect(isCreating, isFalse);
|
||||
|
||||
const newServer = Spi(
|
||||
name: 'new-server',
|
||||
ip: '192.168.1.100',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
id: '',
|
||||
);
|
||||
|
||||
final isCreatingNew = newServer.id.isEmpty;
|
||||
final isEditingExisting = !isCreatingNew;
|
||||
|
||||
expect(isCreatingNew, isTrue);
|
||||
expect(isEditingExisting, isFalse);
|
||||
});
|
||||
|
||||
test('form field population from imported server', () {
|
||||
// Test that imported server data correctly populates form fields
|
||||
|
||||
const importedServer = Spi(
|
||||
name: 'imported-prod-web',
|
||||
ip: '10.0.1.100',
|
||||
port: 2222,
|
||||
user: 'deploy',
|
||||
keyId: '~/.ssh/production.pem',
|
||||
);
|
||||
|
||||
// Simulate form field population
|
||||
final formFields = {
|
||||
'name': importedServer.name,
|
||||
'ip': importedServer.ip,
|
||||
'port': importedServer.port.toString(),
|
||||
'user': importedServer.user,
|
||||
'keyId': importedServer.keyId,
|
||||
};
|
||||
|
||||
expect(formFields['name'], 'imported-prod-web');
|
||||
expect(formFields['ip'], '10.0.1.100');
|
||||
expect(formFields['port'], '2222');
|
||||
expect(formFields['user'], 'deploy');
|
||||
expect(formFields['keyId'], '~/.ssh/production.pem');
|
||||
});
|
||||
|
||||
test('import summary display logic', () {
|
||||
// Test import summary formatting
|
||||
|
||||
const totalFound = 5;
|
||||
const duplicatesRemoved = 2;
|
||||
const serversToImport = 3;
|
||||
|
||||
final summary = {
|
||||
'total': totalFound,
|
||||
'duplicates': duplicatesRemoved,
|
||||
'toImport': serversToImport,
|
||||
};
|
||||
|
||||
expect(summary['total'], 5);
|
||||
expect(summary['duplicates'], 2);
|
||||
expect(summary['toImport'], 3);
|
||||
|
||||
// Summary validation
|
||||
expect(summary['duplicates']! + summary['toImport']!, summary['total']);
|
||||
|
||||
// Format summary message (simplified)
|
||||
final message = 'Found ${summary['total']} servers, '
|
||||
'${summary['duplicates']} duplicates removed, '
|
||||
'${summary['toImport']} will be imported.';
|
||||
|
||||
expect(message, 'Found 5 servers, 2 duplicates removed, 3 will be imported.');
|
||||
});
|
||||
|
||||
test('error handling logic', () {
|
||||
// Test error handling scenarios
|
||||
|
||||
final errors = <String>[];
|
||||
|
||||
// Validation errors
|
||||
void validateServer(Spi server) {
|
||||
if (server.name.isEmpty) {
|
||||
errors.add('Server name is required');
|
||||
}
|
||||
if (server.ip.isEmpty) {
|
||||
errors.add('Server IP is required');
|
||||
}
|
||||
if (server.port <= 0 || server.port > 65535) {
|
||||
errors.add('Port must be between 1 and 65535');
|
||||
}
|
||||
if (server.user.isEmpty) {
|
||||
errors.add('Username is required');
|
||||
}
|
||||
}
|
||||
|
||||
// Test with invalid server
|
||||
const invalidServer = Spi(
|
||||
name: '',
|
||||
ip: '',
|
||||
port: 0,
|
||||
user: '',
|
||||
);
|
||||
|
||||
validateServer(invalidServer);
|
||||
|
||||
expect(errors.length, 4);
|
||||
expect(errors.contains('Server name is required'), isTrue);
|
||||
expect(errors.contains('Server IP is required'), isTrue);
|
||||
expect(errors.contains('Port must be between 1 and 65535'), isTrue);
|
||||
expect(errors.contains('Username is required'), isTrue);
|
||||
|
||||
// Test with valid server
|
||||
errors.clear();
|
||||
const validServer = Spi(
|
||||
name: 'valid',
|
||||
ip: '192.168.1.1',
|
||||
port: 22,
|
||||
user: 'root',
|
||||
);
|
||||
|
||||
validateServer(validServer);
|
||||
expect(errors.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('name conflict resolution logic', () {
|
||||
// Test name conflict resolution during import
|
||||
|
||||
final existingNames = ['server1', 'server2', 'server3'];
|
||||
|
||||
String resolveNameConflict(String proposedName, List<String> existing) {
|
||||
if (!existing.contains(proposedName)) {
|
||||
return proposedName;
|
||||
}
|
||||
|
||||
int suffix = 2;
|
||||
String newName;
|
||||
do {
|
||||
newName = '$proposedName ($suffix)';
|
||||
suffix++;
|
||||
} while (existing.contains(newName));
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
// Test with no conflict
|
||||
expect(resolveNameConflict('unique-name', existingNames), 'unique-name');
|
||||
|
||||
// Test with conflict
|
||||
expect(resolveNameConflict('server1', existingNames), 'server1 (2)');
|
||||
|
||||
// Test with multiple conflicts
|
||||
final extendedNames = [...existingNames, 'server1 (2)'];
|
||||
expect(resolveNameConflict('server1', extendedNames), 'server1 (3)');
|
||||
});
|
||||
|
||||
test('SSH config import button visibility logic', () {
|
||||
// Test when SSH import button should be visible
|
||||
|
||||
bool shouldShowSSHImport({
|
||||
required bool isDesktop,
|
||||
required bool firstTimeReadSSHCfg,
|
||||
required bool isNewServer,
|
||||
}) {
|
||||
return isDesktop && (firstTimeReadSSHCfg || !isNewServer);
|
||||
}
|
||||
|
||||
// Desktop, first time, new server - should show
|
||||
expect(shouldShowSSHImport(
|
||||
isDesktop: true,
|
||||
firstTimeReadSSHCfg: true,
|
||||
isNewServer: true,
|
||||
), isTrue);
|
||||
|
||||
// Desktop, not first time, new server - should not show auto import but manual import available
|
||||
expect(shouldShowSSHImport(
|
||||
isDesktop: true,
|
||||
firstTimeReadSSHCfg: false,
|
||||
isNewServer: true,
|
||||
), isFalse);
|
||||
|
||||
// Desktop, editing existing server - should show manual import
|
||||
expect(shouldShowSSHImport(
|
||||
isDesktop: true,
|
||||
firstTimeReadSSHCfg: false,
|
||||
isNewServer: false,
|
||||
), isTrue);
|
||||
|
||||
// Mobile - should never show
|
||||
expect(shouldShowSSHImport(
|
||||
isDesktop: false,
|
||||
firstTimeReadSSHCfg: true,
|
||||
isNewServer: true,
|
||||
), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
355
test/ssh_config_test.dart
Normal file
355
test/ssh_config_test.dart
Normal file
@@ -0,0 +1,355 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:server_box/core/utils/ssh_config.dart';
|
||||
|
||||
void main() {
|
||||
group('SSHConfig Tests', () {
|
||||
late Directory tempDir;
|
||||
late File configFile;
|
||||
|
||||
setUp(() async {
|
||||
// Create temporary directory for test SSH config files
|
||||
tempDir = await Directory.systemTemp.createTemp('ssh_config_test');
|
||||
configFile = File('${tempDir.path}/config');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
// Clean up temporary files
|
||||
if (tempDir.existsSync()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
test('configExists returns false for non-existent file', () async {
|
||||
final (_, exists) = SSHConfig.configExists('/non/existent/path');
|
||||
expect(exists, false);
|
||||
});
|
||||
|
||||
test('configExists returns true for existing file', () async {
|
||||
await configFile.writeAsString('Host example\n HostName example.com\n');
|
||||
final (_, exists) = SSHConfig.configExists(configFile.path);
|
||||
expect(exists, true);
|
||||
});
|
||||
|
||||
test('parseConfig handles empty file', () async {
|
||||
await configFile.writeAsString('');
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, isEmpty);
|
||||
});
|
||||
|
||||
test('parseConfig handles file with only comments', () async {
|
||||
await configFile.writeAsString('''
|
||||
# This is a comment
|
||||
# Another comment
|
||||
''');
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, isEmpty);
|
||||
});
|
||||
|
||||
test('parseConfig parses single host correctly', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host myserver
|
||||
HostName 192.168.1.100
|
||||
User admin
|
||||
Port 2222
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1));
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.name, 'myserver');
|
||||
expect(server.ip, '192.168.1.100');
|
||||
expect(server.user, 'admin');
|
||||
expect(server.port, 2222);
|
||||
});
|
||||
|
||||
test('parseConfig handles missing HostName', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host myserver
|
||||
User admin
|
||||
Port 2222
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, isEmpty); // Should skip hosts without HostName
|
||||
});
|
||||
|
||||
test('parseConfig uses defaults for missing optional fields', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host simple
|
||||
HostName example.com
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1));
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.name, 'simple');
|
||||
expect(server.ip, 'example.com');
|
||||
expect(server.user, 'root'); // default user
|
||||
expect(server.port, 22); // default port
|
||||
});
|
||||
|
||||
test('parseConfig handles multiple hosts', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host server1
|
||||
HostName 192.168.1.100
|
||||
User alice
|
||||
Port 22
|
||||
|
||||
Host server2
|
||||
HostName 192.168.1.200
|
||||
User bob
|
||||
Port 2222
|
||||
|
||||
Host server3
|
||||
HostName example.com
|
||||
User charlie
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(3));
|
||||
|
||||
expect(servers[0].name, 'server1');
|
||||
expect(servers[0].ip, '192.168.1.100');
|
||||
expect(servers[0].user, 'alice');
|
||||
expect(servers[0].port, 22);
|
||||
|
||||
expect(servers[1].name, 'server2');
|
||||
expect(servers[1].ip, '192.168.1.200');
|
||||
expect(servers[1].user, 'bob');
|
||||
expect(servers[1].port, 2222);
|
||||
|
||||
expect(servers[2].name, 'server3');
|
||||
expect(servers[2].ip, 'example.com');
|
||||
expect(servers[2].user, 'charlie');
|
||||
expect(servers[2].port, 22);
|
||||
});
|
||||
|
||||
test('parseConfig handles case insensitive keywords', () async {
|
||||
await configFile.writeAsString('''
|
||||
host myserver
|
||||
hostname 192.168.1.100
|
||||
user admin
|
||||
port 2222
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1));
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.name, 'myserver');
|
||||
expect(server.ip, '192.168.1.100');
|
||||
expect(server.user, 'admin');
|
||||
expect(server.port, 2222);
|
||||
});
|
||||
|
||||
test('parseConfig handles comments and empty lines', () async {
|
||||
await configFile.writeAsString('''
|
||||
# Global settings
|
||||
Host *
|
||||
ServerAliveInterval 60
|
||||
|
||||
# My development server
|
||||
Host devserver
|
||||
HostName 192.168.1.50
|
||||
User developer # development user
|
||||
Port 22
|
||||
|
||||
# Empty line below
|
||||
|
||||
Host prodserver
|
||||
HostName 10.0.0.100
|
||||
User production
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(2));
|
||||
|
||||
expect(servers[0].name, 'devserver');
|
||||
expect(servers[0].ip, '192.168.1.50');
|
||||
expect(servers[0].user, 'developer');
|
||||
|
||||
expect(servers[1].name, 'prodserver');
|
||||
expect(servers[1].ip, '10.0.0.100');
|
||||
expect(servers[1].user, 'production');
|
||||
});
|
||||
|
||||
test('parseConfig handles wildcard hosts', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host *
|
||||
User defaultuser
|
||||
Port 2222
|
||||
|
||||
Host myserver
|
||||
HostName 192.168.1.100
|
||||
User admin
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1)); // Only named hosts, not wildcards
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.name, 'myserver');
|
||||
expect(server.ip, '192.168.1.100');
|
||||
expect(server.user, 'admin');
|
||||
expect(server.port, 22); // Uses default, not wildcard setting
|
||||
});
|
||||
|
||||
test('parseConfig handles IdentityFile', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host keyserver
|
||||
HostName 192.168.1.100
|
||||
User admin
|
||||
IdentityFile ~/.ssh/special_key
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1));
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.keyId, '~/.ssh/special_key');
|
||||
});
|
||||
|
||||
test('parseConfig handles quoted values', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host "server with spaces"
|
||||
HostName "192.168.1.100"
|
||||
User "admin user"
|
||||
IdentityFile "~/.ssh/key with spaces"
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1));
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.name, 'server with spaces');
|
||||
expect(server.ip, '192.168.1.100');
|
||||
expect(server.user, 'admin user');
|
||||
expect(server.keyId, '~/.ssh/key with spaces');
|
||||
});
|
||||
|
||||
test('parseConfig handles invalid port values', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host badport
|
||||
HostName 192.168.1.100
|
||||
Port notanumber
|
||||
|
||||
Host goodserver
|
||||
HostName 192.168.1.200
|
||||
Port 2222
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(2));
|
||||
|
||||
// First server should use default port due to invalid port
|
||||
expect(servers[0].name, 'badport');
|
||||
expect(servers[0].port, 22); // default port
|
||||
|
||||
// Second server should use specified port
|
||||
expect(servers[1].name, 'goodserver');
|
||||
expect(servers[1].port, 2222);
|
||||
});
|
||||
|
||||
test('parseConfig skips hosts with multiple host patterns', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host server1 server2
|
||||
HostName 192.168.1.100
|
||||
|
||||
Host singleserver
|
||||
HostName 192.168.1.200
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1)); // Only single host patterns
|
||||
|
||||
expect(servers[0].name, 'singleserver');
|
||||
});
|
||||
|
||||
test('parseConfig handles ProxyJump (ignored)', () async {
|
||||
await configFile.writeAsString('''
|
||||
Host jumpserver
|
||||
HostName 192.168.1.100
|
||||
User admin
|
||||
ProxyJump bastion.example.com
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(1));
|
||||
|
||||
final server = servers.first;
|
||||
expect(server.name, 'jumpserver');
|
||||
expect(server.ip, '192.168.1.100');
|
||||
expect(server.user, 'admin');
|
||||
// ProxyJump is ignored in current implementation
|
||||
});
|
||||
|
||||
test('parseConfig returns empty list for non-existent file', () async {
|
||||
final servers = await SSHConfig.parseConfig('/non/existent/path');
|
||||
expect(servers, isEmpty);
|
||||
});
|
||||
|
||||
test('parseConfig handles real-world SSH config example', () async {
|
||||
await configFile.writeAsString('''
|
||||
# Default settings for all hosts
|
||||
Host *
|
||||
ServerAliveInterval 60
|
||||
ServerAliveCountMax 3
|
||||
TCPKeepAlive yes
|
||||
|
||||
# Production servers
|
||||
Host prod-web-01
|
||||
HostName 10.0.1.100
|
||||
User deploy
|
||||
Port 22
|
||||
IdentityFile ~/.ssh/production.pem
|
||||
|
||||
Host prod-db-01
|
||||
HostName 10.0.1.200
|
||||
User ubuntu
|
||||
Port 2222
|
||||
IdentityFile ~/.ssh/production.pem
|
||||
|
||||
# Development environment
|
||||
Host dev
|
||||
HostName dev.example.com
|
||||
User developer
|
||||
Port 22
|
||||
|
||||
# Jump host configuration
|
||||
Host bastion
|
||||
HostName bastion.example.com
|
||||
User ec2-user
|
||||
IdentityFile ~/.ssh/bastion.pem
|
||||
|
||||
Host internal-server
|
||||
HostName 172.16.0.50
|
||||
User admin
|
||||
ProxyJump bastion
|
||||
''');
|
||||
|
||||
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||
expect(servers, hasLength(5));
|
||||
|
||||
// Check specific servers
|
||||
final prodWeb = servers.firstWhere((s) => s.name == 'prod-web-01');
|
||||
expect(prodWeb.ip, '10.0.1.100');
|
||||
expect(prodWeb.user, 'deploy');
|
||||
expect(prodWeb.port, 22);
|
||||
expect(prodWeb.keyId, '~/.ssh/production.pem');
|
||||
|
||||
final prodDb = servers.firstWhere((s) => s.name == 'prod-db-01');
|
||||
expect(prodDb.ip, '10.0.1.200');
|
||||
expect(prodDb.user, 'ubuntu');
|
||||
expect(prodDb.port, 2222);
|
||||
|
||||
final dev = servers.firstWhere((s) => s.name == 'dev');
|
||||
expect(dev.ip, 'dev.example.com');
|
||||
expect(dev.user, 'developer');
|
||||
expect(dev.port, 22);
|
||||
expect(dev.keyId, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user