Compare commits

..

42 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
860c11d4a8 bump: v1262 2025-10-10 09:18:38 +08:00
lollipopkit🏳️‍⚧️
bd949288ed fix: code editor tool bar (#933) 2025-10-10 09:14:41 +08:00
lollipopkit🏳️‍⚧️
bb3e3b4848 opt.: no Tag Switcher on desktop (#932) 2025-10-08 21:21:23 +08:00
lollipopkit🏳️‍⚧️
3307fca620 fix: cant sort servers order (#930) 2025-10-08 17:35:07 +08:00
lollipopkit🏳️‍⚧️
da8517bcf7 migrate: riverpod 3 2025-10-08 17:03:13 +08:00
lollipopkit🏳️‍⚧️
f68c4a851b feat: discover local ssh server (#921) 2025-09-19 23:29:01 +08:00
lollipopkit🏳️‍⚧️
17db393c12 bump: v1256 2025-09-15 02:35:42 +08:00
lollipopkit🏳️‍⚧️
275581cfa3 fix: notification permission (#914) 2025-09-14 22:34:01 +08:00
lollipopkit🏳️‍⚧️
d7168ea1ff fix: version code err caused by Flutter (#913) 2025-09-14 14:23:17 +08:00
lollipopkit🏳️‍⚧️
fd2bf08f78 bump: v1253 2025-09-09 13:32:17 +08:00
lollipopkit🏳️‍⚧️
98e13c39cf fix: android channel invoke 2025-09-09 13:30:37 +08:00
lollipopkit🏳️‍⚧️
e70abeef04 bump: v1251 2025-09-09 13:14:01 +08:00
lollipopkit🏳️‍⚧️
194774d6fb opt.: system detect logic to avoid creating useless file (#905) 2025-09-09 13:10:40 +08:00
lollipopkit🏳️‍⚧️
640d61bab9 fix: holding Backspace doesnt work on desktop (#903) 2025-09-08 14:06:35 +08:00
lollipopkit🏳️‍⚧️
7f4cf22cc9 fix: rm camera perm on mac 2025-09-08 12:37:30 +08:00
lollipopkit🏳️‍⚧️
05a927753f feat: stop all servers in noti center (#901) 2025-09-06 14:04:53 +08:00
lollipopkit🏳️‍⚧️
0c7b72fb2c bump: v1246 2025-09-05 12:31:33 +08:00
lollipopkit🏳️‍⚧️
a869b97502 fix: server stat l10n 2025-09-05 00:24:18 +08:00
lollipopkit🏳️‍⚧️
eadd343205 readd: home drawer
Fixes #900
2025-09-05 00:12:41 +08:00
lollipopkit🏳️‍⚧️
1bac986fe0 bug: single server providers should be keepalived (#899) 2025-09-04 23:50:00 +08:00
lollipopkit🏳️‍⚧️
a94be6c2c3 fix: macOS appstore rejection (#893) 2025-09-03 22:19:04 +08:00
lollipopkit🏳️‍⚧️
fc8e9b4bb1 bump: v1241 2025-09-03 09:24:33 +08:00
lollipopkit🏳️‍⚧️
ec4b633889 fix: watchOS app cfg (#890) 2025-09-03 01:41:08 +08:00
lollipopkit🏳️‍⚧️
e51804fa70 new: custom tabs (#889) 2025-09-03 01:05:03 +08:00
lollipopkit🏳️‍⚧️
2466341999 feat: server conn statistics (#888) 2025-09-02 19:41:56 +08:00
lollipopkit🏳️‍⚧️
929061213f refactor: docker status parsing (#886) 2025-09-02 13:22:54 +08:00
lollipopkit🏳️‍⚧️
6b52679942 fix: resolve Docker interface blank issue caused by LateInitializationError (#884) 2025-09-02 12:44:05 +08:00
lollipopkit🏳️‍⚧️
efc0315c93 new: CLAUDE.md 2025-09-01 23:32:44 +08:00
lollipopkit🏳️‍⚧️
8e4c2a7cde fix: fallback to df on incompatible system (#880) 2025-09-01 23:32:20 +08:00
lollipopkit🏳️‍⚧️
4ec7f5895e fix: imported servers from ssh config are the same (#882) 2025-09-01 23:06:58 +08:00
lollipopkit🏳️‍⚧️
ee22cdb55f fix: private key can't be selected in edit page (#879) 2025-09-01 13:05:54 +08:00
lollipopkit🏳️‍⚧️
b1b0d9a18f bump: v1231 2025-09-01 01:19:23 +08:00
lollipopkit🏳️‍⚧️
56e67f4725 fix: sync will refresh the entire app (#877) 2025-09-01 01:18:06 +08:00
lollipopkit🏳️‍⚧️
3b7fdf36fb opt. 2025-08-31 23:59:53 +08:00
lollipopkit🏳️‍⚧️
5291d316a2 fix: ensure unique IDs for bulk server import to prevent overwriting (#875) 2025-08-31 21:20:27 +08:00
lollipopkit🏳️‍⚧️
4c369546da fix: replace String.fromCharCodes with utf8.decode for proper Chinese character handling in JSON import (#874) 2025-08-31 20:06:47 +08:00
lollipopkit🏳️‍⚧️
12a243d139 feat: import servers from ~/.ssh/config (#873) 2025-08-31 19:33:29 +08:00
lollipopkit🏳️‍⚧️
a97b3cf43e opt.: bak pwd is optional (#872) 2025-08-31 11:11:47 +08:00
lollipopkit🏳️‍⚧️
53a7c0d8ff migrate: riverpod + freezed (#870) 2025-08-31 00:55:54 +08:00
lollipopkit🏳️‍⚧️
9cb705f8dd fix: parsing hostname (#865) 2025-08-22 09:18:21 +08:00
lollipopkit🏳️‍⚧️
8270674b7d chore: tests 2025-08-22 00:25:26 +08:00
lxdklp
24fd4b782d fix: GBK decoding fallback (#863)
* fix #757

* fix #757

* apply the code recommendations from sourcery ai

* Make sure raw is non-empty data

* Modified the way to judge gbk, fixed the problem that null cannot throw an error
2025-08-21 23:28:06 +08:00
162 changed files with 16009 additions and 3676 deletions

View File

@@ -17,17 +17,30 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: subosito/flutter-action@v2
with:
channel: 'stable' # or: 'beta', 'dev' or 'master'
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- name: Cache pub dependencies
uses: actions/cache@v4
with:
path: |
${{ env.PUB_CACHE }}
~/.pub-cache
.dart_tool/package_config.json
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-${{ hashFiles('**/pubspec.yaml') }}
restore-keys: |
${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-
${{ runner.os }}-pub-
- name: Install dependencies
run: flutter pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
- name: Verify formatting
run: dart format --output=none .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
run: dart analyze

View File

@@ -9,6 +9,11 @@ on:
permissions:
contents: write
# Set by fl_build
# env:
# APP_NAME: ServerBox
# BUILD_NUMBER: ${{ github.ref_name }}
jobs:
releaseAndroid:
name: Release android
@@ -20,7 +25,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.35.1"
flutter-version: "3.35.3"
- uses: actions/setup-java@v4
with:
distribution: "zulu"
@@ -98,16 +103,12 @@ jobs:
# uses: actions/checkout@v4
# - name: Install Flutter
# uses: subosito/flutter-action@v2
# with:
# channel: 'stable'
# flutter-version: '3.32.1'
# - name: Build
# run: dart run fl_build -p ios,mac
# run: dart run fl_build -p ios
# - name: Create Release
# uses: softprops/action-gh-release@v2
# with:
# files: |
# ${{ env.APP_NAME }}_universal_macos.zip
# ${{ env.APP_NAME }}_universal.ipa
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

95
CLAUDE.md Normal file
View File

@@ -0,0 +1,95 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Development
- `flutter run` - Run the app in development mode
- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)
- `dart run build_runner build --delete-conflicting-outputs` - Generate code for models with annotations (json_serializable, freezed, hive, riverpod)
- Every time you change model files, run this command to regenerate code (Hive adapters, Riverpod providers, etc.)
- Generated files include: `*.g.dart`, `*.freezed.dart` files
### Testing
- `flutter test` - Run unit tests
- `flutter test test/battery_test.dart` - Run specific test file
## Architecture
This is a Flutter application for managing Linux servers with the following key architectural components:
### Project Structure
- `lib/core/` - Core utilities, extensions, and routing
- `lib/data/` - Data layer with models, providers, and storage
- `model/` - Data models organized by feature (server, container, ssh, etc.)
- `provider/` - Riverpod providers for state management
- `store/` - Local storage implementations using Hive
- `lib/view/` - UI layer with pages and widgets
- `lib/generated/` - Generated localization files
- `lib/hive/` - Hive adapters for local storage
### Key Technologies
- **State Management**: Riverpod with code generation (riverpod_annotation)
- **Local Storage**: Hive for persistent data with generated adapters
- **SSH/SFTP**: Custom dartssh2 fork for server connections
- **Terminal**: Custom xterm.dart fork for SSH terminal interface
- **Networking**: dio for HTTP requests
- **Charts**: fl_chart for server status visualization
- **Localization**: Flutter's built-in i18n with ARB files
- **Code Generation**: Uses build_runner with json_serializable, freezed, hive_generator, riverpod_generator
### Data Models
- Server management models in `lib/data/model/server/`
- Container/Docker models in `lib/data/model/container/`
- SSH and SFTP models in respective directories
- Most models use freezed for immutability and json_annotation for serialization
### Features
- Server status monitoring (CPU, memory, disk, network)
- SSH terminal with virtual keyboard
- SFTP file browser
- Docker container management
- Process and systemd service management
- Server snippets and custom commands
- Multi-language support (12+ languages)
- Cross-platform support (iOS, Android, macOS, Linux, Windows)
### State Management Pattern
- Uses Riverpod providers for dependency injection and state management
- Uses Freezed for immutable state models
- Providers are organized by feature in `lib/data/provider/`
- State is often persisted using Hive stores in `lib/data/store/`
### Build System
- Uses custom `fl_build` package for cross-platform building
- `make.dart` script handles pre/post build tasks (metadata generation)
- Supports building for multiple platforms with platform-specific configurations
- Many dependencies are custom forks hosted on GitHub (dartssh2, xterm, fl_lib, etc.)
### Important Notes
- **Never run code formatting commands** - The codebase has specific formatting that should not be changed
- **Always run code generation** after modifying models with annotations (freezed, json_serializable, hive, riverpod)
- Generated files (`*.g.dart`, `*.freezed.dart`) should not be manually edited
- AGAIN, NEVER run code formatting commands.
- USE dependency injection via GetIt for services like Stores, Services and etc.
- Generate all l10n files using `flutter gen-l10n` command after modifying ARB files.
- USE `hive_ce` not `hive` package for Hive integration.
- Which no need to config `HiveField` and `HiveType` manually.
- USE widgets and utilities from `fl_lib` package for common functionalities.
- Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc.
- You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities.
- USE `libL10n` and `l10n` for localization strings.
- `libL10n` is from `fl_lib` package, and `l10n` is from this project.
- Before adding new strings, check if it already exists in `libL10n`.
- Prioritize using strings from `libL10n` to avoid duplication, even if the meaning is not 100% exact, just use the substitution of `libL10n`.
- Split UI into Widget build, Actions, Utils. use `extension on` to achieve this

View File

@@ -113,7 +113,7 @@ android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
output.versionCodeOverride = variant.versionCode * 100 + abiVersionCode
}
}
}

View File

@@ -2,6 +2,8 @@ package tech.lolli.toolbox
import android.app.*
import android.content.Intent
import android.content.pm.ServiceInfo
import android.graphics.drawable.Icon
import android.os.Build
import android.os.IBinder
import android.util.Log
@@ -16,8 +18,7 @@ class ForegroundService : Service() {
var isRunning: Boolean = false
}
private val chanId = "ForegroundServiceChannel"
private val GROUP_KEY = "ssh_sessions_group"
private val SUMMARY_ID = 1000
private val NOTIFICATION_ID = 1000
private val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
@@ -49,19 +50,22 @@ class ForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
// Check notification permission for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
androidx.core.content.ContextCompat.checkSelfPermission(
this, android.Manifest.permission.POST_NOTIFICATIONS
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
Log.w("ForegroundService", "Notification permission denied. Stopping service.")
stopForegroundService()
Log.w("ForegroundService", "Notification permission denied. Stopping service gracefully.")
// Don't call stopForegroundService() here as we haven't started foreground yet
stopSelf()
return START_NOT_STICKY
}
if (intent == null) {
Log.w("ForegroundService", "onStartCommand called with null intent")
stopForegroundService()
// Don't call stopForegroundService() here as we haven't started foreground yet
stopSelf()
return START_NOT_STICKY
}
@@ -70,6 +74,9 @@ class ForegroundService : Service() {
return when (action) {
ACTION_STOP_FOREGROUND -> {
// Notify Flutter to stop all connections before stopping service
val stopAllIntent = Intent("tech.lolli.toolbox.STOP_ALL_CONNECTIONS")
sendBroadcast(stopAllIntent)
clearAll()
stopForegroundService()
START_NOT_STICKY
@@ -81,7 +88,7 @@ class ForegroundService : Service() {
}
else -> {
// Default bring up foreground with placeholder
ensureForeground(createSummaryNotification(0, emptyList()))
ensureForeground(createMergedNotification(0, emptyList(), emptyList()))
START_STICKY
}
}
@@ -99,37 +106,67 @@ class ForegroundService : Service() {
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
if (manager == null) {
Log.e("ForegroundService", "Failed to get NotificationManager")
return
try {
val manager = getSystemService(NotificationManager::class.java)
if (manager == null) {
Log.e("ForegroundService", "Failed to get NotificationManager")
return
}
val serviceChannel = NotificationChannel(
chanId,
"ForegroundServiceChannel",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "For foreground service"
}
manager.createNotificationChannel(serviceChannel)
Log.d("ForegroundService", "Notification channel created successfully")
} catch (e: Exception) {
logError("Failed to create notification channel", e)
}
val serviceChannel = NotificationChannel(
chanId,
"ForegroundServiceChannel",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "For foreground service"
}
manager.createNotificationChannel(serviceChannel)
}
}
private fun ensureForeground(notification: Notification) {
try {
// Double-check notification permission before starting foreground service
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
androidx.core.content.ContextCompat.checkSelfPermission(
this, android.Manifest.permission.POST_NOTIFICATIONS
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
Log.w("ForegroundService", "Cannot start foreground service without notification permission")
stopSelf()
return
}
if (!isFgStarted) {
startForeground(SUMMARY_ID, notification)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIFICATION_ID, notification)
}
isFgStarted = true
Log.d("ForegroundService", "Foreground service started successfully")
} else {
val nm = getSystemService(NotificationManager::class.java)
nm?.notify(SUMMARY_ID, notification)
if (nm != null) {
nm.notify(NOTIFICATION_ID, notification)
} else {
Log.w("ForegroundService", "NotificationManager is null, cannot update notification")
}
}
} catch (e: SecurityException) {
logError("Security exception when starting foreground service (likely missing permission)", e)
stopSelf()
} catch (e: Exception) {
logError("Failed to start/update foreground", e)
// Don't stop the service for other exceptions, just log them
}
}
private fun createSummaryNotification(count: Int, lines: List<String>): Notification {
private fun createMergedNotification(count: Int, lines: List<String>, sessions: List<SessionItem>): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
@@ -140,24 +177,66 @@ class ForegroundService : Service() {
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
}
val inbox = Notification.InboxStyle()
lines.forEach { inbox.addLine(it) }
// Use the earliest session's start time for chronometer
val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis()
return builder
.setContentTitle("SSH sessions: $count active")
.setContentText(if (lines.isNotEmpty()) lines.first() else "Running")
val title = when (count) {
0 -> "Server Box"
1 -> sessions.first().title
else -> "SSH sessions: $count active"
}
val contentText = when (count) {
0 -> "Ready for connections"
1 -> {
val session = sessions.first()
"${session.subtitle} · ${session.status}"
}
else -> "Multiple SSH connections active"
}
// For multiple sessions, show details in expanded view
val style = if (count > 1) {
val inbox = Notification.InboxStyle()
val maxLines = 5
val displayLines = if (lines.size > maxLines) {
lines.take(maxLines) + "...and ${lines.size - maxLines} more"
} else {
lines
}
displayLines.forEach { inbox.addLine(it) }
inbox.setBigContentTitle(title)
inbox
} else {
null
}
val notification = builder
.setContentTitle(title)
.setContentText(contentText)
.setSmallIcon(R.mipmap.ic_launcher)
.setStyle(inbox)
.setWhen(earliestStartTime)
.setUsesChronometer(true)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setGroup(GROUP_KEY)
.setGroupSummary(true)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", stopPending)
.build()
.addAction(
Notification.Action.Builder(
Icon.createWithResource(this, android.R.drawable.ic_delete),
"Stop All",
stopPending
).build()
)
if (style != null) {
notification.setStyle(style)
}
return notification.build()
}
private fun handleUpdateSessions(payload: String) {
@@ -192,71 +271,21 @@ class ForegroundService : Service() {
return
}
// Build per-session notifications
val currentIds = mutableSetOf<Int>()
val summaryLines = mutableListOf<String>()
sessions.forEach { s ->
// Assign a stable, collision-resistant id per session for this service lifecycle
val nid = notificationIdMap.getOrPut(s.id) { nextNotificationId.getAndIncrement() }
currentIds.add(nid)
summaryLines.add("${s.title}: ${s.status}")
val disconnectIntent = Intent(this, MainActivity::class.java).apply {
action = ACTION_DISCONNECT_SESSION
putExtra("session_id", s.id)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
val disconnectPending = PendingIntent.getActivity(
this, nid, disconnectIntent, PendingIntent.FLAG_IMMUTABLE
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
} else {
Notification.Builder(this)
}
val noti = builder
.setContentTitle(s.title)
.setContentText("${s.subtitle} · ${s.status}")
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(s.startWhen)
.setUsesChronometer(true)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setGroup(GROUP_KEY)
.addAction(android.R.drawable.ic_media_pause, "Disconnect", disconnectPending)
.build()
nm.notify(nid, noti)
}
// Cancel stale ones
val toCancel = postedIds - currentIds
// Cancel any existing individual notifications (we only show merged notification now)
val toCancel = postedIds.toSet()
toCancel.forEach { nm.cancel(it) }
// Clean up id mappings for canceled notifications to prevent growth
if (toCancel.isNotEmpty()) {
val keysToRemove = notificationIdMap.filterValues { it in toCancel }.keys
keysToRemove.forEach { notificationIdMap.remove(it) }
}
postedIds.clear()
postedIds.addAll(currentIds)
notificationIdMap.clear()
// Post/update summary and ensure foreground
val maxSummaryLines = 5
val truncated = summaryLines.size > maxSummaryLines
val displaySummaryLines = if (truncated) {
summaryLines.take(maxSummaryLines) + "...and ${summaryLines.size - maxSummaryLines} more"
} else {
summaryLines
}
val summary = createSummaryNotification(sessions.size, displaySummaryLines)
ensureForeground(summary)
// Create merged notification content
val summaryLines = sessions.map { "${it.title}: ${it.status}" }
val mergedNotification = createMergedNotification(sessions.size, summaryLines, sessions)
ensureForeground(mergedNotification)
}
private fun clearAll() {
val nm = getSystemService(NotificationManager::class.java)
nm?.cancel(SUMMARY_ID)
nm?.cancel(NOTIFICATION_ID)
postedIds.forEach { id -> nm?.cancel(id) }
postedIds.clear()
isFgStarted = false
@@ -272,7 +301,10 @@ class ForegroundService : Service() {
private fun stopForegroundService() {
try {
stopForeground(true)
if (isFgStarted) {
stopForeground(STOP_FOREGROUND_REMOVE)
isFgStarted = false
}
} catch (e: Exception) {
logError("Error stopping foreground", e)
}

View File

@@ -4,6 +4,9 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterFragmentActivity
@@ -16,6 +19,8 @@ class MainActivity: FlutterFragmentActivity() {
private lateinit var channel: MethodChannel
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
private val ACTION_STOP_ALL_CONNECTIONS = "tech.lolli.toolbox.STOP_ALL_CONNECTIONS"
private var stopAllReceiver: BroadcastReceiver? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -92,24 +97,32 @@ class MainActivity: FlutterFragmentActivity() {
// Handle intent if launched via notification action
handleActionIntent(intent)
// Register broadcast receiver for stop all connections
setupStopAllReceiver()
}
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
// Check if we already have the permission to avoid unnecessary prompts
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
try {
try {
// Check if we already have the permission to avoid unnecessary prompts
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
// Check if we should show rationale
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
android.util.Log.i("MainActivity", "User previously denied notification permission")
}
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
}
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
}
}
@@ -141,4 +154,52 @@ class MainActivity: FlutterFragmentActivity() {
}
}
}
private fun setupStopAllReceiver() {
stopAllReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ACTION_STOP_ALL_CONNECTIONS && ::channel.isInitialized) {
try {
channel.invokeMethod("stopAllConnections", null)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to invoke stopAllConnections: ${e.message}")
}
}
}
}
val filter = IntentFilter(ACTION_STOP_ALL_CONNECTIONS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.registerReceiver(this, stopAllReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(stopAllReceiver, filter)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 123) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
android.util.Log.i("MainActivity", "Notification permission granted")
} else {
android.util.Log.w("MainActivity", "Notification permission denied")
// Optionally inform user about the limitation
}
}
}
override fun onDestroy() {
super.onDestroy()
stopAllReceiver?.let {
try {
unregisterReceiver(it)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to unregister receiver: ${e.message}")
}
stopAllReceiver = null
}
}
}

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -748,7 +748,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -758,7 +758,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -884,7 +884,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -894,7 +894,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -912,7 +912,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -922,7 +922,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -943,7 +943,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -956,7 +956,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
@@ -982,7 +982,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -995,7 +995,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1018,7 +1018,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_TEAM = BA88US33G6;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
@@ -1031,7 +1031,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1054,7 +1054,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1066,7 +1066,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
@@ -1095,7 +1095,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1107,7 +1107,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;
@@ -1133,7 +1133,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1262;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_PREVIEWS = YES;
@@ -1145,7 +1145,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1220;
MARKETING_VERSION = 1.0.1262;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
PRODUCT_NAME = ServerBox;

View File

@@ -57,8 +57,10 @@ class MyApp extends StatelessWidget {
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
if (context.isDark && dark != null) {
UIs.primaryColor = dark.primary;
UIs.colorSeed = dark.primary;
} else if (!context.isDark && light != null) {
UIs.primaryColor = light.primary;
UIs.colorSeed = light.primary;
}
return _buildApp(context, light: lightTheme, dark: darkTheme);

View File

@@ -77,8 +77,10 @@ abstract final class MethodChans {
}
/// Register a handler for native -> Flutter callbacks.
/// Currently handles: `disconnectSession` with argument map {id: string}
static void registerHandler(Future<void> Function(String id) onDisconnect) {
/// Currently handles:
/// - `disconnectSession` with argument map {id: string}
/// - `stopAllConnections` with no arguments
static void registerHandler(Future<void> Function(String id) onDisconnect, [VoidCallback? onStopAll]) {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'disconnectSession':
@@ -88,6 +90,9 @@ abstract final class MethodChans {
await onDisconnect(id);
}
return;
case 'stopAllConnections':
onStopAll?.call();
return;
default:
return;
}

View File

@@ -2,10 +2,10 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/server/dist.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/provider/server/single.dart';
import 'package:server_box/data/res/store.dart';
extension LogoExt on Server {
extension LogoExt on ServerState {
String? getLogoUrl(BuildContext context) {
var logoUrl = spi.custom?.logoUrl ?? Stores.setting.serverLogoUrl.fetch().selfNotEmptyOrNull;
if (logoUrl == null) {

View File

@@ -0,0 +1,408 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/discovery_result.dart';
class SshDiscoveryService {
static const _sshPort = 22;
static Future<SshDiscoveryReport> discover([SshDiscoveryConfig config = const SshDiscoveryConfig()]) async {
final t0 = DateTime.now();
final candidates = <InternetAddress>{};
// 1) Get neighbors from ARP/NDP tables
candidates.addAll(await _neighborsIPv4());
candidates.addAll(await _neighborsIPv6());
// 2) Enumerate small subnets from local interfaces (IPv4 only)
final cidrs = await _localIPv4Cidrs();
for (final c in cidrs) {
if (c.prefix >= 24 && c.prefix <= 30) {
candidates.addAll(c.enumerateHosts(limit: config.hostEnumerationLimit));
}
}
// 3) Optional: mDNS/Bonjour SSH services
if (config.enableMdns) {
candidates.addAll(await _mdnsSshCandidates());
}
// Filter out unwanted addresses: loopback, link-local, 0.0.0.0, broadcast, multicast
candidates.removeWhere(
(a) => a.isLoopback || a.isLinkLocal || a.address == '0.0.0.0' || _isBroadcastOrMulticast(a),
);
// 4) Concurrent SSH port scanning
final scanner = _Scanner(
timeout: Duration(milliseconds: config.timeoutMs),
maxConcurrency: config.maxConcurrency,
);
final results = await scanner.scan(candidates.toList(growable: false));
results.sort((a, b) => a.addr.address.compareTo(b.addr.address));
final discoveryResults = results
.map((r) => SshDiscoveryResult(ip: r.addr.address, port: _sshPort, banner: r.banner?.trim()))
.toList();
return SshDiscoveryReport(
generatedAt: DateTime.now().toIso8601String(),
durationMs: DateTime.now().difference(t0).inMilliseconds,
count: discoveryResults.length,
items: discoveryResults,
);
}
static Future<String?> _run(String exe, List<String> args, {Duration? timeout}) async {
try {
final p = await Process.start(exe, args, runInShell: false);
final out = await p.stdout
.transform(utf8.decoder)
.join()
.timeout(
timeout ?? const Duration(seconds: 5),
onTimeout: () {
p.kill();
return '';
},
);
final code = await p.exitCode;
if (code == 0) return out;
// Some tools return non-zero but still have useful output
if (out.trim().isNotEmpty) return out;
return null;
} catch (_) {
return null;
}
}
static bool get _isLinux => Platform.isLinux;
static bool get _isMac => Platform.isMacOS;
static Future<Set<InternetAddress>> _neighborsIPv4() async {
final set = <InternetAddress>{};
if (_isLinux) {
final s = await _run('ip', ['neigh']);
if (s != null) {
for (final line in const LineSplitter().convert(s)) {
final tok = line.split(RegExp(r'\s+'));
if (tok.isNotEmpty) {
final ip = tok[0];
if (InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv4) {
set.add(InternetAddress(ip));
}
}
}
}
} else if (_isMac) {
final s = await _run('/usr/sbin/arp', ['-an']);
if (s != null) {
int matchCount = 0;
for (final line in const LineSplitter().convert(s)) {
final m = RegExp(r'\((\d+\.\d+\.\d+\.\d+)\)').firstMatch(line);
if (m != null) {
set.add(InternetAddress(m.group(1)!));
matchCount++;
}
}
if (matchCount == 0) {
lprint(
'[ssh_discovery] Warning: No ARP entries parsed on macOS. Output may be unexpected or localized. Output sample: ${s.length > 100 ? '${s.substring(0, 100)}...' : s}',
);
}
}
}
return set;
}
static Future<Set<InternetAddress>> _neighborsIPv6() async {
final set = <InternetAddress>{};
if (_isLinux) {
final s = await _run('ip', ['-6', 'neigh']);
if (s != null) {
for (final line in const LineSplitter().convert(s)) {
final ip = line.split(RegExp(r'\s+')).firstOrNull;
if (ip != null && InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv6) {
set.add(InternetAddress(ip));
}
}
}
} else if (_isMac) {
final s = await _run('/usr/sbin/ndp', ['-a']);
if (s != null) {
for (final line in const LineSplitter().convert(s)) {
final ip = line.trim().split(RegExp(r'\s+')).firstOrNull;
if (ip != null && InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv6) {
set.add(InternetAddress(ip));
}
}
}
}
return set;
}
static Future<List<_Cidr>> _localIPv4Cidrs() async {
final res = <_Cidr>[];
if (_isLinux) {
final s = await _run('ip', ['-o', '-4', 'addr', 'show', 'scope', 'global']);
if (s != null) {
for (final line in const LineSplitter().convert(s)) {
final m = RegExp(r'inet\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)').firstMatch(line);
if (m != null) {
final ip = InternetAddress(m.group(1)!);
final prefix = int.parse(m.group(2)!);
final mask = _prefixToMask(prefix);
final net = _networkAddress(ip, mask);
final brd = _broadcastAddress(ip, mask);
res.add(_Cidr(ip, prefix, mask, net, brd));
}
}
}
} else if (_isMac) {
final s = await _run('/sbin/ifconfig', []);
if (s != null) {
for (final raw in const LineSplitter().convert(s)) {
final line = raw.trimRight();
final ifMatch = RegExp(r'^([a-z0-9]+):').firstMatch(line);
if (ifMatch != null) {
continue;
}
if (line.contains('inet ') && !line.contains('127.0.0.1')) {
try {
final ipm = RegExp(
r'inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+0x([0-9a-fA-F]+)(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))?',
).firstMatch(line);
if (ipm == null) {
// Log unexpected format but continue processing other lines
lprint('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');
continue;
}
final ip = InternetAddress(ipm.group(1)!);
final hexMask = int.parse(ipm.group(2)!, radix: 16);
final dotted =
'${(hexMask >> 24) & 0xff}.${(hexMask >> 16) & 0xff}.${(hexMask >> 8) & 0xff}.${hexMask & 0xff}';
final mask = InternetAddress(dotted);
final prefix = _maskToPrefix(mask.address);
final net = _networkAddress(ip, mask);
final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);
res.add(_Cidr(ip, prefix, mask, net, brd));
} catch (e) {
lprint('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');
continue;
}
}
}
}
}
return res;
}
static bool _isBroadcastOrMulticast(InternetAddress a) {
// IPv4 broadcast: ends with .255 or is 255.255.255.255
if (a.type == InternetAddressType.IPv4) {
if (a.address == '255.255.255.255') return true;
if (a.address.split('.').last == '255') return true;
// Multicast: 224.0.0.0 - 239.255.255.255
final firstOctet = int.tryParse(a.address.split('.').first) ?? 0;
if (firstOctet >= 224 && firstOctet <= 239) return true;
} else if (a.type == InternetAddressType.IPv6) {
// IPv6 multicast: starts with ff
if (a.address.toLowerCase().startsWith('ff')) return true;
}
return false;
}
static Future<Set<InternetAddress>> _mdnsSshCandidates() async {
final set = <InternetAddress>{};
if (_isMac) {
try {
final proc = await Process.start('/usr/bin/dns-sd', ['-B', '_ssh._tcp']);
final lines = <String>[];
final subscription = proc.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(lines.add);
await Future<void>.delayed(const Duration(seconds: 2));
proc.kill();
await subscription.cancel();
for (final l in lines) {
final m = RegExp(r'Add\s+\d+\s+(\S+)\.\s+_ssh\._tcp\.').firstMatch(l);
if (m != null) {
final name = m.group(1)!;
final det = await _run('/usr/bin/dns-sd', [
'-L',
name,
'_ssh._tcp',
'local.',
], timeout: const Duration(seconds: 3));
if (det != null) {
for (final ip in RegExp(
r'Address\s*=\s*([0-9a-fA-F:\.]+)',
).allMatches(det).map((e) => e.group(1)!)) {
final parsed = InternetAddress.tryParse(ip);
if (parsed != null) set.add(parsed);
}
}
}
}
} catch (_) {}
} else if (_isLinux) {
final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);
if (s != null) {
for (final ip in RegExp(
r'address = \[(.*?)\]',
).allMatches(s).map((m) => m.group(1)!).where((e) => e.isNotEmpty)) {
final parsed = InternetAddress.tryParse(ip);
if (parsed != null) set.add(parsed);
}
}
}
return set;
}
}
class _Cidr {
final InternetAddress ip;
final int prefix;
final InternetAddress netmask;
final InternetAddress network;
final InternetAddress broadcast;
_Cidr(this.ip, this.prefix, this.netmask, this.network, this.broadcast);
Iterable<InternetAddress> enumerateHosts({int? limit}) sync* {
final n = _ipv4ToInt(network.address);
final b = _ipv4ToInt(broadcast.address);
int emitted = 0;
for (int v = n + 1; v <= b - 1; v++) {
if (limit != null && emitted >= limit) break;
emitted++;
yield InternetAddress(_intToIPv4(v));
}
}
@override
String toString() => '${network.address}/$prefix';
}
class _ScanResult {
final InternetAddress addr;
final String? banner;
_ScanResult(this.addr, this.banner);
}
class _Scanner {
final Duration timeout;
final int maxConcurrency;
_Scanner({required this.timeout, required this.maxConcurrency});
Future<List<_ScanResult>> scan(List<InternetAddress> addrs) async {
final sem = _Semaphore(maxConcurrency);
final futures = <Future<_ScanResult?>>[];
for (final a in addrs) {
futures.add(_guarded(sem, () => _probeSsh(a)));
}
final out = await Future.wait(futures);
return out.whereType<_ScanResult>().toList();
}
Future<_ScanResult?> _probeSsh(InternetAddress ip) async {
Socket? socket;
StreamSubscription? sub;
try {
socket = await Socket.connect(ip, SshDiscoveryService._sshPort, timeout: timeout);
socket.timeout(timeout);
final c = Completer<String?>();
sub = socket.listen(
(data) {
final s = utf8.decode(data, allowMalformed: true);
final line = s.split('\n').firstWhere((_) => true, orElse: () => s);
if (!c.isCompleted) {
c.complete(line.trim());
sub?.cancel();
}
},
onDone: () {
if (!c.isCompleted) c.complete(null);
},
onError: (_) {
if (!c.isCompleted) c.complete(null);
},
);
final banner = await c.future.timeout(timeout, onTimeout: () => null);
return _ScanResult(ip, banner);
} catch (_) {
return null;
} finally {
sub?.cancel();
socket?.destroy();
}
}
}
class _Semaphore {
int _permits;
final Queue<Completer<void>> _q = Queue();
_Semaphore(this._permits);
Future<T> withPermit<T>(Future<T> Function() fn) async {
if (_permits > 0) {
_permits--;
try {
return await fn();
} finally {
_permits++;
if (_q.isNotEmpty) _q.removeFirst().complete();
}
} else {
final c = Completer<void>();
_q.add(c);
await c.future;
return withPermit(fn);
}
}
}
Future<T> _guarded<T>(_Semaphore sem, Future<T> Function() fn) => sem.withPermit(fn);
// IPv4 utilities
int _ipv4ToInt(String ip) {
final p = ip.split('.').map(int.parse).toList();
return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}
String _intToIPv4(int v) => '${(v >> 24) & 0xff}.${(v >> 16) & 0xff}.${(v >> 8) & 0xff}.${v & 0xff}';
InternetAddress _prefixToMask(int prefix) {
final mask = prefix == 0 ? 0 : 0xffffffff << (32 - prefix);
return InternetAddress(_intToIPv4(mask & 0xffffffff));
}
int _maskToPrefix(String mask) {
final v = _ipv4ToInt(mask);
int c = 0;
for (int i = 31; i >= 0; i--) {
if ((v & (1 << i)) != 0) {
c++;
} else {
break;
}
}
return c;
}
InternetAddress _networkAddress(InternetAddress ip, InternetAddress mask) {
final v = _ipv4ToInt(ip.address) & _ipv4ToInt(mask.address);
return InternetAddress(_intToIPv4(v));
}
InternetAddress _broadcastAddress(InternetAddress ip, InternetAddress mask) {
final n = _ipv4ToInt(ip.address) & _ipv4ToInt(mask.address);
final b = n | (~_ipv4ToInt(mask.address) & 0xffffffff);
return InternetAddress(_intToIPv4(b));
}

View File

@@ -14,11 +14,7 @@ final class BakSyncer extends SyncIface {
@override
Future<void> saveToFile() async {
final pwd = await SecureStoreProps.bakPwd.read();
if (pwd == null || pwd.isEmpty) {
// Enforce password for non-clipboard backups
throw Exception('Backup password not set');
}
await BackupV2.backup(null, pwd);
await BackupV2.backup(null, pwd?.isEmpty == true ? null : pwd);
}
@override

View File

@@ -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) {

View 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;
}

View File

@@ -0,0 +1,188 @@
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(
id: ShortId.generate(),
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);
}
}

View File

@@ -9,8 +9,8 @@ class SystemDetector {
///
/// First checks if a custom system type is configured in [spi].
/// If not, attempts to detect the system by running commands:
/// 1. 'ver' command to detect Windows
/// 2. 'uname -a' command to detect Linux/BSD/Darwin
/// 1. 'uname -a' command to detect Linux/BSD/Darwin
/// 2. 'ver' command to detect Windows (if uname fails)
///
/// Returns [SystemType.linux] as default if detection fails.
static Future<SystemType> detect(SSHClient client, Spi spi) async {
@@ -22,17 +22,8 @@ class SystemDetector {
}
try {
// Try to detect Windows systems first (more reliable detection)
final powershellResult = await client.run('ver 2>nul').string;
if (powershellResult.isNotEmpty &&
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
detectedSystemType = SystemType.windows;
dprint('Detected Windows system type for ${spi.oldId}');
return detectedSystemType;
}
// Try to detect Unix/Linux/BSD systems
final unixResult = await client.run('uname -a').string;
// Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)
final unixResult = await client.run('uname -a 2>/dev/null').string;
if (unixResult.contains('Linux')) {
detectedSystemType = SystemType.linux;
dprint('Detected Linux system type for ${spi.oldId}');
@@ -42,6 +33,15 @@ class SystemDetector {
dprint('Detected BSD system type for ${spi.oldId}');
return detectedSystemType;
}
// If uname fails, try to detect Windows systems
final powershellResult = await client.run('ver 2>nul').string;
if (powershellResult.isNotEmpty &&
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
detectedSystemType = SystemType.windows;
dprint('Detected Windows system type for ${spi.oldId}');
return detectedSystemType;
}
} catch (e) {
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
}

View File

@@ -4,6 +4,9 @@ import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:logging/logging.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server/all.dart';
import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
@@ -44,17 +47,17 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
Future<void> merge({bool force = false}) async {
_loggerV2.info('Merging...');
// Merge each store
await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
// Merge each store and check if changes were made
final serverChanged = await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
final snippetChanged = await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
final keyChanged = await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
await Mergeable.mergeStore(backupData: container, store: Stores.container, force: force);
await Mergeable.mergeStore(backupData: history, store: Stores.history, force: force);
await Mergeable.mergeStore(backupData: settings, store: Stores.setting, force: force);
// Reload providers and notify listeners
Provider.reload();
RNodes.app.notify();
if (serverChanged) GlobalRef.gRef?.read(serversProvider.notifier).reload();
if (snippetChanged) GlobalRef.gRef?.read(snippetProvider.notifier).reload();
if (keyChanged) GlobalRef.gRef?.read(privateKeyProvider.notifier).reload();
_loggerV2.info('Merge completed');
}

View File

@@ -11,27 +11,10 @@ class BackupService {
/// Perform backup operation with the given source
static Future<void> backup(BuildContext context, BackupSource source) async {
try {
String? password;
final saved = await SecureStoreProps.bakPwd.read();
final password = saved?.isEmpty == true ? null : saved;
if (source is ClipboardBackupSource) {
// Clipboard backup: allow optional password
password = await _getClipboardPassword(context);
if (password == null) return; // canceled
} else {
// All other backups require pre-set bakPwd (SecureStore)
final saved = await SecureStoreProps.bakPwd.read();
if (saved == null || saved.isEmpty) {
// Prompt to set before proceeding
password = await _showPasswordDialog(context, hint: l10n.backupPasswordTip);
if (password == null || password.isEmpty) return; // Not set
await SecureStoreProps.bakPwd.write(password);
context.showSnackBar(l10n.backupPasswordSet);
} else {
password = saved;
}
}
final path = await BackupV2.backup(null, password.isEmpty ? null : password);
final path = await BackupV2.backup(null, password?.isEmpty == true ? null : password);
await source.saveContent(path);
if (source is ClipboardBackupSource) {
@@ -56,34 +39,6 @@ class BackupService {
await restoreFromText(context, text);
}
/// Handle password dialog for backup operations
static Future<String?> _getClipboardPassword(BuildContext context) async {
// Use saved bakPwd as default for clipboard flow if exists, but allow empty/custom
final savedPassword = await SecureStoreProps.bakPwd.read();
String? password;
if (savedPassword != null && savedPassword.isNotEmpty) {
final useCustom = await context.showRoundDialog<bool>(
title: l10n.backupPassword,
child: Text(l10n.backupPasswordTip),
actions: [
Btn.cancel(),
TextButton(onPressed: () => context.pop(false), child: Text(l10n.backupPasswordSet)),
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.custom)),
],
);
if (useCustom == null) return null;
if (useCustom) {
password = await _showPasswordDialog(context, initial: savedPassword);
} else {
password = savedPassword;
}
} else {
password = await _showPasswordDialog(context);
}
return password;
}
/// Handle restore from text with decryption support
static Future<void> restoreFromText(BuildContext context, String text) async {
// Check if backup is encrypted

View File

@@ -4,7 +4,7 @@ import 'package:server_box/core/extension/context/locale.dart';
enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
class SSHErr extends Err<SSHErrType> {
SSHErr({required super.type, super.message});
const SSHErr({required super.type, super.message});
@override
String? get solution => switch (type) {
@@ -29,7 +29,7 @@ enum ContainerErrType {
}
class ContainerErr extends Err<ContainerErrType> {
ContainerErr({required super.type, super.message});
const ContainerErr({required super.type, super.message});
@override
String? get solution => null;
@@ -38,7 +38,7 @@ class ContainerErr extends Err<ContainerErrType> {
enum ICloudErrType { generic, notFound, multipleFiles }
class ICloudErr extends Err<ICloudErrType> {
ICloudErr({required super.type, super.message});
const ICloudErr({required super.type, super.message});
@override
String? get solution => null;
@@ -47,7 +47,7 @@ class ICloudErr extends Err<ICloudErrType> {
enum WebdavErrType { generic, notFound }
class WebdavErr extends Err<WebdavErrType> {
WebdavErr({required super.type, super.message});
const WebdavErr({required super.type, super.message});
@override
String? get solution => null;
@@ -56,7 +56,7 @@ class WebdavErr extends Err<WebdavErrType> {
enum PveErrType { unknown, net, loginFailed }
class PveErr extends Err<PveErrType> {
PveErr({required super.type, super.message});
const PveErr({required super.type, super.message});
@override
String? get solution => null;

View File

@@ -55,8 +55,8 @@ enum StatusCmdType implements ShellCmdType {
uptime('uptime'),
conn('cat /proc/net/snmp'),
disk(
'lsblk --bytes --json --output '
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
'(lsblk --bytes --json --output '
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID 2>/dev/null && echo "LSBLK_SUCCESS") || df -k'
),
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
tempType('cat /sys/class/thermal/thermal_zone*/type'),

View File

@@ -1,7 +1,6 @@
import 'package:server_box/data/model/app/scripts/script_builders.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/provider/server.dart';
/// Shell functions available in the ServerBox application
enum ShellFunc {
@@ -26,8 +25,8 @@ enum ShellFunc {
};
/// Execute this shell function on the specified server
String exec(String id, {SystemType? systemType}) {
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType);
String exec(String id, {SystemType? systemType, required String? customDir}) {
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType, customDir: customDir);
final isWindows = systemType == SystemType.windows;
final builder = ScriptBuilderFactory.getBuilder(isWindows);
@@ -51,11 +50,10 @@ class ShellFuncManager {
/// Get the script directory for the given [id].
///
/// Checks for custom script directory first, then falls back to default.
static String getScriptDir(String id, {SystemType? systemType}) {
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
static String getScriptDir(String id, {SystemType? systemType, required String? customDir}) {
final isWindows = systemType == SystemType.windows;
if (customScriptDir != null) return _normalizeDir(customScriptDir, isWindows);
if (customDir != null) return _normalizeDir(customDir, isWindows);
return ScriptPaths.getScriptDir(id, isWindows: isWindows);
}
@@ -66,11 +64,10 @@ class ShellFuncManager {
}
/// Get the full script path for the given [id]
static String getScriptPath(String id, {SystemType? systemType}) {
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
if (customScriptDir != null) {
static String getScriptPath(String id, {SystemType? systemType, required String? customDir}) {
if (customDir != null) {
final isWindows = systemType == SystemType.windows;
final normalizedDir = _normalizeDir(customScriptDir, isWindows);
final normalizedDir = _normalizeDir(customDir, isWindows);
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
return '$normalizedDir$separator$fileName';
@@ -81,8 +78,8 @@ class ShellFuncManager {
}
/// Get the installation shell command for the script
static String getInstallShellCmd(String id, {SystemType? systemType}) {
final scriptDir = getScriptDir(id, systemType: systemType);
static String getInstallShellCmd(String id, {SystemType? systemType, required String? customDir}) {
final scriptDir = getScriptDir(id, systemType: systemType, customDir: customDir);
final isWindows = systemType == SystemType.windows;
final normalizedDir = _normalizeDir(scriptDir, isWindows);
final builder = ScriptBuilderFactory.getBuilder(isWindows);

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:hive_ce_flutter/adapters.dart';
import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/view/page/server/tab/tab.dart';
@@ -8,10 +9,17 @@ import 'package:server_box/view/page/snippet/list.dart';
import 'package:server_box/view/page/ssh/tab.dart';
import 'package:server_box/view/page/storage/local.dart';
part 'tab.g.dart';
@HiveType(typeId: 103)
enum AppTab {
@HiveField(0)
server,
@HiveField(1)
ssh,
@HiveField(2)
file,
@HiveField(3)
snippet
//settings,
;
@@ -93,4 +101,35 @@ enum AppTab {
static List<NavigationRailDestination> get navRailDestinations {
return AppTab.values.map((e) => e.navRailDestination).toList();
}
/// Helper function to parse AppTab list from stored object
static List<AppTab> parseAppTabsFromObj(dynamic val) {
if (val is List) {
final tabs = <AppTab>[];
for (final e in val) {
final tab = _parseAppTabFromElement(e);
if (tab != null) {
tabs.add(tab);
}
}
if (tabs.isNotEmpty) return tabs;
}
return AppTab.values;
}
/// Helper function to parse a single AppTab from various element types
static AppTab? _parseAppTabFromElement(dynamic e) {
if (e is AppTab) {
return e;
} else if (e is String) {
return AppTab.values.firstWhereOrNull((t) => t.name == e);
} else if (e is int) {
if (e >= 0 && e < AppTab.values.length) {
return AppTab.values[e];
}
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AppTabAdapter extends TypeAdapter<AppTab> {
@override
final typeId = 103;
@override
AppTab read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return AppTab.server;
case 1:
return AppTab.ssh;
case 2:
return AppTab.file;
case 3:
return AppTab.snippet;
default:
return AppTab.server;
}
}
@override
void write(BinaryWriter writer, AppTab obj) {
switch (obj) {
case AppTab.server:
writer.writeByte(0);
case AppTab.ssh:
writer.writeByte(1);
case AppTab.file:
writer.writeByte(2);
case AppTab.snippet:
writer.writeByte(3);
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppTabAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/container/status.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/misc.dart';
@@ -10,7 +11,7 @@ sealed class ContainerPs {
final String? image = null;
String? get name;
String? get cmd;
bool get running;
ContainerStatus get status;
String? cpu;
String? mem;
@@ -51,7 +52,7 @@ final class PodmanPs implements ContainerPs {
String? get cmd => command?.firstOrNull;
@override
bool get running => exited != true;
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
@override
void parseStats(String s) {
@@ -121,10 +122,7 @@ final class DockerPs implements ContainerPs {
String? get cmd => null;
@override
bool get running {
if (state?.contains('Exited') == true) return false;
return true;
}
ContainerStatus get status => ContainerStatus.fromDockerState(state);
@override
void parseStats(String s) {

View File

@@ -0,0 +1,70 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/context/locale.dart';
/// Represents the various states a container can be in.
/// Supports both Docker and Podman container status parsing.
enum ContainerStatus {
running,
exited,
created,
paused,
restarting,
removing,
dead,
unknown;
/// Check if the container is actively running
bool get isRunning => this == ContainerStatus.running;
/// Check if the container can be started
bool get canStart =>
this == ContainerStatus.exited ||
this == ContainerStatus.created ||
this == ContainerStatus.dead;
/// Check if the container can be stopped
bool get canStop =>
this == ContainerStatus.running || this == ContainerStatus.paused;
/// Check if the container can be restarted
bool get canRestart =>
this != ContainerStatus.removing && this != ContainerStatus.unknown;
/// Parse Docker container status string to ContainerStatus
static ContainerStatus fromDockerState(String? state) {
if (state == null || state.isEmpty) return ContainerStatus.unknown;
final lowerState = state.toLowerCase();
if (lowerState.startsWith('up')) return ContainerStatus.running;
if (lowerState.contains('exited')) return ContainerStatus.exited;
if (lowerState.contains('created')) return ContainerStatus.created;
if (lowerState.contains('paused')) return ContainerStatus.paused;
if (lowerState.contains('restarting')) return ContainerStatus.restarting;
if (lowerState.contains('removing')) return ContainerStatus.removing;
if (lowerState.contains('dead')) return ContainerStatus.dead;
return ContainerStatus.unknown;
}
/// Parse Podman container status from exited boolean
static ContainerStatus fromPodmanExited(bool? exited) {
if (exited == true) return ContainerStatus.exited;
if (exited == false) return ContainerStatus.running;
return ContainerStatus.unknown;
}
/// Get display string for the status
String get displayName {
return switch (this) {
ContainerStatus.running => l10n.running,
ContainerStatus.exited => libL10n.exit,
ContainerStatus.created => 'Created',
ContainerStatus.paused => 'Paused',
ContainerStatus.restarting => 'Restarting',
ContainerStatus.removing => 'Removing',
ContainerStatus.dead => 'Dead',
ContainerStatus.unknown => libL10n.unknown,
};
}
}

View File

@@ -0,0 +1,79 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce/hive.dart';
part 'connection_stat.freezed.dart';
part 'connection_stat.g.dart';
@freezed
@HiveType(typeId: 100)
abstract class ConnectionStat with _$ConnectionStat {
const factory ConnectionStat({
@HiveField(0) required String serverId,
@HiveField(1) required String serverName,
@HiveField(2) required DateTime timestamp,
@HiveField(3) required ConnectionResult result,
@HiveField(4) @Default('') String errorMessage,
@HiveField(5) required int durationMs,
}) = _ConnectionStat;
factory ConnectionStat.fromJson(Map<String, dynamic> json) =>
_$ConnectionStatFromJson(json);
}
@freezed
@HiveType(typeId: 101)
abstract class ServerConnectionStats with _$ServerConnectionStats {
const factory ServerConnectionStats({
@HiveField(0) required String serverId,
@HiveField(1) required String serverName,
@HiveField(2) required int totalAttempts,
@HiveField(3) required int successCount,
@HiveField(4) required int failureCount,
@HiveField(5) @Default(null) DateTime? lastSuccessTime,
@HiveField(6) @Default(null) DateTime? lastFailureTime,
@HiveField(7) @Default([]) List<ConnectionStat> recentConnections,
@HiveField(8) required double successRate,
}) = _ServerConnectionStats;
factory ServerConnectionStats.fromJson(Map<String, dynamic> json) =>
_$ServerConnectionStatsFromJson(json);
}
@HiveType(typeId: 102)
enum ConnectionResult {
@HiveField(0)
@JsonValue('success')
success,
@HiveField(1)
@JsonValue('timeout')
timeout,
@HiveField(2)
@JsonValue('auth_failed')
authFailed,
@HiveField(3)
@JsonValue('network_error')
networkError,
@HiveField(4)
@JsonValue('unknown_error')
unknownError,
}
extension ConnectionResultExtension on ConnectionResult {
String get displayName {
switch (this) {
case ConnectionResult.success:
return libL10n.success;
case ConnectionResult.timeout:
return '${libL10n.error}(timeout)';
case ConnectionResult.authFailed:
return '${libL10n.error}(auth)';
case ConnectionResult.networkError:
return '${libL10n.error}(${libL10n.network})';
case ConnectionResult.unknownError:
return '${libL10n.error}(${libL10n.unknown})';
}
}
bool get isSuccess => this == ConnectionResult.success;
}

View File

@@ -0,0 +1,585 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'connection_stat.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ConnectionStat {
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) DateTime get timestamp;@HiveField(3) ConnectionResult get result;@HiveField(4) String get errorMessage;@HiveField(5) int get durationMs;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ConnectionStatCopyWith<ConnectionStat> get copyWith => _$ConnectionStatCopyWithImpl<ConnectionStat>(this as ConnectionStat, _$identity);
/// Serializes this ConnectionStat to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
@override
String toString() {
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
}
}
/// @nodoc
abstract mixin class $ConnectionStatCopyWith<$Res> {
factory $ConnectionStatCopyWith(ConnectionStat value, $Res Function(ConnectionStat) _then) = _$ConnectionStatCopyWithImpl;
@useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
});
}
/// @nodoc
class _$ConnectionStatCopyWithImpl<$Res>
implements $ConnectionStatCopyWith<$Res> {
_$ConnectionStatCopyWithImpl(this._self, this._then);
final ConnectionStat _self;
final $Res Function(ConnectionStat) _then;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
return _then(_self.copyWith(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [ConnectionStat].
extension ConnectionStatPatterns on ConnectionStat {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ConnectionStat value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ConnectionStat value) $default,){
final _that = this;
switch (_that) {
case _ConnectionStat():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ConnectionStat value)? $default,){
final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs) $default,) {final _that = this;
switch (_that) {
case _ConnectionStat():
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,) {final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ConnectionStat implements ConnectionStat {
const _ConnectionStat({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.timestamp, @HiveField(3) required this.result, @HiveField(4) this.errorMessage = '', @HiveField(5) required this.durationMs});
factory _ConnectionStat.fromJson(Map<String, dynamic> json) => _$ConnectionStatFromJson(json);
@override@HiveField(0) final String serverId;
@override@HiveField(1) final String serverName;
@override@HiveField(2) final DateTime timestamp;
@override@HiveField(3) final ConnectionResult result;
@override@JsonKey()@HiveField(4) final String errorMessage;
@override@HiveField(5) final int durationMs;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ConnectionStatCopyWith<_ConnectionStat> get copyWith => __$ConnectionStatCopyWithImpl<_ConnectionStat>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ConnectionStatToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
@override
String toString() {
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
}
}
/// @nodoc
abstract mixin class _$ConnectionStatCopyWith<$Res> implements $ConnectionStatCopyWith<$Res> {
factory _$ConnectionStatCopyWith(_ConnectionStat value, $Res Function(_ConnectionStat) _then) = __$ConnectionStatCopyWithImpl;
@override @useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
});
}
/// @nodoc
class __$ConnectionStatCopyWithImpl<$Res>
implements _$ConnectionStatCopyWith<$Res> {
__$ConnectionStatCopyWithImpl(this._self, this._then);
final _ConnectionStat _self;
final $Res Function(_ConnectionStat) _then;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
return _then(_ConnectionStat(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
mixin _$ServerConnectionStats {
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) int get totalAttempts;@HiveField(3) int get successCount;@HiveField(4) int get failureCount;@HiveField(5) DateTime? get lastSuccessTime;@HiveField(6) DateTime? get lastFailureTime;@HiveField(7) List<ConnectionStat> get recentConnections;@HiveField(8) double get successRate;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServerConnectionStatsCopyWith<ServerConnectionStats> get copyWith => _$ServerConnectionStatsCopyWithImpl<ServerConnectionStats>(this as ServerConnectionStats, _$identity);
/// Serializes this ServerConnectionStats to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other.recentConnections, recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(recentConnections),successRate);
@override
String toString() {
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
}
}
/// @nodoc
abstract mixin class $ServerConnectionStatsCopyWith<$Res> {
factory $ServerConnectionStatsCopyWith(ServerConnectionStats value, $Res Function(ServerConnectionStats) _then) = _$ServerConnectionStatsCopyWithImpl;
@useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
});
}
/// @nodoc
class _$ServerConnectionStatsCopyWithImpl<$Res>
implements $ServerConnectionStatsCopyWith<$Res> {
_$ServerConnectionStatsCopyWithImpl(this._self, this._then);
final ServerConnectionStats _self;
final $Res Function(ServerConnectionStats) _then;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
return _then(_self.copyWith(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
as DateTime?,recentConnections: null == recentConnections ? _self.recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// Adds pattern-matching-related methods to [ServerConnectionStats].
extension ServerConnectionStatsPatterns on ServerConnectionStats {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerConnectionStats value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerConnectionStats value) $default,){
final _that = this;
switch (_that) {
case _ServerConnectionStats():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerConnectionStats value)? $default,){
final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate) $default,) {final _that = this;
switch (_that) {
case _ServerConnectionStats():
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,) {final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ServerConnectionStats implements ServerConnectionStats {
const _ServerConnectionStats({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.totalAttempts, @HiveField(3) required this.successCount, @HiveField(4) required this.failureCount, @HiveField(5) this.lastSuccessTime = null, @HiveField(6) this.lastFailureTime = null, @HiveField(7) final List<ConnectionStat> recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections;
factory _ServerConnectionStats.fromJson(Map<String, dynamic> json) => _$ServerConnectionStatsFromJson(json);
@override@HiveField(0) final String serverId;
@override@HiveField(1) final String serverName;
@override@HiveField(2) final int totalAttempts;
@override@HiveField(3) final int successCount;
@override@HiveField(4) final int failureCount;
@override@JsonKey()@HiveField(5) final DateTime? lastSuccessTime;
@override@JsonKey()@HiveField(6) final DateTime? lastFailureTime;
final List<ConnectionStat> _recentConnections;
@override@JsonKey()@HiveField(7) List<ConnectionStat> get recentConnections {
if (_recentConnections is EqualUnmodifiableListView) return _recentConnections;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recentConnections);
}
@override@HiveField(8) final double successRate;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServerConnectionStatsCopyWith<_ServerConnectionStats> get copyWith => __$ServerConnectionStatsCopyWithImpl<_ServerConnectionStats>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ServerConnectionStatsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other._recentConnections, _recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(_recentConnections),successRate);
@override
String toString() {
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
}
}
/// @nodoc
abstract mixin class _$ServerConnectionStatsCopyWith<$Res> implements $ServerConnectionStatsCopyWith<$Res> {
factory _$ServerConnectionStatsCopyWith(_ServerConnectionStats value, $Res Function(_ServerConnectionStats) _then) = __$ServerConnectionStatsCopyWithImpl;
@override @useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
});
}
/// @nodoc
class __$ServerConnectionStatsCopyWithImpl<$Res>
implements _$ServerConnectionStatsCopyWith<$Res> {
__$ServerConnectionStatsCopyWithImpl(this._self, this._then);
final _ServerConnectionStats _self;
final $Res Function(_ServerConnectionStats) _then;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
return _then(_ServerConnectionStats(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
as DateTime?,recentConnections: null == recentConnections ? _self._recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
// dart format on

View File

@@ -0,0 +1,233 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connection_stat.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ConnectionStatAdapter extends TypeAdapter<ConnectionStat> {
@override
final typeId = 100;
@override
ConnectionStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ConnectionStat(
serverId: fields[0] as String,
serverName: fields[1] as String,
timestamp: fields[2] as DateTime,
result: fields[3] as ConnectionResult,
errorMessage: fields[4] == null ? '' : fields[4] as String,
durationMs: (fields[5] as num).toInt(),
);
}
@override
void write(BinaryWriter writer, ConnectionStat obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.serverId)
..writeByte(1)
..write(obj.serverName)
..writeByte(2)
..write(obj.timestamp)
..writeByte(3)
..write(obj.result)
..writeByte(4)
..write(obj.errorMessage)
..writeByte(5)
..write(obj.durationMs);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConnectionStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ServerConnectionStatsAdapter extends TypeAdapter<ServerConnectionStats> {
@override
final typeId = 101;
@override
ServerConnectionStats read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerConnectionStats(
serverId: fields[0] as String,
serverName: fields[1] as String,
totalAttempts: (fields[2] as num).toInt(),
successCount: (fields[3] as num).toInt(),
failureCount: (fields[4] as num).toInt(),
lastSuccessTime: fields[5] == null ? null : fields[5] as DateTime?,
lastFailureTime: fields[6] == null ? null : fields[6] as DateTime?,
recentConnections: fields[7] == null
? []
: (fields[7] as List).cast<ConnectionStat>(),
successRate: (fields[8] as num).toDouble(),
);
}
@override
void write(BinaryWriter writer, ServerConnectionStats obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.serverId)
..writeByte(1)
..write(obj.serverName)
..writeByte(2)
..write(obj.totalAttempts)
..writeByte(3)
..write(obj.successCount)
..writeByte(4)
..write(obj.failureCount)
..writeByte(5)
..write(obj.lastSuccessTime)
..writeByte(6)
..write(obj.lastFailureTime)
..writeByte(7)
..write(obj.recentConnections)
..writeByte(8)
..write(obj.successRate);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerConnectionStatsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ConnectionResultAdapter extends TypeAdapter<ConnectionResult> {
@override
final typeId = 102;
@override
ConnectionResult read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return ConnectionResult.success;
case 1:
return ConnectionResult.timeout;
case 2:
return ConnectionResult.authFailed;
case 3:
return ConnectionResult.networkError;
case 4:
return ConnectionResult.unknownError;
default:
return ConnectionResult.success;
}
}
@override
void write(BinaryWriter writer, ConnectionResult obj) {
switch (obj) {
case ConnectionResult.success:
writer.writeByte(0);
case ConnectionResult.timeout:
writer.writeByte(1);
case ConnectionResult.authFailed:
writer.writeByte(2);
case ConnectionResult.networkError:
writer.writeByte(3);
case ConnectionResult.unknownError:
writer.writeByte(4);
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConnectionResultAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ConnectionStat _$ConnectionStatFromJson(Map<String, dynamic> json) =>
_ConnectionStat(
serverId: json['serverId'] as String,
serverName: json['serverName'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
result: $enumDecode(_$ConnectionResultEnumMap, json['result']),
errorMessage: json['errorMessage'] as String? ?? '',
durationMs: (json['durationMs'] as num).toInt(),
);
Map<String, dynamic> _$ConnectionStatToJson(_ConnectionStat instance) =>
<String, dynamic>{
'serverId': instance.serverId,
'serverName': instance.serverName,
'timestamp': instance.timestamp.toIso8601String(),
'result': _$ConnectionResultEnumMap[instance.result]!,
'errorMessage': instance.errorMessage,
'durationMs': instance.durationMs,
};
const _$ConnectionResultEnumMap = {
ConnectionResult.success: 'success',
ConnectionResult.timeout: 'timeout',
ConnectionResult.authFailed: 'auth_failed',
ConnectionResult.networkError: 'network_error',
ConnectionResult.unknownError: 'unknown_error',
};
_ServerConnectionStats _$ServerConnectionStatsFromJson(
Map<String, dynamic> json,
) => _ServerConnectionStats(
serverId: json['serverId'] as String,
serverName: json['serverName'] as String,
totalAttempts: (json['totalAttempts'] as num).toInt(),
successCount: (json['successCount'] as num).toInt(),
failureCount: (json['failureCount'] as num).toInt(),
lastSuccessTime: json['lastSuccessTime'] == null
? null
: DateTime.parse(json['lastSuccessTime'] as String),
lastFailureTime: json['lastFailureTime'] == null
? null
: DateTime.parse(json['lastFailureTime'] as String),
recentConnections:
(json['recentConnections'] as List<dynamic>?)
?.map((e) => ConnectionStat.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
successRate: (json['successRate'] as num).toDouble(),
);
Map<String, dynamic> _$ServerConnectionStatsToJson(
_ServerConnectionStats instance,
) => <String, dynamic>{
'serverId': instance.serverId,
'serverName': instance.serverName,
'totalAttempts': instance.totalAttempts,
'successCount': instance.successCount,
'failureCount': instance.failureCount,
'lastSuccessTime': instance.lastSuccessTime?.toIso8601String(),
'lastFailureTime': instance.lastFailureTime?.toIso8601String(),
'recentConnections': instance.recentConnections,
'successRate': instance.successRate,
};

View File

@@ -0,0 +1,49 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'discovery_result.freezed.dart';
part 'discovery_result.g.dart';
@freezed
abstract class SshDiscoveryResult with _$SshDiscoveryResult {
const factory SshDiscoveryResult({
required String ip,
required int port,
String? banner,
@Default(false) bool isSelected,
}) = _SshDiscoveryResult;
factory SshDiscoveryResult.fromJson(Map<String, dynamic> json) => _$SshDiscoveryResultFromJson(json);
}
@freezed
abstract class SshDiscoveryReport with _$SshDiscoveryReport {
const factory SshDiscoveryReport({
required String generatedAt,
required int durationMs,
required int count,
required List<SshDiscoveryResult> items,
}) = _SshDiscoveryReport;
factory SshDiscoveryReport.fromJson(Map<String, dynamic> json) => _$SshDiscoveryReportFromJson(json);
}
@freezed
abstract class SshDiscoveryConfig with _$SshDiscoveryConfig {
const factory SshDiscoveryConfig({
@Default(700) int timeoutMs,
@Default(128) int maxConcurrency,
@Default(false) bool enableMdns,
@Default(4096) int hostEnumerationLimit,
}) = _SshDiscoveryConfig;
}
extension SshDiscoveryConfigX on SshDiscoveryConfig {
List<String> toArgs() {
final args = <String>[];
args.add('--timeout-ms=$timeoutMs');
args.add('--max-concurrency=$maxConcurrency');
args.add('--host-enumeration-limit=$hostEnumerationLimit');
if (enableMdns) args.add('--enable-mdns');
return args;
}
}

View File

@@ -0,0 +1,830 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'discovery_result.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SshDiscoveryResult {
String get ip; int get port; String? get banner; bool get isSelected;
/// Create a copy of SshDiscoveryResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SshDiscoveryResultCopyWith<SshDiscoveryResult> get copyWith => _$SshDiscoveryResultCopyWithImpl<SshDiscoveryResult>(this as SshDiscoveryResult, _$identity);
/// Serializes this SshDiscoveryResult to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryResult&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.banner, banner) || other.banner == banner)&&(identical(other.isSelected, isSelected) || other.isSelected == isSelected));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,ip,port,banner,isSelected);
@override
String toString() {
return 'SshDiscoveryResult(ip: $ip, port: $port, banner: $banner, isSelected: $isSelected)';
}
}
/// @nodoc
abstract mixin class $SshDiscoveryResultCopyWith<$Res> {
factory $SshDiscoveryResultCopyWith(SshDiscoveryResult value, $Res Function(SshDiscoveryResult) _then) = _$SshDiscoveryResultCopyWithImpl;
@useResult
$Res call({
String ip, int port, String? banner, bool isSelected
});
}
/// @nodoc
class _$SshDiscoveryResultCopyWithImpl<$Res>
implements $SshDiscoveryResultCopyWith<$Res> {
_$SshDiscoveryResultCopyWithImpl(this._self, this._then);
final SshDiscoveryResult _self;
final $Res Function(SshDiscoveryResult) _then;
/// Create a copy of SshDiscoveryResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? ip = null,Object? port = null,Object? banner = freezed,Object? isSelected = null,}) {
return _then(_self.copyWith(
ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
as int,banner: freezed == banner ? _self.banner : banner // ignore: cast_nullable_to_non_nullable
as String?,isSelected: null == isSelected ? _self.isSelected : isSelected // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [SshDiscoveryResult].
extension SshDiscoveryResultPatterns on SshDiscoveryResult {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryResult value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SshDiscoveryResult() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryResult value) $default,){
final _that = this;
switch (_that) {
case _SshDiscoveryResult():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryResult value)? $default,){
final _that = this;
switch (_that) {
case _SshDiscoveryResult() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String ip, int port, String? banner, bool isSelected)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SshDiscoveryResult() when $default != null:
return $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String ip, int port, String? banner, bool isSelected) $default,) {final _that = this;
switch (_that) {
case _SshDiscoveryResult():
return $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String ip, int port, String? banner, bool isSelected)? $default,) {final _that = this;
switch (_that) {
case _SshDiscoveryResult() when $default != null:
return $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SshDiscoveryResult implements SshDiscoveryResult {
const _SshDiscoveryResult({required this.ip, required this.port, this.banner, this.isSelected = false});
factory _SshDiscoveryResult.fromJson(Map<String, dynamic> json) => _$SshDiscoveryResultFromJson(json);
@override final String ip;
@override final int port;
@override final String? banner;
@override@JsonKey() final bool isSelected;
/// Create a copy of SshDiscoveryResult
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SshDiscoveryResultCopyWith<_SshDiscoveryResult> get copyWith => __$SshDiscoveryResultCopyWithImpl<_SshDiscoveryResult>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SshDiscoveryResultToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryResult&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.banner, banner) || other.banner == banner)&&(identical(other.isSelected, isSelected) || other.isSelected == isSelected));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,ip,port,banner,isSelected);
@override
String toString() {
return 'SshDiscoveryResult(ip: $ip, port: $port, banner: $banner, isSelected: $isSelected)';
}
}
/// @nodoc
abstract mixin class _$SshDiscoveryResultCopyWith<$Res> implements $SshDiscoveryResultCopyWith<$Res> {
factory _$SshDiscoveryResultCopyWith(_SshDiscoveryResult value, $Res Function(_SshDiscoveryResult) _then) = __$SshDiscoveryResultCopyWithImpl;
@override @useResult
$Res call({
String ip, int port, String? banner, bool isSelected
});
}
/// @nodoc
class __$SshDiscoveryResultCopyWithImpl<$Res>
implements _$SshDiscoveryResultCopyWith<$Res> {
__$SshDiscoveryResultCopyWithImpl(this._self, this._then);
final _SshDiscoveryResult _self;
final $Res Function(_SshDiscoveryResult) _then;
/// Create a copy of SshDiscoveryResult
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? ip = null,Object? port = null,Object? banner = freezed,Object? isSelected = null,}) {
return _then(_SshDiscoveryResult(
ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
as int,banner: freezed == banner ? _self.banner : banner // ignore: cast_nullable_to_non_nullable
as String?,isSelected: null == isSelected ? _self.isSelected : isSelected // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
mixin _$SshDiscoveryReport {
String get generatedAt; int get durationMs; int get count; List<SshDiscoveryResult> get items;
/// Create a copy of SshDiscoveryReport
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SshDiscoveryReportCopyWith<SshDiscoveryReport> get copyWith => _$SshDiscoveryReportCopyWithImpl<SshDiscoveryReport>(this as SshDiscoveryReport, _$identity);
/// Serializes this SshDiscoveryReport to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryReport&&(identical(other.generatedAt, generatedAt) || other.generatedAt == generatedAt)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs)&&(identical(other.count, count) || other.count == count)&&const DeepCollectionEquality().equals(other.items, items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,generatedAt,durationMs,count,const DeepCollectionEquality().hash(items));
@override
String toString() {
return 'SshDiscoveryReport(generatedAt: $generatedAt, durationMs: $durationMs, count: $count, items: $items)';
}
}
/// @nodoc
abstract mixin class $SshDiscoveryReportCopyWith<$Res> {
factory $SshDiscoveryReportCopyWith(SshDiscoveryReport value, $Res Function(SshDiscoveryReport) _then) = _$SshDiscoveryReportCopyWithImpl;
@useResult
$Res call({
String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items
});
}
/// @nodoc
class _$SshDiscoveryReportCopyWithImpl<$Res>
implements $SshDiscoveryReportCopyWith<$Res> {
_$SshDiscoveryReportCopyWithImpl(this._self, this._then);
final SshDiscoveryReport _self;
final $Res Function(SshDiscoveryReport) _then;
/// Create a copy of SshDiscoveryReport
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? generatedAt = null,Object? durationMs = null,Object? count = null,Object? items = null,}) {
return _then(_self.copyWith(
generatedAt: null == generatedAt ? _self.generatedAt : generatedAt // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
as int,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
as List<SshDiscoveryResult>,
));
}
}
/// Adds pattern-matching-related methods to [SshDiscoveryReport].
extension SshDiscoveryReportPatterns on SshDiscoveryReport {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryReport value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SshDiscoveryReport() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryReport value) $default,){
final _that = this;
switch (_that) {
case _SshDiscoveryReport():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryReport value)? $default,){
final _that = this;
switch (_that) {
case _SshDiscoveryReport() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SshDiscoveryReport() when $default != null:
return $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items) $default,) {final _that = this;
switch (_that) {
case _SshDiscoveryReport():
return $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items)? $default,) {final _that = this;
switch (_that) {
case _SshDiscoveryReport() when $default != null:
return $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SshDiscoveryReport implements SshDiscoveryReport {
const _SshDiscoveryReport({required this.generatedAt, required this.durationMs, required this.count, required final List<SshDiscoveryResult> items}): _items = items;
factory _SshDiscoveryReport.fromJson(Map<String, dynamic> json) => _$SshDiscoveryReportFromJson(json);
@override final String generatedAt;
@override final int durationMs;
@override final int count;
final List<SshDiscoveryResult> _items;
@override List<SshDiscoveryResult> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
/// Create a copy of SshDiscoveryReport
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SshDiscoveryReportCopyWith<_SshDiscoveryReport> get copyWith => __$SshDiscoveryReportCopyWithImpl<_SshDiscoveryReport>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SshDiscoveryReportToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryReport&&(identical(other.generatedAt, generatedAt) || other.generatedAt == generatedAt)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs)&&(identical(other.count, count) || other.count == count)&&const DeepCollectionEquality().equals(other._items, _items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,generatedAt,durationMs,count,const DeepCollectionEquality().hash(_items));
@override
String toString() {
return 'SshDiscoveryReport(generatedAt: $generatedAt, durationMs: $durationMs, count: $count, items: $items)';
}
}
/// @nodoc
abstract mixin class _$SshDiscoveryReportCopyWith<$Res> implements $SshDiscoveryReportCopyWith<$Res> {
factory _$SshDiscoveryReportCopyWith(_SshDiscoveryReport value, $Res Function(_SshDiscoveryReport) _then) = __$SshDiscoveryReportCopyWithImpl;
@override @useResult
$Res call({
String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items
});
}
/// @nodoc
class __$SshDiscoveryReportCopyWithImpl<$Res>
implements _$SshDiscoveryReportCopyWith<$Res> {
__$SshDiscoveryReportCopyWithImpl(this._self, this._then);
final _SshDiscoveryReport _self;
final $Res Function(_SshDiscoveryReport) _then;
/// Create a copy of SshDiscoveryReport
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? generatedAt = null,Object? durationMs = null,Object? count = null,Object? items = null,}) {
return _then(_SshDiscoveryReport(
generatedAt: null == generatedAt ? _self.generatedAt : generatedAt // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
as int,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
as List<SshDiscoveryResult>,
));
}
}
/// @nodoc
mixin _$SshDiscoveryConfig {
int get timeoutMs; int get maxConcurrency; bool get enableMdns; int get hostEnumerationLimit;
/// Create a copy of SshDiscoveryConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SshDiscoveryConfigCopyWith<SshDiscoveryConfig> get copyWith => _$SshDiscoveryConfigCopyWithImpl<SshDiscoveryConfig>(this as SshDiscoveryConfig, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryConfig&&(identical(other.timeoutMs, timeoutMs) || other.timeoutMs == timeoutMs)&&(identical(other.maxConcurrency, maxConcurrency) || other.maxConcurrency == maxConcurrency)&&(identical(other.enableMdns, enableMdns) || other.enableMdns == enableMdns)&&(identical(other.hostEnumerationLimit, hostEnumerationLimit) || other.hostEnumerationLimit == hostEnumerationLimit));
}
@override
int get hashCode => Object.hash(runtimeType,timeoutMs,maxConcurrency,enableMdns,hostEnumerationLimit);
@override
String toString() {
return 'SshDiscoveryConfig(timeoutMs: $timeoutMs, maxConcurrency: $maxConcurrency, enableMdns: $enableMdns, hostEnumerationLimit: $hostEnumerationLimit)';
}
}
/// @nodoc
abstract mixin class $SshDiscoveryConfigCopyWith<$Res> {
factory $SshDiscoveryConfigCopyWith(SshDiscoveryConfig value, $Res Function(SshDiscoveryConfig) _then) = _$SshDiscoveryConfigCopyWithImpl;
@useResult
$Res call({
int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit
});
}
/// @nodoc
class _$SshDiscoveryConfigCopyWithImpl<$Res>
implements $SshDiscoveryConfigCopyWith<$Res> {
_$SshDiscoveryConfigCopyWithImpl(this._self, this._then);
final SshDiscoveryConfig _self;
final $Res Function(SshDiscoveryConfig) _then;
/// Create a copy of SshDiscoveryConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? timeoutMs = null,Object? maxConcurrency = null,Object? enableMdns = null,Object? hostEnumerationLimit = null,}) {
return _then(_self.copyWith(
timeoutMs: null == timeoutMs ? _self.timeoutMs : timeoutMs // ignore: cast_nullable_to_non_nullable
as int,maxConcurrency: null == maxConcurrency ? _self.maxConcurrency : maxConcurrency // ignore: cast_nullable_to_non_nullable
as int,enableMdns: null == enableMdns ? _self.enableMdns : enableMdns // ignore: cast_nullable_to_non_nullable
as bool,hostEnumerationLimit: null == hostEnumerationLimit ? _self.hostEnumerationLimit : hostEnumerationLimit // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [SshDiscoveryConfig].
extension SshDiscoveryConfigPatterns on SshDiscoveryConfig {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryConfig value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SshDiscoveryConfig() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryConfig value) $default,){
final _that = this;
switch (_that) {
case _SshDiscoveryConfig():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryConfig value)? $default,){
final _that = this;
switch (_that) {
case _SshDiscoveryConfig() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SshDiscoveryConfig() when $default != null:
return $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit) $default,) {final _that = this;
switch (_that) {
case _SshDiscoveryConfig():
return $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit)? $default,) {final _that = this;
switch (_that) {
case _SshDiscoveryConfig() when $default != null:
return $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:
return null;
}
}
}
/// @nodoc
class _SshDiscoveryConfig implements SshDiscoveryConfig {
const _SshDiscoveryConfig({this.timeoutMs = 700, this.maxConcurrency = 128, this.enableMdns = false, this.hostEnumerationLimit = 4096});
@override@JsonKey() final int timeoutMs;
@override@JsonKey() final int maxConcurrency;
@override@JsonKey() final bool enableMdns;
@override@JsonKey() final int hostEnumerationLimit;
/// Create a copy of SshDiscoveryConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SshDiscoveryConfigCopyWith<_SshDiscoveryConfig> get copyWith => __$SshDiscoveryConfigCopyWithImpl<_SshDiscoveryConfig>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryConfig&&(identical(other.timeoutMs, timeoutMs) || other.timeoutMs == timeoutMs)&&(identical(other.maxConcurrency, maxConcurrency) || other.maxConcurrency == maxConcurrency)&&(identical(other.enableMdns, enableMdns) || other.enableMdns == enableMdns)&&(identical(other.hostEnumerationLimit, hostEnumerationLimit) || other.hostEnumerationLimit == hostEnumerationLimit));
}
@override
int get hashCode => Object.hash(runtimeType,timeoutMs,maxConcurrency,enableMdns,hostEnumerationLimit);
@override
String toString() {
return 'SshDiscoveryConfig(timeoutMs: $timeoutMs, maxConcurrency: $maxConcurrency, enableMdns: $enableMdns, hostEnumerationLimit: $hostEnumerationLimit)';
}
}
/// @nodoc
abstract mixin class _$SshDiscoveryConfigCopyWith<$Res> implements $SshDiscoveryConfigCopyWith<$Res> {
factory _$SshDiscoveryConfigCopyWith(_SshDiscoveryConfig value, $Res Function(_SshDiscoveryConfig) _then) = __$SshDiscoveryConfigCopyWithImpl;
@override @useResult
$Res call({
int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit
});
}
/// @nodoc
class __$SshDiscoveryConfigCopyWithImpl<$Res>
implements _$SshDiscoveryConfigCopyWith<$Res> {
__$SshDiscoveryConfigCopyWithImpl(this._self, this._then);
final _SshDiscoveryConfig _self;
final $Res Function(_SshDiscoveryConfig) _then;
/// Create a copy of SshDiscoveryConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? timeoutMs = null,Object? maxConcurrency = null,Object? enableMdns = null,Object? hostEnumerationLimit = null,}) {
return _then(_SshDiscoveryConfig(
timeoutMs: null == timeoutMs ? _self.timeoutMs : timeoutMs // ignore: cast_nullable_to_non_nullable
as int,maxConcurrency: null == maxConcurrency ? _self.maxConcurrency : maxConcurrency // ignore: cast_nullable_to_non_nullable
as int,enableMdns: null == enableMdns ? _self.enableMdns : enableMdns // ignore: cast_nullable_to_non_nullable
as bool,hostEnumerationLimit: null == hostEnumerationLimit ? _self.hostEnumerationLimit : hostEnumerationLimit // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
// dart format on

View File

@@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'discovery_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SshDiscoveryResult _$SshDiscoveryResultFromJson(Map<String, dynamic> json) =>
_SshDiscoveryResult(
ip: json['ip'] as String,
port: (json['port'] as num).toInt(),
banner: json['banner'] as String?,
isSelected: json['isSelected'] as bool? ?? false,
);
Map<String, dynamic> _$SshDiscoveryResultToJson(_SshDiscoveryResult instance) =>
<String, dynamic>{
'ip': instance.ip,
'port': instance.port,
'banner': instance.banner,
'isSelected': instance.isSelected,
};
_SshDiscoveryReport _$SshDiscoveryReportFromJson(Map<String, dynamic> json) =>
_SshDiscoveryReport(
generatedAt: json['generatedAt'] as String,
durationMs: (json['durationMs'] as num).toInt(),
count: (json['count'] as num).toInt(),
items: (json['items'] as List<dynamic>)
.map((e) => SshDiscoveryResult.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$SshDiscoveryReportToJson(_SshDiscoveryReport instance) =>
<String, dynamic>{
'generatedAt': instance.generatedAt,
'durationMs': instance.durationMs,
'count': instance.count,
'items': instance.items,
};

View File

@@ -44,22 +44,49 @@ class Disk with EquatableMixin {
static List<Disk> parse(String raw) {
final list = <Disk>[];
raw = raw.trim();
try {
if (raw.startsWith('{')) {
// Parse JSON output from lsblk command
final Map<String, dynamic> jsonData = json.decode(raw);
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
if (raw.isEmpty) {
dprint('Empty disk info data received');
return list;
}
for (final device in blockdevices) {
// Process each device
_processTopLevelDevice(device, list);
try {
// Check if we have lsblk JSON output with success marker
if (raw.startsWith('{')) {
// Extract JSON part (excluding the success marker if present)
final jsonEnd = raw.indexOf('\nLSBLK_SUCCESS');
final jsonPart = jsonEnd > 0 ? raw.substring(0, jsonEnd) : raw;
try {
final Map<String, dynamic> jsonData = json.decode(jsonPart);
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
for (final device in blockdevices) {
// Process each device
_processTopLevelDevice(device, list);
}
// If we successfully parsed JSON and have valid disks, return them
if (list.isNotEmpty) {
return list;
}
} on FormatException catch (e) {
Loggers.app.warning('JSON parsing failed, falling back to df -k output: $e');
} catch (e) {
Loggers.app.warning('Error processing JSON disk data, falling back to df -k output: $e', e);
}
} else {
// Fallback to the old parsing method in case of non-JSON output
}
// Check if we have df -k output (fallback case)
if (raw.contains('Filesystem') && raw.contains('Mounted on')) {
return _parseWithOldMethod(raw);
}
// If we reach here, both parsing methods failed
Loggers.app.warning('Unable to parse disk info with any method');
} catch (e) {
Loggers.app.warning('Failed to parse disk info: $e', e);
Loggers.app.warning('Failed to parse disk info with both methods: $e', e);
}
return list;
}
@@ -88,6 +115,32 @@ class Disk with EquatableMixin {
}
}
/// Parse filesystem fields from device data
static ({BigInt size, BigInt used, BigInt avail, int usedPercent}) _parseFilesystemFields(Map<String, dynamic> device) {
// Helper function to parse size strings safely
BigInt parseSize(String? sizeStr) {
if (sizeStr == null || sizeStr.isEmpty || sizeStr == 'null' || sizeStr == '0') {
return BigInt.zero;
}
return (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
}
// Helper function to parse percentage strings
int parsePercent(String? percentStr) {
if (percentStr == null || percentStr.isEmpty || percentStr == 'null') {
return 0;
}
return int.tryParse(percentStr.replaceAll('%', '')) ?? 0;
}
return (
size: parseSize(device['fssize']?.toString()),
used: parseSize(device['fsused']?.toString()),
avail: parseSize(device['fsavail']?.toString()),
usedPercent: parsePercent(device['fsuse%']?.toString()),
);
}
/// Process a single device without recursively processing its children
static Disk? _processSingleDevice(Map<String, dynamic> device) {
final fstype = device['fstype']?.toString();
@@ -102,20 +155,7 @@ class Disk with EquatableMixin {
return null;
}
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final usedStr = device['fsused']?.toString() ?? '0';
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final availStr = device['fsavail']?.toString() ?? '0';
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
// Parse fsuse% which is usually in the format "45%"
String usePercentStr = device['fsuse%']?.toString() ?? '0';
usePercentStr = usePercentStr.replaceAll('%', '');
final usedPercent = int.tryParse(usePercentStr) ?? 0;
final fsFields = _parseFilesystemFields(device);
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
@@ -124,10 +164,10 @@ class Disk with EquatableMixin {
path: path,
fsTyp: fstype,
mount: mountpoint,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
usedPercent: fsFields.usedPercent,
used: fsFields.used,
size: fsFields.size,
avail: fsFields.avail,
name: name,
kname: kname,
uuid: uuid,
@@ -155,20 +195,7 @@ class Disk with EquatableMixin {
// Handle common filesystem cases or parent devices with children
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
final sizeStr = device['fssize']?.toString() ?? '0';
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final usedStr = device['fsused']?.toString() ?? '0';
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
final availStr = device['fsavail']?.toString() ?? '0';
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
// Parse fsuse% which is usually in the format "45%"
String usePercentStr = device['fsuse%']?.toString() ?? '0';
usePercentStr = usePercentStr.replaceAll('%', '');
final usedPercent = int.tryParse(usePercentStr) ?? 0;
final fsFields = _parseFilesystemFields(device);
final name = device['name']?.toString();
final kname = device['kname']?.toString();
final uuid = device['uuid']?.toString();
@@ -177,10 +204,10 @@ class Disk with EquatableMixin {
path: path,
fsTyp: fstype,
mount: mount,
usedPercent: usedPercent,
used: used,
size: size,
avail: avail,
usedPercent: fsFields.usedPercent,
used: fsFields.used,
size: fsFields.size,
avail: fsFields.avail,
name: name,
kname: kname,
uuid: uuid,

View File

@@ -1,4 +1,3 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/server/amd.dart';
@@ -11,25 +10,9 @@ import 'package:server_box/data/model/server/memory.dart';
import 'package:server_box/data/model/server/net_speed.dart';
import 'package:server_box/data/model/server/nvdia.dart';
import 'package:server_box/data/model/server/sensors.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/temp.dart';
class Server {
Spi spi;
ServerStatus status;
SSHClient? client;
ServerConn conn;
Server(this.spi, this.status, this.conn, {this.client});
bool get needGenClient => conn < ServerConn.connecting;
bool get canViewDetails => conn == ServerConn.finished;
String get id => spi.id;
}
class ServerStatus {
Cpus cpu;
Memory mem;

View File

@@ -4,10 +4,8 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/store/server.dart';
part 'server_private_info.freezed.dart';
@@ -58,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;
@@ -85,23 +84,26 @@ extension Spix on Spi {
return newSpi.id;
}
/// Json encode to string.
String toJsonString() => json.encode(toJson());
VNode<Server>? get server => ServerProvider.pick(spi: this);
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
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');
}
@@ -146,5 +148,6 @@ extension Spix on Spi {
id: 'id',
);
/// Returns true if the user is 'root'.
bool get isRoot => user == 'root';
}

View File

@@ -104,6 +104,11 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
try {
req.ss.disk = Disk.parse(StatusCmdType.disk.findInMap(parsedOutput));
} catch (e, s) {
Loggers.app.warning(e, s);
}
try {
req.ss.diskUsage = DiskUsage.parse(req.ss.disk);
} catch (e, s) {
Loggers.app.warning(e, s);
@@ -294,7 +299,9 @@ String? _parseSysVer(String raw) {
String? _parseHostName(String raw) {
if (raw.isEmpty) return null;
if (raw.contains(ScriptConstants.scriptFile)) return null;
return raw;
final trimmed = raw.trim();
if (trimmed.isEmpty) return null;
return trimmed;
}
// Windows status parsing implementation

View File

@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app.g.dart';
part 'app.freezed.dart';
@freezed
abstract class AppState with _$AppState {
const factory AppState() = _AppState;
}
@Riverpod(keepAlive: true)
class AppStates extends _$AppStates {
static BuildContext? ctx;
@override
AppState build() {
return const AppState();
}
}

View File

@@ -1,206 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'app.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppState {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'AppState()';
}
}
/// @nodoc
class $AppStateCopyWith<$Res> {
$AppStateCopyWith(AppState _, $Res Function(AppState) __);
}
/// Adds pattern-matching-related methods to [AppState].
extension AppStatePatterns on AppState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AppState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AppState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AppState value) $default,){
final _that = this;
switch (_that) {
case _AppState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AppState value)? $default,){
final _that = this;
switch (_that) {
case _AppState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function()? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppState() when $default != null:
return $default();case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function() $default,) {final _that = this;
switch (_that) {
case _AppState():
return $default();case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function()? $default,) {final _that = this;
switch (_that) {
case _AppState() when $default != null:
return $default();case _:
return null;
}
}
}
/// @nodoc
class _AppState implements AppState {
const _AppState();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'AppState()';
}
}
// dart format on

View File

@@ -1,62 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
@ProviderFor(AppStates)
const appStatesProvider = AppStatesProvider._();
final class AppStatesProvider extends $NotifierProvider<AppStates, AppState> {
const AppStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appStatesHash();
@$internal
@override
AppStates create() => AppStates();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AppState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AppState>(value),
);
}
}
String _$appStatesHash() => r'ef96f10f6fff0f3dd6d3128ebf070ad79cbc8bc9';
abstract class _$AppStates extends $Notifier<AppState> {
AppState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AppState, AppState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AppState, AppState>,
AppState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -4,6 +4,8 @@ import 'dart:convert';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
@@ -12,63 +14,61 @@ import 'package:server_box/data/model/container/ps.dart';
import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/store.dart';
part 'container.freezed.dart';
part 'container.g.dart';
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
class ContainerProvider extends ChangeNotifier {
final SSHClient? client;
final String userName;
final String hostId;
final BuildContext context;
List<ContainerPs>? items;
List<ContainerImg>? images;
String? version;
ContainerErr? error;
String? runLog;
ContainerType type;
var sudoCompleter = Completer<bool>();
bool isBusy = false;
@freezed
abstract class ContainerState with _$ContainerState {
const factory ContainerState({
@Default(null) List<ContainerPs>? items,
@Default(null) List<ContainerImg>? images,
@Default(null) String? version,
@Default(null) ContainerErr? error,
@Default(null) String? runLog,
@Default(ContainerType.docker) ContainerType type,
@Default(false) bool isBusy,
}) = _ContainerState;
}
ContainerProvider({
required this.client,
required this.userName,
required this.hostId,
required this.context,
}) : type = Stores.container.getType(hostId) {
refresh();
@riverpod
class ContainerNotifier extends _$ContainerNotifier {
var sudoCompleter = Completer<bool>();
@override
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
final type = Stores.container.getType(hostId);
final initialState = ContainerState(type: type);
// Async initialization
Future.microtask(() => refresh());
return initialState;
}
Future<void> setType(ContainerType type) async {
this.type = type;
state = state.copyWith(
type: type,
error: null,
runLog: null,
items: null,
images: null,
version: null,
);
Stores.container.setType(type, hostId);
error = runLog = items = images = version = null;
sudoCompleter = Completer<bool>();
notifyListeners();
await refresh();
}
// Future<bool> _checkDockerInstalled(SSHClient client) async {
// final session = await client.execute("docker");
// await session.done;
// // debugPrint('docker code: ${session.exitCode}');
// return session.exitCode == 0;
// }
// String _removeSudoPrompts(String value) {
// final regex = RegExp(r"\[sudo\] password for \w+:");
// if (value.startsWith(regex)) {
// return value.replaceFirstMapped(regex, (match) => "");
// }
// return value;
// }
void _requiresSudo() async {
/// Podman is rootless
if (type == ContainerType.podman) return sudoCompleter.complete(false);
if (state.type == ContainerType.podman) return sudoCompleter.complete(false);
if (!Stores.setting.containerTrySudo.fetch()) {
return sudoCompleter.complete(false);
}
final res = await client?.run(_wrap(ContainerCmdType.images.exec(type)));
final res = await client?.run(_wrap(ContainerCmdType.images.exec(state.type)));
if (res?.string.toLowerCase().contains('permission denied') ?? false) {
return sudoCompleter.complete(true);
}
@@ -76,8 +76,8 @@ class ContainerProvider extends ChangeNotifier {
}
Future<void> refresh({bool isAuto = false}) async {
if (isBusy) return;
isBusy = true;
if (state.isBusy) return;
state = state.copyWith(isBusy: true);
if (!sudoCompleter.isCompleted) _requiresSudo();
@@ -85,11 +85,14 @@ class ContainerProvider extends ChangeNotifier {
/// If sudo is required and auto refresh is enabled, skip the refresh.
/// Or this will ask for pwd again and again.
if (sudo && isAuto) return;
if (sudo && isAuto) {
state = state.copyWith(isBusy: false);
return;
}
final includeStats = Stores.setting.containerParseStat.fetch();
var raw = '';
final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats));
final code = await client?.execWithPwd(
cmd,
context: context,
@@ -97,75 +100,79 @@ class ContainerProvider extends ChangeNotifier {
id: hostId,
);
isBusy = false;
state = state.copyWith(isBusy: false);
if (!context.mounted) return;
/// Code 127 means command not found
if (code == 127 || raw.contains(_dockerNotFound)) {
error = ContainerErr(type: ContainerErrType.notInstalled);
notifyListeners();
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
return;
}
// Check result segments count
final segments = raw.split(ScriptConstants.separator);
if (segments.length != ContainerCmdType.values.length) {
error = ContainerErr(
type: ContainerErrType.segmentsNotMatch,
message: 'Container segments: ${segments.length}',
state = state.copyWith(
error: ContainerErr(
type: ContainerErrType.segmentsNotMatch,
message: 'Container segments: ${segments.length}',
),
);
Loggers.app.warning('Container segments: ${segments.length}\n$raw');
notifyListeners();
return;
}
// Parse version
final verRaw = ContainerCmdType.version.find(segments);
try {
version = json.decode(verRaw)['Client']['Version'];
final version = json.decode(verRaw)['Client']['Version'];
state = state.copyWith(version: version, error: null);
} catch (e, trace) {
error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
state = state.copyWith(
error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),
);
Loggers.app.warning('Container version failed', e, trace);
} finally {
notifyListeners();
}
// Parse ps
final psRaw = ContainerCmdType.ps.find(segments);
try {
final lines = psRaw.split('\n');
if (type == ContainerType.docker) {
if (state.type == ContainerType.docker) {
/// Due to the fetched data is not in json format, skip table header
lines.removeWhere((element) => element.contains('CONTAINER ID'));
}
lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList();
state = state.copyWith(items: items);
} catch (e, trace) {
error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
state = state.copyWith(
error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),
);
Loggers.app.warning('Container ps failed', e, trace);
} finally {
notifyListeners();
}
// Parse images
final imageRaw = ContainerCmdType.images.find(segments).trim();
final isEntireJson = imageRaw.startsWith('[') && imageRaw.endsWith(']');
try {
List<ContainerImg> images;
if (isEntireJson) {
images = (json.decode(imageRaw) as List)
.map((e) => ContainerImg.fromRawJson(json.encode(e), type))
.map((e) => ContainerImg.fromRawJson(json.encode(e), state.type))
.toList();
} else {
final lines = imageRaw.split('\n');
lines.removeWhere((element) => element.isEmpty);
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
images = lines.map((e) => ContainerImg.fromRawJson(e, state.type)).toList();
}
state = state.copyWith(images: images);
} catch (e, trace) {
error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
state = state.copyWith(
error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),
);
Loggers.app.warning('Container images failed', e, trace);
} finally {
notifyListeners();
}
// Parse stats
@@ -173,7 +180,7 @@ class ContainerProvider extends ChangeNotifier {
try {
final statsLines = statsRaw.split('\n');
statsLines.removeWhere((element) => element.isEmpty);
for (var item in items!) {
for (var item in state.items!) {
final id = item.id;
if (id == null) continue;
final statsLine = statsLines.firstWhereOrNull(
@@ -185,10 +192,10 @@ class ContainerProvider extends ChangeNotifier {
item.parseStats(statsLine);
}
} catch (e, trace) {
error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
state = state.copyWith(
error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),
);
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
} finally {
notifyListeners();
}
}
@@ -223,25 +230,23 @@ class ContainerProvider extends ChangeNotifier {
}
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
cmd = switch (type) {
cmd = switch (state.type) {
ContainerType.docker => 'docker $cmd',
ContainerType.podman => 'podman $cmd',
};
runLog = '';
state = state.copyWith(runLog: '');
final errs = <String>[];
final code = await client?.execWithPwd(
_wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
context: context,
onStdout: (data, _) {
runLog = '$runLog$data';
notifyListeners();
state = state.copyWith(runLog: '${state.runLog}$data');
},
onStderr: (data, _) => errs.add(data),
id: hostId,
);
runLog = null;
notifyListeners();
state = state.copyWith(runLog: null);
if (code != 0) {
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
@@ -262,6 +267,7 @@ class ContainerProvider extends ChangeNotifier {
}
}
const _jsonFmt = '--format "{{json .}}"';
enum ContainerCmdType {

View File

@@ -0,0 +1,305 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'container.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ContainerState {
List<ContainerPs>? get items; List<ContainerImg>? get images; String? get version; ContainerErr? get error; String? get runLog; ContainerType get type; bool get isBusy;
/// Create a copy of ContainerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ContainerStateCopyWith<ContainerState> get copyWith => _$ContainerStateCopyWithImpl<ContainerState>(this as ContainerState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,error,runLog,type,isBusy);
@override
String toString() {
return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)';
}
}
/// @nodoc
abstract mixin class $ContainerStateCopyWith<$Res> {
factory $ContainerStateCopyWith(ContainerState value, $Res Function(ContainerState) _then) = _$ContainerStateCopyWithImpl;
@useResult
$Res call({
List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy
});
}
/// @nodoc
class _$ContainerStateCopyWithImpl<$Res>
implements $ContainerStateCopyWith<$Res> {
_$ContainerStateCopyWithImpl(this._self, this._then);
final ContainerState _self;
final $Res Function(ContainerState) _then;
/// Create a copy of ContainerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) {
return _then(_self.copyWith(
items: freezed == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
as List<ContainerPs>?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable
as List<ContainerImg>?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [ContainerState].
extension ContainerStatePatterns on ContainerState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ContainerState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ContainerState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ContainerState value) $default,){
final _that = this;
switch (_that) {
case _ContainerState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ContainerState value)? $default,){
final _that = this;
switch (_that) {
case _ContainerState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ContainerState() when $default != null:
return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this;
switch (_that) {
case _ContainerState():
return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this;
switch (_that) {
case _ContainerState() when $default != null:
return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:
return null;
}
}
}
/// @nodoc
class _ContainerState implements ContainerState {
const _ContainerState({final List<ContainerPs>? items = null, final List<ContainerImg>? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images;
final List<ContainerPs>? _items;
@override@JsonKey() List<ContainerPs>? get items {
final value = _items;
if (value == null) return null;
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<ContainerImg>? _images;
@override@JsonKey() List<ContainerImg>? get images {
final value = _images;
if (value == null) return null;
if (_images is EqualUnmodifiableListView) return _images;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override@JsonKey() final String? version;
@override@JsonKey() final ContainerErr? error;
@override@JsonKey() final String? runLog;
@override@JsonKey() final ContainerType type;
@override@JsonKey() final bool isBusy;
/// Create a copy of ContainerState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ContainerStateCopyWith<_ContainerState> get copyWith => __$ContainerStateCopyWithImpl<_ContainerState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,error,runLog,type,isBusy);
@override
String toString() {
return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)';
}
}
/// @nodoc
abstract mixin class _$ContainerStateCopyWith<$Res> implements $ContainerStateCopyWith<$Res> {
factory _$ContainerStateCopyWith(_ContainerState value, $Res Function(_ContainerState) _then) = __$ContainerStateCopyWithImpl;
@override @useResult
$Res call({
List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy
});
}
/// @nodoc
class __$ContainerStateCopyWithImpl<$Res>
implements _$ContainerStateCopyWith<$Res> {
__$ContainerStateCopyWithImpl(this._self, this._then);
final _ContainerState _self;
final $Res Function(_ContainerState) _then;
/// Create a copy of ContainerState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) {
return _then(_ContainerState(
items: freezed == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
as List<ContainerPs>?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable
as List<ContainerImg>?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,123 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'container.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ContainerNotifier)
const containerProvider = ContainerNotifierFamily._();
final class ContainerNotifierProvider
extends $NotifierProvider<ContainerNotifier, ContainerState> {
const ContainerNotifierProvider._({
required ContainerNotifierFamily super.from,
required (SSHClient?, String, String, BuildContext) super.argument,
}) : super(
retry: null,
name: r'containerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$containerNotifierHash();
@override
String toString() {
return r'containerProvider'
''
'$argument';
}
@$internal
@override
ContainerNotifier create() => ContainerNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ContainerState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ContainerState>(value),
);
}
@override
bool operator ==(Object other) {
return other is ContainerNotifierProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
final class ContainerNotifierFamily extends $Family
with
$ClassFamilyOverride<
ContainerNotifier,
ContainerState,
ContainerState,
ContainerState,
(SSHClient?, String, String, BuildContext)
> {
const ContainerNotifierFamily._()
: super(
retry: null,
name: r'containerProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ContainerNotifierProvider call(
SSHClient? client,
String userName,
String hostId,
BuildContext context,
) => ContainerNotifierProvider._(
argument: (client, userName, hostId, context),
from: this,
);
@override
String toString() => r'containerProvider';
}
abstract class _$ContainerNotifier extends $Notifier<ContainerState> {
late final _$args = ref.$arg as (SSHClient?, String, String, BuildContext);
SSHClient? get client => _$args.$1;
String get userName => _$args.$2;
String get hostId => _$args.$3;
BuildContext get context => _$args.$4;
ContainerState build(
SSHClient? client,
String userName,
String hostId,
BuildContext context,
);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args.$1, _$args.$2, _$args.$3, _$args.$4);
final ref = this.ref as $Ref<ContainerState, ContainerState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ContainerState, ContainerState>,
ContainerState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,45 +1,61 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/res/store.dart';
class PrivateKeyProvider extends Provider {
const PrivateKeyProvider._();
static const instance = PrivateKeyProvider._();
part 'private_key.freezed.dart';
part 'private_key.g.dart';
static final pkis = <PrivateKeyInfo>[].vn;
@freezed
abstract class PrivateKeyState with _$PrivateKeyState {
const factory PrivateKeyState({@Default(<PrivateKeyInfo>[]) List<PrivateKeyInfo> keys}) = _PrivateKeyState;
}
@Riverpod(keepAlive: true)
class PrivateKeyNotifier extends _$PrivateKeyNotifier {
@override
void load() {
super.load();
pkis.value = Stores.key.fetch();
PrivateKeyState build() {
return _load();
}
static void add(PrivateKeyInfo info) {
pkis.value.add(info);
pkis.notify();
void reload() {
final newState = _load();
if (newState == state) return;
state = newState;
}
PrivateKeyState _load() {
final keys = Stores.key.fetch();
return stateOrNull?.copyWith(keys: keys) ?? PrivateKeyState(keys: keys);
}
void add(PrivateKeyInfo info) {
final newKeys = [...state.keys, info];
state = state.copyWith(keys: newKeys);
Stores.key.put(info);
bakSync.sync(milliDelay: 1000);
}
static void delete(PrivateKeyInfo info) {
pkis.value.removeWhere((e) => e.id == info.id);
pkis.notify();
void delete(PrivateKeyInfo info) {
final newKeys = state.keys.where((e) => e.id != info.id).toList();
state = state.copyWith(keys: newKeys);
Stores.key.delete(info);
bakSync.sync(milliDelay: 1000);
}
static void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final idx = pkis.value.indexWhere((e) => e.id == old.id);
void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
final keys = [...state.keys];
final idx = keys.indexWhere((e) => e.id == old.id);
if (idx == -1) {
pkis.value.add(newInfo);
keys.add(newInfo);
Stores.key.put(newInfo);
Stores.key.delete(old);
} else {
pkis.value[idx] = newInfo;
keys[idx] = newInfo;
Stores.key.put(newInfo);
}
pkis.notify();
state = state.copyWith(keys: keys);
bakSync.sync(milliDelay: 1000);
}
}

View File

@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'private_key.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PrivateKeyState {
List<PrivateKeyInfo> get keys;
/// Create a copy of PrivateKeyState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PrivateKeyStateCopyWith<PrivateKeyState> get copyWith => _$PrivateKeyStateCopyWithImpl<PrivateKeyState>(this as PrivateKeyState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PrivateKeyState&&const DeepCollectionEquality().equals(other.keys, keys));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(keys));
@override
String toString() {
return 'PrivateKeyState(keys: $keys)';
}
}
/// @nodoc
abstract mixin class $PrivateKeyStateCopyWith<$Res> {
factory $PrivateKeyStateCopyWith(PrivateKeyState value, $Res Function(PrivateKeyState) _then) = _$PrivateKeyStateCopyWithImpl;
@useResult
$Res call({
List<PrivateKeyInfo> keys
});
}
/// @nodoc
class _$PrivateKeyStateCopyWithImpl<$Res>
implements $PrivateKeyStateCopyWith<$Res> {
_$PrivateKeyStateCopyWithImpl(this._self, this._then);
final PrivateKeyState _self;
final $Res Function(PrivateKeyState) _then;
/// Create a copy of PrivateKeyState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? keys = null,}) {
return _then(_self.copyWith(
keys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable
as List<PrivateKeyInfo>,
));
}
}
/// Adds pattern-matching-related methods to [PrivateKeyState].
extension PrivateKeyStatePatterns on PrivateKeyState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PrivateKeyState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PrivateKeyState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PrivateKeyState value) $default,){
final _that = this;
switch (_that) {
case _PrivateKeyState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PrivateKeyState value)? $default,){
final _that = this;
switch (_that) {
case _PrivateKeyState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<PrivateKeyInfo> keys)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PrivateKeyState() when $default != null:
return $default(_that.keys);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<PrivateKeyInfo> keys) $default,) {final _that = this;
switch (_that) {
case _PrivateKeyState():
return $default(_that.keys);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<PrivateKeyInfo> keys)? $default,) {final _that = this;
switch (_that) {
case _PrivateKeyState() when $default != null:
return $default(_that.keys);case _:
return null;
}
}
}
/// @nodoc
class _PrivateKeyState implements PrivateKeyState {
const _PrivateKeyState({final List<PrivateKeyInfo> keys = const <PrivateKeyInfo>[]}): _keys = keys;
final List<PrivateKeyInfo> _keys;
@override@JsonKey() List<PrivateKeyInfo> get keys {
if (_keys is EqualUnmodifiableListView) return _keys;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_keys);
}
/// Create a copy of PrivateKeyState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PrivateKeyStateCopyWith<_PrivateKeyState> get copyWith => __$PrivateKeyStateCopyWithImpl<_PrivateKeyState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PrivateKeyState&&const DeepCollectionEquality().equals(other._keys, _keys));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_keys));
@override
String toString() {
return 'PrivateKeyState(keys: $keys)';
}
}
/// @nodoc
abstract mixin class _$PrivateKeyStateCopyWith<$Res> implements $PrivateKeyStateCopyWith<$Res> {
factory _$PrivateKeyStateCopyWith(_PrivateKeyState value, $Res Function(_PrivateKeyState) _then) = __$PrivateKeyStateCopyWithImpl;
@override @useResult
$Res call({
List<PrivateKeyInfo> keys
});
}
/// @nodoc
class __$PrivateKeyStateCopyWithImpl<$Res>
implements _$PrivateKeyStateCopyWith<$Res> {
__$PrivateKeyStateCopyWithImpl(this._self, this._then);
final _PrivateKeyState _self;
final $Res Function(_PrivateKeyState) _then;
/// Create a copy of PrivateKeyState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? keys = null,}) {
return _then(_PrivateKeyState(
keys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable
as List<PrivateKeyInfo>,
));
}
}
// dart format on

View File

@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'private_key.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(PrivateKeyNotifier)
const privateKeyProvider = PrivateKeyNotifierProvider._();
final class PrivateKeyNotifierProvider
extends $NotifierProvider<PrivateKeyNotifier, PrivateKeyState> {
const PrivateKeyNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'privateKeyProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$privateKeyNotifierHash();
@$internal
@override
PrivateKeyNotifier create() => PrivateKeyNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PrivateKeyState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PrivateKeyState>(value),
);
}
}
String _$privateKeyNotifierHash() =>
r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7';
abstract class _$PrivateKeyNotifier extends $Notifier<PrivateKeyState> {
PrivateKeyState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<PrivateKeyState, PrivateKeyState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<PrivateKeyState, PrivateKeyState>,
PrivateKeyState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/misc.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server/all.dart';
import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/provider/snippet.dart';
/// !library;
/// ref.useNotifier, ref.readProvider, ref.watchProvider
///
/// Usage:
/// - `providers.read.server` -> `ref.read(serversProvider)`
/// - `providers.use.snippet` -> `ref.read(snippetsNotifierProvider.notifier)`
extension RiverpodNotifiers on ConsumerState {
T useNotifier<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) {
return ref.read(provider.notifier);
}
T readProvider<T>(ProviderBase<T> provider) {
return ref.read(provider);
}
T watchProvider<T>(ProviderBase<T> provider) {
return ref.watch(provider);
}
MyProviders get providers => MyProviders(ref);
}
final class MyProviders {
final WidgetRef ref;
const MyProviders(this.ref);
ReadMyProvider get read => ReadMyProvider(ref);
WatchMyProvider get watch => WatchMyProvider(ref);
UseNotifierMyProvider get use => UseNotifierMyProvider(ref);
}
final class ReadMyProvider {
final WidgetRef ref;
const ReadMyProvider(this.ref);
T call<T>(ProviderBase<T> provider) => ref.read(provider);
// Specific provider getters
ServersState get server => ref.read(serversProvider);
SnippetState get snippet => ref.read(snippetProvider);
AppState get app => ref.read(appStatesProvider);
PrivateKeyState get privateKey => ref.read(privateKeyProvider);
SftpState get sftp => ref.read(sftpProvider);
}
final class WatchMyProvider {
final WidgetRef ref;
const WatchMyProvider(this.ref);
T call<T>(ProviderBase<T> provider) => ref.watch(provider);
// Specific provider getters
ServersState get server => ref.watch(serversProvider);
SnippetState get snippet => ref.watch(snippetProvider);
AppState get app => ref.watch(appStatesProvider);
PrivateKeyState get privateKey => ref.watch(privateKeyProvider);
SftpState get sftp => ref.watch(sftpProvider);
}
final class UseNotifierMyProvider {
final WidgetRef ref;
const UseNotifierMyProvider(this.ref);
T call<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) =>
ref.read(provider.notifier);
// Specific provider notifier getters
ServersNotifier get server => ref.read(serversProvider.notifier);
SnippetNotifier get snippet => ref.read(snippetProvider.notifier);
AppStates get app => ref.read(appStatesProvider.notifier);
PrivateKeyNotifier get privateKey => ref.read(privateKeyProvider.notifier);
SftpNotifier get sftp => ref.read(sftpProvider.notifier);
}

View File

@@ -7,71 +7,90 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/pve.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/provider/server/single.dart';
part 'pve.freezed.dart';
part 'pve.g.dart';
typedef PveCtrlFunc = Future<bool> Function(String node, String id);
final class PveProvider extends ChangeNotifier {
final Spi spi;
@freezed
abstract class PveState with _$PveState {
const factory PveState({
@Default(null) PveErr? error,
@Default(null) PveRes? data,
@Default(null) String? release,
@Default(false) bool isBusy,
@Default(false) bool isConnected,
}) = _PveState;
}
@riverpod
class PveNotifier extends _$PveNotifier {
late final Spi spi;
late String addr;
late final SSHClient _client;
late final ServerSocket _serverSocket;
final List<SSHForwardChannel> _forwards = [];
int _localPort = 0;
late final Dio session;
late final bool _ignoreCert;
PveProvider({required this.spi}) {
final client = spi.server?.value.client;
@override
PveState build(Spi spiParam) {
spi = spiParam;
final serverState = ref.watch(serverProvider(spi.id));
final client = serverState.client;
if (client == null) {
throw Exception('Server client is null');
return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null'));
}
_client = client;
final addr = spi.custom?.pveAddr;
if (addr == null) {
err.value = 'PVE address is null';
return;
return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null'));
}
this.addr = addr;
_init();
_ignoreCert = spi.custom?.pveIgnoreCert ?? false;
_initSession();
// Async initialization
Future.microtask(() => _init());
return const PveState();
}
final err = ValueNotifier<String?>(null);
final connected = Completer<void>();
void _initSession() {
session = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.connectionFactory = cf;
if (_ignoreCert) {
client.badCertificateCallback = (_, _, _) => true;
}
return client;
},
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
);
}
late final _ignoreCert = spi.custom?.pveIgnoreCert ?? false;
late final session = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.connectionFactory = cf;
if (_ignoreCert) {
client.badCertificateCallback = (_, _, _) => true;
}
return client;
},
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
);
final data = ValueNotifier<PveRes?>(null);
bool get onlyOneNode => data.value?.nodes.length == 1;
String? release;
bool isBusy = false;
bool get onlyOneNode => state.data?.nodes.length == 1;
Future<void> _init() async {
try {
await _forward();
await _login();
await _getRelease();
state = state.copyWith(isConnected: true);
} on PveErr {
err.value = l10n.pveLoginFailed;
state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed));
} catch (e, s) {
Loggers.app.warning('PVE init failed', e, s);
err.value = e.toString();
} finally {
connected.complete();
state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));
}
}
@@ -146,72 +165,81 @@ final class PveProvider extends ChangeNotifier {
final resp = await session.get('$addr/api2/extjs/version');
final version = resp.data['data']['release'] as String?;
if (version != null) {
release = version;
state = state.copyWith(release: version);
}
}
Future<void> list() async {
await connected.future;
if (isBusy) return;
isBusy = true;
if (!state.isConnected || state.isBusy) return;
state = state.copyWith(isBusy: true);
try {
final resp = await session.get('$addr/api2/json/cluster/resources');
final res = resp.data['data'] as List;
final result = await Computer.shared.start(PveRes.parse, (
res,
data.value,
state.data,
));
data.value = result;
state = state.copyWith(data: result, error: null);
} catch (e) {
Loggers.app.warning('PVE list failed', e);
err.value = e.toString();
state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));
} finally {
isBusy = false;
state = state.copyWith(isBusy: false);
}
}
Future<bool> reboot(String node, String id) async {
await connected.future;
if (!state.isConnected) return false;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/reboot',
);
return _isCtrlSuc(resp);
final success = _isCtrlSuc(resp);
if (success) await list(); // Refresh data
return success;
}
Future<bool> start(String node, String id) async {
await connected.future;
if (!state.isConnected) return false;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/start',
);
return _isCtrlSuc(resp);
final success = _isCtrlSuc(resp);
if (success) await list(); // Refresh data
return success;
}
Future<bool> stop(String node, String id) async {
await connected.future;
if (!state.isConnected) return false;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/stop',
);
return _isCtrlSuc(resp);
final success = _isCtrlSuc(resp);
if (success) await list(); // Refresh data
return success;
}
Future<bool> shutdown(String node, String id) async {
await connected.future;
if (!state.isConnected) return false;
final resp = await session.post(
'$addr/api2/json/nodes/$node/$id/status/shutdown',
);
return _isCtrlSuc(resp);
final success = _isCtrlSuc(resp);
if (success) await list(); // Refresh data
return success;
}
bool _isCtrlSuc(Response resp) {
return resp.statusCode == 200;
}
@override
Future<void> dispose() async {
super.dispose();
await _serverSocket.close();
try {
await _serverSocket.close();
} catch (_) {}
for (final forward in _forwards) {
forward.close();
try {
forward.close();
} catch (_) {}
}
}
}

View File

@@ -0,0 +1,283 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'pve.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PveState {
PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected;
/// Create a copy of PveState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PveStateCopyWith<PveState> get copyWith => _$PveStateCopyWithImpl<PveState>(this as PveState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected));
}
@override
int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected);
@override
String toString() {
return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)';
}
}
/// @nodoc
abstract mixin class $PveStateCopyWith<$Res> {
factory $PveStateCopyWith(PveState value, $Res Function(PveState) _then) = _$PveStateCopyWithImpl;
@useResult
$Res call({
PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected
});
}
/// @nodoc
class _$PveStateCopyWithImpl<$Res>
implements $PveStateCopyWith<$Res> {
_$PveStateCopyWithImpl(this._self, this._then);
final PveState _self;
final $Res Function(PveState) _then;
/// Create a copy of PveState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) {
return _then(_self.copyWith(
error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable
as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [PveState].
extension PveStatePatterns on PveState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PveState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PveState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PveState value) $default,){
final _that = this;
switch (_that) {
case _PveState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PveState value)? $default,){
final _that = this;
switch (_that) {
case _PveState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PveState() when $default != null:
return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected) $default,) {final _that = this;
switch (_that) {
case _PveState():
return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,) {final _that = this;
switch (_that) {
case _PveState() when $default != null:
return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:
return null;
}
}
}
/// @nodoc
class _PveState implements PveState {
const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false});
@override@JsonKey() final PveErr? error;
@override@JsonKey() final PveRes? data;
@override@JsonKey() final String? release;
@override@JsonKey() final bool isBusy;
@override@JsonKey() final bool isConnected;
/// Create a copy of PveState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PveStateCopyWith<_PveState> get copyWith => __$PveStateCopyWithImpl<_PveState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected));
}
@override
int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected);
@override
String toString() {
return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)';
}
}
/// @nodoc
abstract mixin class _$PveStateCopyWith<$Res> implements $PveStateCopyWith<$Res> {
factory _$PveStateCopyWith(_PveState value, $Res Function(_PveState) _then) = __$PveStateCopyWithImpl;
@override @useResult
$Res call({
PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected
});
}
/// @nodoc
class __$PveStateCopyWithImpl<$Res>
implements _$PveStateCopyWith<$Res> {
__$PveStateCopyWithImpl(this._self, this._then);
final _PveState _self;
final $Res Function(_PveState) _then;
/// Create a copy of PveState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) {
return _then(_PveState(
error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable
as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,101 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pve.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(PveNotifier)
const pveProvider = PveNotifierFamily._();
final class PveNotifierProvider
extends $NotifierProvider<PveNotifier, PveState> {
const PveNotifierProvider._({
required PveNotifierFamily super.from,
required Spi super.argument,
}) : super(
retry: null,
name: r'pveProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pveNotifierHash();
@override
String toString() {
return r'pveProvider'
''
'($argument)';
}
@$internal
@override
PveNotifier create() => PveNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PveState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PveState>(value),
);
}
@override
bool operator ==(Object other) {
return other is PveNotifierProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$pveNotifierHash() => r'ba5f2d6cb47c33735f7cc09b771b4a86501b86c6';
final class PveNotifierFamily extends $Family
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
const PveNotifierFamily._()
: super(
retry: null,
name: r'pveProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
PveNotifierProvider call(Spi spiParam) =>
PveNotifierProvider._(argument: spiParam, from: this);
@override
String toString() => r'pveProvider';
}
abstract class _$PveNotifier extends $Notifier<PveState> {
late final _$args = ref.$arg as Spi;
Spi get spiParam => _$args;
PveState build(Spi spiParam);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref = this.ref as $Ref<PveState, PveState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<PveState, PveState>,
PveState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,505 +0,0 @@
import 'dart:async';
// import 'dart:io';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/helper/system_detector.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/res/status.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/ssh/session_manager.dart';
class ServerProvider extends Provider {
const ServerProvider._();
static const instance = ServerProvider._();
static final Map<String, VNode<Server>> servers = {};
static final serverOrder = <String>[].vn;
static final _tags = <String>{}.vn;
static VNode<Set<String>> get tags => _tags;
static Timer? _timer;
static final _manualDisconnectedIds = <String>{};
static final _serverIdsUpdating = <String, Future<void>?>{};
@override
Future<void> load() async {
super.load();
// #147
// Clear all servers because of restarting app will cause duplicate servers
final oldServers = Map<String, VNode<Server>>.from(servers);
servers.clear();
serverOrder.value.clear();
final spis = Stores.server.fetch();
for (int idx = 0; idx < spis.length; idx++) {
final spi = spis[idx];
final originServer = oldServers[spi.id];
/// #258
/// If not [shouldReconnect], then keep the old state.
if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) {
originServer.value.spi = spi;
servers[spi.id] = originServer;
} else {
final newServer = genServer(spi);
servers[spi.id] = newServer.vn;
}
}
final serverOrder_ = Stores.setting.serverOrder.fetch();
if (serverOrder_.isNotEmpty) {
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
serverOrder.value.addAll(spis.map((e) => e.id));
} else {
serverOrder.value.addAll(servers.keys);
}
// Must use [equals] to compare [Order] here.
if (!serverOrder.value.equals(serverOrder_)) {
Stores.setting.serverOrder.put(serverOrder.value);
}
_updateTags();
// Must notify here, or the UI will not be updated.
serverOrder.notify();
}
/// Get a [Server] by [spi] or [id].
///
/// Priority: [spi] > [id]
static VNode<Server>? pick({Spi? spi, String? id}) {
if (spi != null) {
return servers[spi.id];
}
if (id != null) {
return servers[id];
}
return null;
}
static void _updateTags() {
final tags = <String>{};
for (final s in servers.values) {
final spiTags = s.value.spi.tags;
if (spiTags == null) continue;
for (final t in spiTags) {
tags.add(t);
}
}
_tags.value = tags;
}
static Server genServer(Spi spi) {
return Server(spi, InitStatus.status, ServerConn.disconnected);
}
/// if [spi] is specificed then only refresh this server
/// [onlyFailed] only refresh failed servers
static Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
if (spi != null) {
_manualDisconnectedIds.remove(spi.id);
await _getData(spi);
return;
}
await Future.wait(
servers.values.map((val) async {
final s = val.value;
if (onlyFailed) {
if (s.conn != ServerConn.failed) return;
TryLimiter.reset(s.spi.id);
}
if (_manualDisconnectedIds.contains(s.spi.id)) return;
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
return;
}
// Check if already updating, and if so, wait for it to complete
final existingUpdate = _serverIdsUpdating[s.spi.id];
if (existingUpdate != null) {
// Already updating, wait for the existing update to complete
try {
await existingUpdate;
} catch (e) {
// Ignore errors from the existing update, we'll try our own
}
return;
}
// Start a new update operation
final updateFuture = _updateServer(s.spi);
_serverIdsUpdating[s.spi.id] = updateFuture;
try {
await updateFuture;
} finally {
_serverIdsUpdating.remove(s.spi.id);
}
}),
);
}
static Future<void> _updateServer(Spi spi) async {
await _getData(spi);
}
static Future<void> startAutoRefresh() async {
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
stopAutoRefresh();
if (duration == 0) return;
if (duration < 0 || duration > 10 || duration == 1) {
duration = 3;
Loggers.app.warning('Invalid duration: $duration, use default 3');
}
_timer = Timer.periodic(Duration(seconds: duration), (_) async {
await refresh();
});
}
static void stopAutoRefresh() {
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
}
static bool get isAutoRefreshOn => _timer != null;
static void setDisconnected() {
for (final s in servers.values) {
s.value.conn = ServerConn.disconnected;
s.notify();
// Update SSH session status to disconnected
final sessionId = 'ssh_${s.value.spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
}
//TryLimiter.clear();
}
static void closeServer({String? id}) {
if (id == null) {
for (final s in servers.values) {
_closeOneServer(s.value.spi.id);
}
return;
}
_closeOneServer(id);
}
static void _closeOneServer(String id) {
final s = servers[id];
if (s == null) {
Loggers.app.warning('Server with id $id not found');
return;
}
final item = s.value;
item.client?.close();
item.client = null;
item.conn = ServerConn.disconnected;
_manualDisconnectedIds.add(id);
s.notify();
// Remove SSH session when server is manually closed
final sessionId = 'ssh_$id';
TermSessionManager.remove(sessionId);
}
static void addServer(Spi spi) {
servers[spi.id] = genServer(spi).vn;
Stores.server.put(spi);
serverOrder.value.add(spi.id);
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
_updateTags();
refresh(spi: spi);
bakSync.sync(milliDelay: 1000);
}
static void delServer(String id) {
servers.remove(id);
serverOrder.value.remove(id);
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
Stores.server.delete(id);
_updateTags();
// Remove SSH session when server is deleted
final sessionId = 'ssh_$id';
TermSessionManager.remove(sessionId);
bakSync.sync(milliDelay: 1000);
}
static void deleteAll() {
// Remove all SSH sessions before clearing servers
for (final id in servers.keys) {
final sessionId = 'ssh_$id';
TermSessionManager.remove(sessionId);
}
servers.clear();
serverOrder.value.clear();
serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value);
Stores.server.clear();
_updateTags();
bakSync.sync(milliDelay: 1000);
}
static Future<void> updateServer(Spi old, Spi newSpi) async {
if (old != newSpi) {
Stores.server.update(old, newSpi);
servers[old.id]?.value.spi = newSpi;
if (newSpi.id != old.id) {
servers[newSpi.id] = servers[old.id]!;
servers.remove(old.id);
serverOrder.value.update(old.id, newSpi.id);
Stores.setting.serverOrder.put(serverOrder.value);
serverOrder.notify();
// Update SSH session ID when server ID changes
final oldSessionId = 'ssh_${old.id}';
TermSessionManager.remove(oldSessionId);
// Session will be re-added when reconnecting if necessary
}
// Only reconnect if neccessary
if (newSpi.shouldReconnect(old)) {
// Use [newSpi.id] instead of [old.id] because [old.id] may be changed
TryLimiter.reset(newSpi.id);
refresh(spi: newSpi);
}
}
_updateTags();
bakSync.sync(milliDelay: 1000);
}
static void _setServerState(VNode<Server> s, ServerConn ss) {
s.value.conn = ss;
s.notify();
}
static Future<void> _getData(Spi spi) async {
final sid = spi.id;
final s = servers[sid];
if (s == null) return;
final sv = s.value;
if (!TryLimiter.canTry(sid)) {
if (sv.conn != ServerConn.failed) {
_setServerState(s, ServerConn.failed);
}
return;
}
sv.status.err = null;
if (sv.needGenClient || (sv.client?.isClosed ?? true)) {
_setServerState(s, ServerConn.connecting);
final wol = spi.wolCfg;
if (wol != null) {
try {
await wol.wake();
} catch (e) {
// TryLimiter.inc(sid);
// s.status.err = SSHErr(
// type: SSHErrType.connect,
// message: 'Wake on lan failed: $e',
// );
// _setServerState(s, ServerConn.failed);
Loggers.app.warning('Wake on lan failed', e);
// return;
}
}
try {
final time1 = DateTime.now();
sv.client = await genClient(
spi,
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
);
final time2 = DateTime.now();
final spentTime = time2.difference(time1).inMilliseconds;
if (spi.jumpId == null) {
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
} else {
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
}
// Add SSH session to TermSessionManager
final sessionId = 'ssh_${spi.id}';
TermSessionManager.add(
id: sessionId,
spi: spi,
startTimeMs: time1.millisecondsSinceEpoch,
disconnect: () => _closeOneServer(spi.id),
status: TermSessionStatus.connecting,
);
TermSessionManager.setActive(sessionId, hasTerminal: false);
} catch (e) {
TryLimiter.inc(sid);
sv.status.err = SSHErr(type: SSHErrType.connect, message: e.toString());
_setServerState(s, ServerConn.failed);
// Remove SSH session on connection failure
final sessionId = 'ssh_${spi.id}';
TermSessionManager.remove(sessionId);
/// In order to keep privacy, print [spi.name] instead of [spi.id]
Loggers.app.warning('Connect to ${spi.name} failed', e);
return;
}
_setServerState(s, ServerConn.connected);
// Update SSH session status to connected
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);
try {
// Detect system type using helper
final detectedSystemType = await SystemDetector.detect(sv.client!, spi);
sv.status.system = detectedSystemType;
final (_, writeScriptResult) = await sv.client!.exec((session) async {
final scriptRaw = ShellFuncManager.allScript(
spi.custom?.cmds,
systemType: detectedSystemType,
disabledCmdTypes: spi.disabledCmdTypes,
).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
throw writeScriptResult;
}
} on SSHAuthAbortError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
sv.status.err = err;
Loggers.app.warning(err);
_setServerState(s, ServerConn.failed);
// Update SSH session status to disconnected
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
} on SSHAuthFailError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
sv.status.err = err;
Loggers.app.warning(err);
_setServerState(s, ServerConn.failed);
// Update SSH session status to disconnected
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
} catch (e) {
// If max try times < 2 and can't write script, this will stop the status getting and etc.
// TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
sv.status.err = err;
Loggers.app.warning(err);
_setServerState(s, ServerConn.failed);
// Update SSH session status to disconnected
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
}
}
if (sv.conn == ServerConn.connecting) return;
/// Keep [finished] state, or the UI will be refreshed to [loading] state
/// instead of the '$Temp | $Uptime'.
/// eg: '32C | 7 days'
if (sv.conn != ServerConn.finished) {
_setServerState(s, ServerConn.loading);
}
List<String>? segments;
String? raw;
try {
raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
//dprint('Get status from ${spi.name}:\n$raw');
segments = raw?.split(ScriptConstants.separator).map((e) => e.trim()).toList();
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) {
// Keep previous server status when err occurs
if (sv.conn != ServerConn.failed && sv.status.more.isNotEmpty) {
return;
}
}
TryLimiter.inc(sid);
sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
_setServerState(s, ServerConn.failed);
// Update SSH session status to disconnected on segments error
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
} catch (e) {
TryLimiter.inc(sid);
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
_setServerState(s, ServerConn.failed);
Loggers.app.warning('Get status from ${spi.name} failed', e);
// Update SSH session status to disconnected on status error
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
try {
// Parse script output into command-specific map
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
final req = ServerStatusUpdateReq(
ss: sv.status,
parsedOutput: parsedOutput,
system: sv.status.system,
customCmds: spi.custom?.cmds ?? {},
);
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');
} catch (e, trace) {
TryLimiter.inc(sid);
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
_setServerState(s, ServerConn.failed);
Loggers.app.warning('Server status', e, trace);
// Update SSH session status to disconnected on parse error
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
/// Call this every time for setting [Server.isBusy] to false
_setServerState(s, ServerConn.finished);
// reset try times only after prepared successfully
TryLimiter.reset(sid);
}
}

View File

@@ -0,0 +1,322 @@
import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/provider/server/single.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/ssh/session_manager.dart';
part 'all.freezed.dart';
part 'all.g.dart';
@freezed
abstract class ServersState with _$ServersState {
const factory ServersState({
@Default({}) Map<String, Spi> servers,
@Default([]) List<String> serverOrder,
@Default(<String>{}) Set<String> tags,
@Default(<String>{}) Set<String> manualDisconnectedIds,
Timer? autoRefreshTimer,
}) = _ServersState;
}
@Riverpod(keepAlive: true)
class ServersNotifier extends _$ServersNotifier {
@override
ServersState build() {
return _load();
}
Future<void> reload() async {
final newState = _load();
if (newState == state) return;
state = newState;
await refresh();
}
ServersState _load() {
final spis = Stores.server.fetch();
final newServers = <String, Spi>{};
final newServerOrder = <String>[];
for (final spi in spis) {
newServers[spi.id] = spi;
}
final serverOrder_ = Stores.setting.serverOrder.fetch();
if (serverOrder_.isNotEmpty) {
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
newServerOrder.addAll(spis.map((e) => e.id));
} else {
newServerOrder.addAll(newServers.keys);
}
// Must use [equals] to compare [Order] here.
if (!newServerOrder.equals(serverOrder_)) {
Stores.setting.serverOrder.put(newServerOrder);
}
final newTags = _calculateTags(newServers);
return stateOrNull?.copyWith(servers: newServers, serverOrder: newServerOrder, tags: newTags) ??
ServersState(servers: newServers, serverOrder: newServerOrder, tags: newTags);
}
Set<String> _calculateTags(Map<String, Spi> servers) {
final tags = <String>{};
for (final spi in servers.values) {
final spiTags = spi.tags;
if (spiTags == null) continue;
for (final t in spiTags) {
tags.add(t);
}
}
return tags;
}
/// Get a [Spi] by [spi] or [id].
///
/// Priority: [spi] > [id]
Spi? pick({Spi? spi, String? id}) {
if (spi != null) {
return state.servers[spi.id];
}
if (id != null) {
return state.servers[id];
}
return null;
}
/// if [spi] is specificed then only refresh this server
/// [onlyFailed] only refresh failed servers
Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
if (spi != null) {
final newManualDisconnected = Set<String>.from(state.manualDisconnectedIds)..remove(spi.id);
state = state.copyWith(manualDisconnectedIds: newManualDisconnected);
final serverNotifier = ref.read(serverProvider(spi.id).notifier);
await serverNotifier.refresh();
return;
}
await Future.wait(
state.servers.entries.map((entry) async {
final serverId = entry.key;
final spi = entry.value;
if (onlyFailed) {
final serverState = ref.read(serverProvider(serverId));
if (serverState.conn != ServerConn.failed) return;
TryLimiter.reset(serverId);
}
if (state.manualDisconnectedIds.contains(serverId)) return;
final serverState = ref.read(serverProvider(serverId));
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) {
return;
}
final serverNotifier = ref.read(serverProvider(serverId).notifier);
await serverNotifier.refresh();
}),
);
}
Future<void> startAutoRefresh() async {
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
stopAutoRefresh();
if (duration == 0) return;
if (duration < 0 || duration > 10 || duration == 1) {
duration = 3;
Loggers.app.warning('Invalid duration: $duration, use default 3');
}
final timer = Timer.periodic(Duration(seconds: duration), (_) async {
await refresh();
});
state = state.copyWith(autoRefreshTimer: timer);
}
void stopAutoRefresh() {
final timer = state.autoRefreshTimer;
if (timer != null) {
timer.cancel();
state = state.copyWith(autoRefreshTimer: null);
}
}
bool get isAutoRefreshOn => state.autoRefreshTimer != null;
void setDisconnected() {
for (final serverId in state.servers.keys) {
final serverNotifier = ref.read(serverProvider(serverId).notifier);
serverNotifier.updateConnection(ServerConn.disconnected);
// Update SSH session status to disconnected
final sessionId = 'ssh_$serverId';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
}
//TryLimiter.clear();
}
void closeServer({String? id}) {
if (id == null) {
for (final serverId in state.servers.keys) {
closeOneServer(serverId);
}
return;
}
closeOneServer(id);
}
void closeOneServer(String id) {
final spi = state.servers[id];
if (spi == null) {
Loggers.app.warning('Server with id $id not found');
return;
}
final serverNotifier = ref.read(serverProvider(id).notifier);
serverNotifier.closeConnection();
final newManualDisconnected = Set<String>.from(state.manualDisconnectedIds)..add(id);
state = state.copyWith(manualDisconnectedIds: newManualDisconnected);
// Remove SSH session when server is manually closed
final sessionId = 'ssh_$id';
TermSessionManager.remove(sessionId);
}
void addServer(Spi spi) {
final newServers = Map<String, Spi>.from(state.servers);
newServers[spi.id] = spi;
final newOrder = List<String>.from(state.serverOrder)..add(spi.id);
final newTags = _calculateTags(newServers);
state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);
Stores.server.put(spi);
Stores.setting.serverOrder.put(newOrder);
refresh(spi: spi);
bakSync.sync(milliDelay: 1000);
}
void delServer(String id) {
final newServers = Map<String, Spi>.from(state.servers);
newServers.remove(id);
final newOrder = List<String>.from(state.serverOrder)..remove(id);
final newTags = _calculateTags(newServers);
state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);
Stores.setting.serverOrder.put(newOrder);
Stores.server.delete(id);
// Remove SSH session when server is deleted
final sessionId = 'ssh_$id';
TermSessionManager.remove(sessionId);
bakSync.sync(milliDelay: 1000);
}
void deleteAll() {
// Remove all SSH sessions before clearing servers
for (final id in state.servers.keys) {
final sessionId = 'ssh_$id';
TermSessionManager.remove(sessionId);
}
state = const ServersState();
Stores.setting.serverOrder.put([]);
Stores.server.clear();
bakSync.sync(milliDelay: 1000);
}
void updateServerOrder(List<String> order) {
final seen = <String>{};
final newOrder = <String>[];
for (final id in order) {
if (!state.servers.containsKey(id)) {
continue;
}
if (!seen.add(id)) {
continue;
}
newOrder.add(id);
}
for (final id in state.servers.keys) {
if (seen.add(id)) {
newOrder.add(id);
}
}
if (_isSameOrder(newOrder, state.serverOrder)) {
return;
}
state = state.copyWith(serverOrder: newOrder);
Stores.setting.serverOrder.put(newOrder);
bakSync.sync(milliDelay: 1000);
}
bool _isSameOrder(List<String> a, List<String> b) {
if (identical(a, b)) {
return true;
}
if (a.length != b.length) {
return false;
}
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
Future<void> updateServer(Spi old, Spi newSpi) async {
if (old != newSpi) {
Stores.server.update(old, newSpi);
final newServers = Map<String, Spi>.from(state.servers);
final newOrder = List<String>.from(state.serverOrder);
if (newSpi.id != old.id) {
newServers[newSpi.id] = newSpi;
newServers.remove(old.id);
newOrder.update(old.id, newSpi.id);
Stores.setting.serverOrder.put(newOrder);
// Update SSH session ID when server ID changes
final oldSessionId = 'ssh_${old.id}';
TermSessionManager.remove(oldSessionId);
// Session will be re-added when reconnecting if necessary
} else {
newServers[old.id] = newSpi;
// Update SPI in the corresponding IndividualServerNotifier
final serverNotifier = ref.read(serverProvider(old.id).notifier);
serverNotifier.updateSpi(newSpi);
}
final newTags = _calculateTags(newServers);
state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);
// Only reconnect if neccessary
if (newSpi.shouldReconnect(old)) {
// Use [newSpi.id] instead of [old.id] because [old.id] may be changed
TryLimiter.reset(newSpi.id);
refresh(spi: newSpi);
}
}
bakSync.sync(milliDelay: 1000);
}
}

View File

@@ -0,0 +1,307 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'all.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ServersState {
Map<String, Spi> get servers; List<String> get serverOrder; Set<String> get tags; Set<String> get manualDisconnectedIds; Timer? get autoRefreshTimer;
/// Create a copy of ServersState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServersStateCopyWith<ServersState> get copyWith => _$ServersStateCopyWithImpl<ServersState>(this as ServersState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServersState&&const DeepCollectionEquality().equals(other.servers, servers)&&const DeepCollectionEquality().equals(other.serverOrder, serverOrder)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.manualDisconnectedIds, manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(servers),const DeepCollectionEquality().hash(serverOrder),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(manualDisconnectedIds),autoRefreshTimer);
@override
String toString() {
return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)';
}
}
/// @nodoc
abstract mixin class $ServersStateCopyWith<$Res> {
factory $ServersStateCopyWith(ServersState value, $Res Function(ServersState) _then) = _$ServersStateCopyWithImpl;
@useResult
$Res call({
Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer
});
}
/// @nodoc
class _$ServersStateCopyWithImpl<$Res>
implements $ServersStateCopyWith<$Res> {
_$ServersStateCopyWithImpl(this._self, this._then);
final ServersState _self;
final $Res Function(ServersState) _then;
/// Create a copy of ServersState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) {
return _then(_self.copyWith(
servers: null == servers ? _self.servers : servers // ignore: cast_nullable_to_non_nullable
as Map<String, Spi>,serverOrder: null == serverOrder ? _self.serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable
as List<String>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self.manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable
as Set<String>,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable
as Timer?,
));
}
}
/// Adds pattern-matching-related methods to [ServersState].
extension ServersStatePatterns on ServersState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServersState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ServersState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServersState value) $default,){
final _that = this;
switch (_that) {
case _ServersState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServersState value)? $default,){
final _that = this;
switch (_that) {
case _ServersState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ServersState() when $default != null:
return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer) $default,) {final _that = this;
switch (_that) {
case _ServersState():
return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer)? $default,) {final _that = this;
switch (_that) {
case _ServersState() when $default != null:
return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:
return null;
}
}
}
/// @nodoc
class _ServersState implements ServersState {
const _ServersState({final Map<String, Spi> servers = const {}, final List<String> serverOrder = const [], final Set<String> tags = const <String>{}, final Set<String> manualDisconnectedIds = const <String>{}, this.autoRefreshTimer}): _servers = servers,_serverOrder = serverOrder,_tags = tags,_manualDisconnectedIds = manualDisconnectedIds;
final Map<String, Spi> _servers;
@override@JsonKey() Map<String, Spi> get servers {
if (_servers is EqualUnmodifiableMapView) return _servers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_servers);
}
final List<String> _serverOrder;
@override@JsonKey() List<String> get serverOrder {
if (_serverOrder is EqualUnmodifiableListView) return _serverOrder;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_serverOrder);
}
final Set<String> _tags;
@override@JsonKey() Set<String> get tags {
if (_tags is EqualUnmodifiableSetView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_tags);
}
final Set<String> _manualDisconnectedIds;
@override@JsonKey() Set<String> get manualDisconnectedIds {
if (_manualDisconnectedIds is EqualUnmodifiableSetView) return _manualDisconnectedIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_manualDisconnectedIds);
}
@override final Timer? autoRefreshTimer;
/// Create a copy of ServersState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServersStateCopyWith<_ServersState> get copyWith => __$ServersStateCopyWithImpl<_ServersState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServersState&&const DeepCollectionEquality().equals(other._servers, _servers)&&const DeepCollectionEquality().equals(other._serverOrder, _serverOrder)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._manualDisconnectedIds, _manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_servers),const DeepCollectionEquality().hash(_serverOrder),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_manualDisconnectedIds),autoRefreshTimer);
@override
String toString() {
return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)';
}
}
/// @nodoc
abstract mixin class _$ServersStateCopyWith<$Res> implements $ServersStateCopyWith<$Res> {
factory _$ServersStateCopyWith(_ServersState value, $Res Function(_ServersState) _then) = __$ServersStateCopyWithImpl;
@override @useResult
$Res call({
Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer
});
}
/// @nodoc
class __$ServersStateCopyWithImpl<$Res>
implements _$ServersStateCopyWith<$Res> {
__$ServersStateCopyWithImpl(this._self, this._then);
final _ServersState _self;
final $Res Function(_ServersState) _then;
/// Create a copy of ServersState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) {
return _then(_ServersState(
servers: null == servers ? _self._servers : servers // ignore: cast_nullable_to_non_nullable
as Map<String, Spi>,serverOrder: null == serverOrder ? _self._serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable
as List<String>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self._manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable
as Set<String>,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable
as Timer?,
));
}
}
// dart format on

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'all.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ServersNotifier)
const serversProvider = ServersNotifierProvider._();
final class ServersNotifierProvider
extends $NotifierProvider<ServersNotifier, ServersState> {
const ServersNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'serversProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$serversNotifierHash();
@$internal
@override
ServersNotifier create() => ServersNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ServersState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ServersState>(value),
);
}
}
String _$serversNotifierHash() => r'3292bdce7d602ff64687b05ff81d120e71761ec2';
abstract class _$ServersNotifier extends $Notifier<ServersState> {
ServersState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<ServersState, ServersState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ServersState, ServersState>,
ServersState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -0,0 +1,394 @@
import 'dart:async';
import 'dart:convert';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/helper/system_detector.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/provider/server/all.dart';
import 'package:server_box/data/res/status.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/ssh/session_manager.dart';
part 'single.g.dart';
part 'single.freezed.dart';
// Individual server state, including connection and status information
@freezed
abstract class ServerState with _$ServerState {
const factory ServerState({
required Spi spi,
required ServerStatus status,
@Default(ServerConn.disconnected) ServerConn conn,
SSHClient? client,
Future<void>? updateFuture,
}) = _ServerState;
}
// Individual server state management
@Riverpod(keepAlive: true)
class ServerNotifier extends _$ServerNotifier {
@override
ServerState build(String serverId) {
final serverNotifier = ref.read(serversProvider);
final spi = serverNotifier.servers[serverId];
if (spi == null) {
throw StateError('Server $serverId not found');
}
return ServerState(spi: spi, status: InitStatus.status);
}
// Update connection status
void updateConnection(ServerConn conn) {
state = state.copyWith(conn: conn);
}
// Update server status
void updateStatus(ServerStatus status) {
state = state.copyWith(status: status);
}
// Update SSH client
void updateClient(SSHClient? client) {
state = state.copyWith(client: client);
}
// Update SPI configuration
void updateSpi(Spi spi) {
state = state.copyWith(spi: spi);
}
// Close connection
void closeConnection() {
final client = state.client;
client?.close();
state = state.copyWith(client: null, conn: ServerConn.disconnected);
}
// Refresh server status
Future<void> refresh() async {
if (state.updateFuture != null) {
await state.updateFuture;
return;
}
final updateFuture = _updateServer();
state = state.copyWith(updateFuture: updateFuture);
try {
await updateFuture;
} finally {
state = state.copyWith(updateFuture: null);
}
}
Future<void> _updateServer() async {
await _getData();
}
Future<void> _getData() async {
final spi = state.spi;
final sid = spi.id;
if (!TryLimiter.canTry(sid)) {
if (state.conn != ServerConn.failed) {
updateConnection(ServerConn.failed);
}
return;
}
final newStatus = state.status..err = null; // Clear previous error
updateStatus(newStatus);
if (state.conn < ServerConn.connecting || (state.client?.isClosed ?? true)) {
updateConnection(ServerConn.connecting);
// Wake on LAN
final wol = spi.wolCfg;
if (wol != null) {
try {
await wol.wake();
} catch (e) {
Loggers.app.warning('Wake on lan failed', e);
}
}
try {
final time1 = DateTime.now();
final client = await genClient(
spi,
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
);
updateClient(client);
final time2 = DateTime.now();
final spentTime = time2.difference(time1).inMilliseconds;
if (spi.jumpId == null) {
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
} else {
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
}
// Record successful connection
Stores.connectionStats.recordConnection(ConnectionStat(
serverId: spi.id,
serverName: spi.name,
timestamp: time1,
result: ConnectionResult.success,
durationMs: spentTime,
));
final sessionId = 'ssh_${spi.id}';
TermSessionManager.add(
id: sessionId,
spi: spi,
startTimeMs: time1.millisecondsSinceEpoch,
disconnect: () => ref.read(serversProvider.notifier).closeOneServer(spi.id),
status: TermSessionStatus.connecting,
);
TermSessionManager.setActive(sessionId, hasTerminal: false);
} catch (e) {
TryLimiter.inc(sid);
// Determine connection failure type
ConnectionResult failureResult;
if (e.toString().contains('timeout') || e.toString().contains('Timeout')) {
failureResult = ConnectionResult.timeout;
} else if (e.toString().contains('auth') || e.toString().contains('Authentication')) {
failureResult = ConnectionResult.authFailed;
} else if (e.toString().contains('network') || e.toString().contains('Network')) {
failureResult = ConnectionResult.networkError;
} else {
failureResult = ConnectionResult.unknownError;
}
// Record failed connection
Stores.connectionStats.recordConnection(ConnectionStat(
serverId: spi.id,
serverName: spi.name,
timestamp: DateTime.now(),
result: failureResult,
errorMessage: e.toString(),
durationMs: 0,
));
final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
updateStatus(newStatus);
updateConnection(ServerConn.failed);
// Remove SSH session when connection fails
final sessionId = 'ssh_${spi.id}';
TermSessionManager.remove(sessionId);
Loggers.app.warning('Connect to ${spi.name} failed', e);
return;
}
updateConnection(ServerConn.connected);
// Update SSH session status to connected
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);
try {
// Detect system type
final detectedSystemType = await SystemDetector.detect(state.client!, spi);
final newStatus = state.status..system = detectedSystemType;
updateStatus(newStatus);
final (_, writeScriptResult) = await state.client!.exec(
(session) async {
final scriptRaw = ShellFuncManager.allScript(
spi.custom?.cmds,
systemType: detectedSystemType,
disabledCmdTypes: spi.disabledCmdTypes,
).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
},
entry: ShellFuncManager.getInstallShellCmd(
spi.id,
systemType: detectedSystemType,
customDir: spi.custom?.scriptDir,
),
);
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
throw writeScriptResult;
}
} on SSHAuthAbortError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
final newStatus = state.status..err = err;
updateStatus(newStatus);
Loggers.app.warning(err);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
} on SSHAuthFailError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
final newStatus = state.status..err = err;
updateStatus(newStatus);
Loggers.app.warning(err);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
} catch (e) {
final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
final newStatus = state.status..err = err;
updateStatus(newStatus);
Loggers.app.warning(err);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
}
}
if (state.conn == ServerConn.connecting) return;
// Keep finished status to prevent UI from refreshing to loading state
if (state.conn != ServerConn.finished) {
updateConnection(ServerConn.loading);
}
List<String>? segments;
String? raw;
try {
final execResult = await state.client?.run(
ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir),
);
if (execResult != null) {
String? rawStr;
bool needGbk = false;
try {
rawStr = utf8.decode(execResult, allowMalformed: true);
// If there are unparseable characters, try fallback to GBK decoding
if (rawStr.contains('<EFBFBD>')) {
Loggers.app.warning('UTF8 decoding failed, use GBK decoding');
needGbk = true;
}
} catch (e) {
Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e);
needGbk = true;
}
if (needGbk) {
try {
rawStr = gbk.decode(execResult);
} catch (e2) {
Loggers.app.warning('GBK decoding failed', e2);
rawStr = null;
}
}
if (rawStr == null) {
Loggers.app.warning('Decoding failed, execResult: $execResult');
}
raw = rawStr;
} else {
raw = execResult.toString();
}
if (raw == null || raw.isEmpty) {
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.segements, message: 'decode or split failed, raw:\n$raw');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
if (raw.isEmpty || segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) {
// Keep previous server status when error occurs
if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
return;
}
}
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
} catch (e) {
TryLimiter.inc(sid);
final newStatus = state.status..err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
updateStatus(newStatus);
updateConnection(ServerConn.failed);
Loggers.app.warning('Get status from ${spi.name} failed', e);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
try {
// Parse script output into command-specific mappings
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
final req = ServerStatusUpdateReq(
ss: state.status,
parsedOutput: parsedOutput,
system: state.status.system,
customCmds: spi.custom?.cmds ?? {},
);
final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>');
updateStatus(newStatus);
} catch (e, trace) {
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
Loggers.app.warning('Server status', e, trace);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
// Set Server.isBusy to false each time this method is called
updateConnection(ServerConn.finished);
// Reset retry count only after successful preparation
TryLimiter.reset(sid);
}
}
extension IndividualServerStateExtension on ServerState {
bool get needGenClient => conn < ServerConn.connecting;
bool get canViewDetails => conn == ServerConn.finished;
String get id => spi.id;
}

View File

@@ -0,0 +1,301 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'single.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ServerState {
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client; Future<void>? get updateFuture;
/// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServerStateCopyWith<ServerState> get copyWith => _$ServerStateCopyWithImpl<ServerState>(this as ServerState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
}
@override
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
@override
String toString() {
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
}
}
/// @nodoc
abstract mixin class $ServerStateCopyWith<$Res> {
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
@useResult
$Res call({
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
});
$SpiCopyWith<$Res> get spi;
}
/// @nodoc
class _$ServerStateCopyWithImpl<$Res>
implements $ServerStateCopyWith<$Res> {
_$ServerStateCopyWithImpl(this._self, this._then);
final ServerState _self;
final $Res Function(ServerState) _then;
/// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
return _then(_self.copyWith(
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
as Future<void>?,
));
}
/// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SpiCopyWith<$Res> get spi {
return $SpiCopyWith<$Res>(_self.spi, (value) {
return _then(_self.copyWith(spi: value));
});
}
}
/// Adds pattern-matching-related methods to [ServerState].
extension ServerStatePatterns on ServerState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ServerState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerState value) $default,){
final _that = this;
switch (_that) {
case _ServerState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerState value)? $default,){
final _that = this;
switch (_that) {
case _ServerState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ServerState() when $default != null:
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture) $default,) {final _that = this;
switch (_that) {
case _ServerState():
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,) {final _that = this;
switch (_that) {
case _ServerState() when $default != null:
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
return null;
}
}
}
/// @nodoc
class _ServerState implements ServerState {
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client, this.updateFuture});
@override final Spi spi;
@override final ServerStatus status;
@override@JsonKey() final ServerConn conn;
@override final SSHClient? client;
@override final Future<void>? updateFuture;
/// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_ServerState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
}
@override
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
@override
String toString() {
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
}
}
/// @nodoc
abstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith<$Res> {
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
@override @useResult
$Res call({
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
});
@override $SpiCopyWith<$Res> get spi;
}
/// @nodoc
class __$ServerStateCopyWithImpl<$Res>
implements _$ServerStateCopyWith<$Res> {
__$ServerStateCopyWithImpl(this._self, this._then);
final _ServerState _self;
final $Res Function(_ServerState) _then;
/// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
return _then(_ServerState(
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
as Future<void>?,
));
}
/// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SpiCopyWith<$Res> get spi {
return $SpiCopyWith<$Res>(_self.spi, (value) {
return _then(_self.copyWith(spi: value));
});
}
}
// dart format on

View File

@@ -0,0 +1,108 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'single.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ServerNotifier)
const serverProvider = ServerNotifierFamily._();
final class ServerNotifierProvider
extends $NotifierProvider<ServerNotifier, ServerState> {
const ServerNotifierProvider._({
required ServerNotifierFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'serverProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$serverNotifierHash();
@override
String toString() {
return r'serverProvider'
''
'($argument)';
}
@$internal
@override
ServerNotifier create() => ServerNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ServerState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ServerState>(value),
);
}
@override
bool operator ==(Object other) {
return other is ServerNotifierProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$serverNotifierHash() => r'185c6b4546c3bc526f5b2ca79d16aed665818863';
final class ServerNotifierFamily extends $Family
with
$ClassFamilyOverride<
ServerNotifier,
ServerState,
ServerState,
ServerState,
String
> {
const ServerNotifierFamily._()
: super(
retry: null,
name: r'serverProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
ServerNotifierProvider call(String serverId) =>
ServerNotifierProvider._(argument: serverId, from: this);
@override
String toString() => r'serverProvider';
}
abstract class _$ServerNotifier extends $Notifier<ServerState> {
late final _$args = ref.$arg as String;
String get serverId => _$args;
ServerState build(String serverId);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref = this.ref as $Ref<ServerState, ServerState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ServerState, ServerState>,
ServerState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,41 +1,69 @@
import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/data/model/sftp/worker.dart';
class SftpProvider extends Provider {
const SftpProvider._();
static const instance = SftpProvider._();
part 'sftp.freezed.dart';
part 'sftp.g.dart';
static final status = <SftpReqStatus>[].vn;
@freezed
abstract class SftpState with _$SftpState {
const factory SftpState({
@Default(<SftpReqStatus>[]) List<SftpReqStatus> requests,
}) = _SftpState;
}
static SftpReqStatus? get(int id) {
return status.value.singleWhere((element) => element.id == id);
@Riverpod(keepAlive: true)
class SftpNotifier extends _$SftpNotifier {
@override
SftpState build() {
return const SftpState();
}
static int add(SftpReq req, {Completer? completer}) {
final reqStat = SftpReqStatus(notifyListeners: status.notify, completer: completer, req: req);
status.value.add(reqStat);
status.notify();
SftpReqStatus? get(int id) {
try {
return state.requests.singleWhere((element) => element.id == id);
} catch (e) {
return null;
}
}
int add(SftpReq req, {Completer? completer}) {
final reqStat = SftpReqStatus(
notifyListeners: _notifyListeners,
completer: completer,
req: req,
);
state = state.copyWith(
requests: [...state.requests, reqStat],
);
return reqStat.id;
}
static void dispose() {
for (final item in status.value) {
void dispose() {
for (final item in state.requests) {
item.dispose();
}
status.value.clear();
status.notify();
state = state.copyWith(requests: []);
}
static void cancel(int id) {
final idx = status.value.indexWhere((e) => e.id == id);
if (idx < 0 || idx >= status.value.length) {
void cancel(int id) {
final idx = state.requests.indexWhere((e) => e.id == id);
if (idx < 0 || idx >= state.requests.length) {
dprint('SftpProvider.cancel: id $id not found');
return;
}
status.value[idx].dispose();
status.value.removeAt(idx);
status.notify();
final item = state.requests[idx];
item.dispose();
final newRequests = List<SftpReqStatus>.from(state.requests)
..removeAt(idx);
state = state.copyWith(requests: newRequests);
}
void _notifyListeners() {
// Force state update to notify listeners
state = state.copyWith();
}
}

View File

@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'sftp.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SftpState {
List<SftpReqStatus> get requests;
/// Create a copy of SftpState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SftpStateCopyWith<SftpState> get copyWith => _$SftpStateCopyWithImpl<SftpState>(this as SftpState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SftpState&&const DeepCollectionEquality().equals(other.requests, requests));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(requests));
@override
String toString() {
return 'SftpState(requests: $requests)';
}
}
/// @nodoc
abstract mixin class $SftpStateCopyWith<$Res> {
factory $SftpStateCopyWith(SftpState value, $Res Function(SftpState) _then) = _$SftpStateCopyWithImpl;
@useResult
$Res call({
List<SftpReqStatus> requests
});
}
/// @nodoc
class _$SftpStateCopyWithImpl<$Res>
implements $SftpStateCopyWith<$Res> {
_$SftpStateCopyWithImpl(this._self, this._then);
final SftpState _self;
final $Res Function(SftpState) _then;
/// Create a copy of SftpState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? requests = null,}) {
return _then(_self.copyWith(
requests: null == requests ? _self.requests : requests // ignore: cast_nullable_to_non_nullable
as List<SftpReqStatus>,
));
}
}
/// Adds pattern-matching-related methods to [SftpState].
extension SftpStatePatterns on SftpState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SftpState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SftpState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SftpState value) $default,){
final _that = this;
switch (_that) {
case _SftpState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SftpState value)? $default,){
final _that = this;
switch (_that) {
case _SftpState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SftpReqStatus> requests)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SftpState() when $default != null:
return $default(_that.requests);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SftpReqStatus> requests) $default,) {final _that = this;
switch (_that) {
case _SftpState():
return $default(_that.requests);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SftpReqStatus> requests)? $default,) {final _that = this;
switch (_that) {
case _SftpState() when $default != null:
return $default(_that.requests);case _:
return null;
}
}
}
/// @nodoc
class _SftpState implements SftpState {
const _SftpState({final List<SftpReqStatus> requests = const <SftpReqStatus>[]}): _requests = requests;
final List<SftpReqStatus> _requests;
@override@JsonKey() List<SftpReqStatus> get requests {
if (_requests is EqualUnmodifiableListView) return _requests;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_requests);
}
/// Create a copy of SftpState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SftpStateCopyWith<_SftpState> get copyWith => __$SftpStateCopyWithImpl<_SftpState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SftpState&&const DeepCollectionEquality().equals(other._requests, _requests));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_requests));
@override
String toString() {
return 'SftpState(requests: $requests)';
}
}
/// @nodoc
abstract mixin class _$SftpStateCopyWith<$Res> implements $SftpStateCopyWith<$Res> {
factory _$SftpStateCopyWith(_SftpState value, $Res Function(_SftpState) _then) = __$SftpStateCopyWithImpl;
@override @useResult
$Res call({
List<SftpReqStatus> requests
});
}
/// @nodoc
class __$SftpStateCopyWithImpl<$Res>
implements _$SftpStateCopyWith<$Res> {
__$SftpStateCopyWithImpl(this._self, this._then);
final _SftpState _self;
final $Res Function(_SftpState) _then;
/// Create a copy of SftpState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? requests = null,}) {
return _then(_SftpState(
requests: null == requests ? _self._requests : requests // ignore: cast_nullable_to_non_nullable
as List<SftpReqStatus>,
));
}
}
// dart format on

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sftp.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(SftpNotifier)
const sftpProvider = SftpNotifierProvider._();
final class SftpNotifierProvider
extends $NotifierProvider<SftpNotifier, SftpState> {
const SftpNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sftpProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sftpNotifierHash();
@$internal
@override
SftpNotifier create() => SftpNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SftpState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SftpState>(value),
);
}
}
String _$sftpNotifierHash() => r'f8412a4bd1f2bc5919ec31a3eba1c27e9a578f41';
abstract class _$SftpNotifier extends $Notifier<SftpState> {
SftpState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<SftpState, SftpState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<SftpState, SftpState>,
SftpState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,80 +1,105 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/sync.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/store.dart';
class SnippetProvider extends Provider {
const SnippetProvider._();
static const instance = SnippetProvider._();
part 'snippet.freezed.dart';
part 'snippet.g.dart';
static final snippets = <Snippet>[].vn;
static final tags = <String>{}.vn;
@freezed
abstract class SnippetState with _$SnippetState {
const factory SnippetState({
@Default(<Snippet>[]) List<Snippet> snippets,
@Default(<String>{}) Set<String> tags,
}) = _SnippetState;
}
@Riverpod(keepAlive: true)
class SnippetNotifier extends _$SnippetNotifier {
@override
void load() {
super.load();
final snippets_ = Stores.snippet.fetch();
SnippetState build() {
return _load();
}
void reload() {
final newState = _load();
if (newState == state) return;
state = newState;
}
SnippetState _load() {
final snippets = Stores.snippet.fetch();
final order = Stores.setting.snippetOrder.fetch();
List<Snippet> orderedSnippets = snippets;
if (order.isNotEmpty) {
final surplus = snippets_.reorder(
order: order,
finder: (n, name) => n.name == name,
);
final surplus = snippets.reorder(order: order, finder: (n, name) => n.name == name);
order.removeWhere((e) => surplus.any((ele) => ele == e));
if (order != Stores.setting.snippetOrder.fetch()) {
Stores.setting.snippetOrder.put(order);
}
orderedSnippets = snippets;
}
snippets.value = snippets_;
_updateTags();
final newTags = _computeTags(orderedSnippets);
return stateOrNull?.copyWith(snippets: orderedSnippets, tags: newTags) ??
SnippetState(snippets: orderedSnippets, tags: newTags);
}
static void _updateTags() {
final tags_ = <String>{};
for (final s in snippets.value) {
Set<String> _computeTags(List<Snippet> snippets) {
final tags = <String>{};
for (final s in snippets) {
final t = s.tags;
if (t != null) {
tags_.addAll(t);
tags.addAll(t);
}
}
tags.value = tags_;
return tags;
}
static void add(Snippet snippet) {
snippets.value.add(snippet);
snippets.notify();
void add(Snippet snippet) {
final newSnippets = [...state.snippets, snippet];
final newTags = _computeTags(newSnippets);
state = state.copyWith(snippets: newSnippets, tags: newTags);
Stores.snippet.put(snippet);
_updateTags();
bakSync.sync(milliDelay: 1000);
}
static void del(Snippet snippet) {
snippets.value.remove(snippet);
snippets.notify();
void del(Snippet snippet) {
final newSnippets = state.snippets.where((s) => s != snippet).toList();
final newTags = _computeTags(newSnippets);
state = state.copyWith(snippets: newSnippets, tags: newTags);
Stores.snippet.delete(snippet);
_updateTags();
bakSync.sync(milliDelay: 1000);
}
static void update(Snippet old, Snippet newOne) {
snippets.value.remove(old);
snippets.value.add(newOne);
snippets.notify();
void update(Snippet old, Snippet newOne) {
final newSnippets = state.snippets.map((s) => s == old ? newOne : s).toList();
final newTags = _computeTags(newSnippets);
state = state.copyWith(snippets: newSnippets, tags: newTags);
Stores.snippet.delete(old);
Stores.snippet.put(newOne);
_updateTags();
bakSync.sync(milliDelay: 1000);
}
static void renameTag(String old, String newOne) {
for (final s in snippets.value) {
void renameTag(String old, String newOne) {
final updatedSnippets = <Snippet>[];
for (final s in state.snippets) {
if (s.tags?.contains(old) ?? false) {
s.tags?.remove(old);
s.tags?.add(newOne);
Stores.snippet.put(s);
final newTags = Set<String>.from(s.tags!);
newTags.remove(old);
newTags.add(newOne);
final updatedSnippet = s.copyWith(tags: newTags.toList());
updatedSnippets.add(updatedSnippet);
Stores.snippet.put(updatedSnippet);
} else {
updatedSnippets.add(s);
}
}
_updateTags();
final newTags = _computeTags(updatedSnippets);
state = state.copyWith(snippets: updatedSnippets, tags: newTags);
bakSync.sync(milliDelay: 1000);
}
}

View File

@@ -0,0 +1,286 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'snippet.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnippetState {
List<Snippet> get snippets; Set<String> get tags;
/// Create a copy of SnippetState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnippetStateCopyWith<SnippetState> get copyWith => _$SnippetStateCopyWithImpl<SnippetState>(this as SnippetState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnippetState&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.tags, tags));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(tags));
@override
String toString() {
return 'SnippetState(snippets: $snippets, tags: $tags)';
}
}
/// @nodoc
abstract mixin class $SnippetStateCopyWith<$Res> {
factory $SnippetStateCopyWith(SnippetState value, $Res Function(SnippetState) _then) = _$SnippetStateCopyWithImpl;
@useResult
$Res call({
List<Snippet> snippets, Set<String> tags
});
}
/// @nodoc
class _$SnippetStateCopyWithImpl<$Res>
implements $SnippetStateCopyWith<$Res> {
_$SnippetStateCopyWithImpl(this._self, this._then);
final SnippetState _self;
final $Res Function(SnippetState) _then;
/// Create a copy of SnippetState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? snippets = null,Object? tags = null,}) {
return _then(_self.copyWith(
snippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable
as List<Snippet>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,
));
}
}
/// Adds pattern-matching-related methods to [SnippetState].
extension SnippetStatePatterns on SnippetState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnippetState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnippetState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnippetState value) $default,){
final _that = this;
switch (_that) {
case _SnippetState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnippetState value)? $default,){
final _that = this;
switch (_that) {
case _SnippetState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<Snippet> snippets, Set<String> tags)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnippetState() when $default != null:
return $default(_that.snippets,_that.tags);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<Snippet> snippets, Set<String> tags) $default,) {final _that = this;
switch (_that) {
case _SnippetState():
return $default(_that.snippets,_that.tags);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<Snippet> snippets, Set<String> tags)? $default,) {final _that = this;
switch (_that) {
case _SnippetState() when $default != null:
return $default(_that.snippets,_that.tags);case _:
return null;
}
}
}
/// @nodoc
class _SnippetState implements SnippetState {
const _SnippetState({final List<Snippet> snippets = const <Snippet>[], final Set<String> tags = const <String>{}}): _snippets = snippets,_tags = tags;
final List<Snippet> _snippets;
@override@JsonKey() List<Snippet> get snippets {
if (_snippets is EqualUnmodifiableListView) return _snippets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_snippets);
}
final Set<String> _tags;
@override@JsonKey() Set<String> get tags {
if (_tags is EqualUnmodifiableSetView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_tags);
}
/// Create a copy of SnippetState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnippetStateCopyWith<_SnippetState> get copyWith => __$SnippetStateCopyWithImpl<_SnippetState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnippetState&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._tags, _tags));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_tags));
@override
String toString() {
return 'SnippetState(snippets: $snippets, tags: $tags)';
}
}
/// @nodoc
abstract mixin class _$SnippetStateCopyWith<$Res> implements $SnippetStateCopyWith<$Res> {
factory _$SnippetStateCopyWith(_SnippetState value, $Res Function(_SnippetState) _then) = __$SnippetStateCopyWithImpl;
@override @useResult
$Res call({
List<Snippet> snippets, Set<String> tags
});
}
/// @nodoc
class __$SnippetStateCopyWithImpl<$Res>
implements _$SnippetStateCopyWith<$Res> {
__$SnippetStateCopyWithImpl(this._self, this._then);
final _SnippetState _self;
final $Res Function(_SnippetState) _then;
/// Create a copy of SnippetState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? snippets = null,Object? tags = null,}) {
return _then(_SnippetState(
snippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable
as List<Snippet>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,
));
}
}
// dart format on

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'snippet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(SnippetNotifier)
const snippetProvider = SnippetNotifierProvider._();
final class SnippetNotifierProvider
extends $NotifierProvider<SnippetNotifier, SnippetState> {
const SnippetNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'snippetProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$snippetNotifierHash();
@$internal
@override
SnippetNotifier create() => SnippetNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SnippetState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SnippetState>(value),
);
}
}
String _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9';
abstract class _$SnippetNotifier extends $Notifier<SnippetState> {
SnippetState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<SnippetState, SnippetState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<SnippetState, SnippetState>,
SnippetState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,45 +1,57 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/systemd.dart';
import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/provider/server/single.dart';
final class SystemdProvider {
late final VNode<Server> _si;
part 'systemd.freezed.dart';
part 'systemd.g.dart';
SystemdProvider.init(Spi spi) {
_si = ServerProvider.pick(spi: spi)!;
getUnits();
}
@freezed
abstract class SystemdState with _$SystemdState {
const factory SystemdState({
@Default(false) bool isBusy,
@Default(<SystemdUnit>[]) List<SystemdUnit> units,
@Default(SystemdScopeFilter.all) SystemdScopeFilter scopeFilter,
}) = _SystemdState;
}
final isBusy = false.vn;
final units = <SystemdUnit>[].vn;
final scopeFilter = SystemdScopeFilter.all.vn;
@riverpod
class SystemdNotifier extends _$SystemdNotifier {
late final ServerState _si;
void dispose() {
isBusy.dispose();
units.dispose();
scopeFilter.dispose();
@override
SystemdState build(Spi spi) {
final si = ref.read(serverProvider(spi.id));
_si = si;
// Async initialization
Future.microtask(() => getUnits());
return const SystemdState();
}
List<SystemdUnit> get filteredUnits {
switch (scopeFilter.value) {
switch (state.scopeFilter) {
case SystemdScopeFilter.all:
return units.value;
return state.units;
case SystemdScopeFilter.system:
return units.value.where((unit) => unit.scope == SystemdUnitScope.system).toList();
return state.units.where((unit) => unit.scope == SystemdUnitScope.system).toList();
case SystemdScopeFilter.user:
return units.value.where((unit) => unit.scope == SystemdUnitScope.user).toList();
return state.units.where((unit) => unit.scope == SystemdUnitScope.user).toList();
}
}
void setScopeFilter(SystemdScopeFilter filter) {
state = state.copyWith(scopeFilter: filter);
}
Future<void> getUnits() async {
isBusy.value = true;
state = state.copyWith(isBusy: true);
try {
final client = _si.value.client;
final client = _si.client;
final result = await client!.execForOutput(_getUnitsCmd);
final units = result.split('\n');
@@ -57,12 +69,11 @@ final class SystemdProvider {
final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
state = state.copyWith(units: [...parsedUserUnits, ...parsedSystemUnits], isBusy: false);
} catch (e, s) {
dprint('Parse systemd', e, s);
state = state.copyWith(isBusy: false);
}
isBusy.value = false;
}
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
@@ -75,7 +86,7 @@ for unit in ${unitNames_.join(' ')}; do
echo -n "\n${ScriptConstants.separator}\n"
done
''';
final client = _si.value.client!;
final client = _si.client!;
final result = await client.execForOutput(script);
final units = result.split(ScriptConstants.separator);

View File

@@ -0,0 +1,283 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'systemd.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SystemdState {
bool get isBusy; List<SystemdUnit> get units; SystemdScopeFilter get scopeFilter;
/// Create a copy of SystemdState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SystemdStateCopyWith<SystemdState> get copyWith => _$SystemdStateCopyWithImpl<SystemdState>(this as SystemdState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other.units, units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter));
}
@override
int get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(units),scopeFilter);
@override
String toString() {
return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)';
}
}
/// @nodoc
abstract mixin class $SystemdStateCopyWith<$Res> {
factory $SystemdStateCopyWith(SystemdState value, $Res Function(SystemdState) _then) = _$SystemdStateCopyWithImpl;
@useResult
$Res call({
bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter
});
}
/// @nodoc
class _$SystemdStateCopyWithImpl<$Res>
implements $SystemdStateCopyWith<$Res> {
_$SystemdStateCopyWithImpl(this._self, this._then);
final SystemdState _self;
final $Res Function(SystemdState) _then;
/// Create a copy of SystemdState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) {
return _then(_self.copyWith(
isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
as bool,units: null == units ? _self.units : units // ignore: cast_nullable_to_non_nullable
as List<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable
as SystemdScopeFilter,
));
}
}
/// Adds pattern-matching-related methods to [SystemdState].
extension SystemdStatePatterns on SystemdState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SystemdState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SystemdState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SystemdState value) $default,){
final _that = this;
switch (_that) {
case _SystemdState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SystemdState value)? $default,){
final _that = this;
switch (_that) {
case _SystemdState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SystemdState() when $default != null:
return $default(_that.isBusy,_that.units,_that.scopeFilter);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter) $default,) {final _that = this;
switch (_that) {
case _SystemdState():
return $default(_that.isBusy,_that.units,_that.scopeFilter);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter)? $default,) {final _that = this;
switch (_that) {
case _SystemdState() when $default != null:
return $default(_that.isBusy,_that.units,_that.scopeFilter);case _:
return null;
}
}
}
/// @nodoc
class _SystemdState implements SystemdState {
const _SystemdState({this.isBusy = false, final List<SystemdUnit> units = const <SystemdUnit>[], this.scopeFilter = SystemdScopeFilter.all}): _units = units;
@override@JsonKey() final bool isBusy;
final List<SystemdUnit> _units;
@override@JsonKey() List<SystemdUnit> get units {
if (_units is EqualUnmodifiableListView) return _units;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_units);
}
@override@JsonKey() final SystemdScopeFilter scopeFilter;
/// Create a copy of SystemdState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SystemdStateCopyWith<_SystemdState> get copyWith => __$SystemdStateCopyWithImpl<_SystemdState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other._units, _units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter));
}
@override
int get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(_units),scopeFilter);
@override
String toString() {
return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)';
}
}
/// @nodoc
abstract mixin class _$SystemdStateCopyWith<$Res> implements $SystemdStateCopyWith<$Res> {
factory _$SystemdStateCopyWith(_SystemdState value, $Res Function(_SystemdState) _then) = __$SystemdStateCopyWithImpl;
@override @useResult
$Res call({
bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter
});
}
/// @nodoc
class __$SystemdStateCopyWithImpl<$Res>
implements _$SystemdStateCopyWith<$Res> {
__$SystemdStateCopyWithImpl(this._self, this._then);
final _SystemdState _self;
final $Res Function(_SystemdState) _then;
/// Create a copy of SystemdState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) {
return _then(_SystemdState(
isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
as bool,units: null == units ? _self._units : units // ignore: cast_nullable_to_non_nullable
as List<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable
as SystemdScopeFilter,
));
}
}
// dart format on

View File

@@ -0,0 +1,108 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'systemd.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(SystemdNotifier)
const systemdProvider = SystemdNotifierFamily._();
final class SystemdNotifierProvider
extends $NotifierProvider<SystemdNotifier, SystemdState> {
const SystemdNotifierProvider._({
required SystemdNotifierFamily super.from,
required Spi super.argument,
}) : super(
retry: null,
name: r'systemdProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$systemdNotifierHash();
@override
String toString() {
return r'systemdProvider'
''
'($argument)';
}
@$internal
@override
SystemdNotifier create() => SystemdNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SystemdState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SystemdState>(value),
);
}
@override
bool operator ==(Object other) {
return other is SystemdNotifierProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$systemdNotifierHash() => r'030d556efc3d897419cd3462d37cb705813e24c7';
final class SystemdNotifierFamily extends $Family
with
$ClassFamilyOverride<
SystemdNotifier,
SystemdState,
SystemdState,
SystemdState,
Spi
> {
const SystemdNotifierFamily._()
: super(
retry: null,
name: r'systemdProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
SystemdNotifierProvider call(Spi spi) =>
SystemdNotifierProvider._(argument: spi, from: this);
@override
String toString() => r'systemdProvider';
}
abstract class _$SystemdNotifier extends $Notifier<SystemdState> {
late final _$args = ref.$arg as Spi;
Spi get spi => _$args;
SystemdState build(Spi spi);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref = this.ref as $Ref<SystemdState, SystemdState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<SystemdState, SystemdState>,
SystemdState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,56 +1,63 @@
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/data/res/store.dart';
import 'package:xterm/core.dart';
class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
VirtKeyProvider();
part 'virtual_keyboard.g.dart';
part 'virtual_keyboard.freezed.dart';
bool _ctrl = false;
bool get ctrl => _ctrl;
set ctrl(bool value) {
if (value != _ctrl) {
_ctrl = value;
notifyListeners();
@freezed
abstract class VirtKeyState with _$VirtKeyState {
const factory VirtKeyState({
@Default(false) final bool ctrl,
@Default(false) final bool alt,
@Default(false) final bool shift,
}) = _VirtKeyState;
}
@riverpod
class VirtKeyboard extends _$VirtKeyboard implements TerminalInputHandler {
@override
VirtKeyState build() {
return const VirtKeyState();
}
bool get ctrl => state.ctrl;
bool get alt => state.alt;
bool get shift => state.shift;
void setCtrl(bool value) {
if (value != state.ctrl) {
state = state.copyWith(ctrl: value);
}
}
bool _alt = false;
bool get alt => _alt;
set alt(bool value) {
if (value != _alt) {
_alt = value;
notifyListeners();
void setAlt(bool value) {
if (value != state.alt) {
state = state.copyWith(alt: value);
}
}
bool _shift = false;
bool get shift => _shift;
set shift(bool value) {
if (value != _shift) {
_shift = value;
notifyListeners();
void setShift(bool value) {
if (value != state.shift) {
state = state.copyWith(shift: value);
}
}
void reset(TerminalKeyboardEvent e) {
if (e.ctrl) {
ctrl = false;
}
if (e.alt) {
alt = false;
}
if (e.shift) {
shift = false;
}
notifyListeners();
state = state.copyWith(
ctrl: e.ctrl ? false : state.ctrl,
alt: e.alt ? false : state.alt,
shift: e.shift ? false : state.shift,
);
}
@override
String? call(TerminalKeyboardEvent event) {
final e = event.copyWith(
ctrl: event.ctrl || ctrl,
alt: event.alt || alt,
shift: event.shift || shift,
ctrl: event.ctrl || state.ctrl,
alt: event.alt || state.alt,
shift: event.shift || state.shift,
);
if (Stores.setting.sshVirtualKeyAutoOff.fetch()) {
reset(e);

View File

@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'virtual_keyboard.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$VirtKeyState {
bool get ctrl; bool get alt; bool get shift;
/// Create a copy of VirtKeyState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$VirtKeyStateCopyWith<VirtKeyState> get copyWith => _$VirtKeyStateCopyWithImpl<VirtKeyState>(this as VirtKeyState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift));
}
@override
int get hashCode => Object.hash(runtimeType,ctrl,alt,shift);
@override
String toString() {
return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)';
}
}
/// @nodoc
abstract mixin class $VirtKeyStateCopyWith<$Res> {
factory $VirtKeyStateCopyWith(VirtKeyState value, $Res Function(VirtKeyState) _then) = _$VirtKeyStateCopyWithImpl;
@useResult
$Res call({
bool ctrl, bool alt, bool shift
});
}
/// @nodoc
class _$VirtKeyStateCopyWithImpl<$Res>
implements $VirtKeyStateCopyWith<$Res> {
_$VirtKeyStateCopyWithImpl(this._self, this._then);
final VirtKeyState _self;
final $Res Function(VirtKeyState) _then;
/// Create a copy of VirtKeyState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) {
return _then(_self.copyWith(
ctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable
as bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable
as bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [VirtKeyState].
extension VirtKeyStatePatterns on VirtKeyState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _VirtKeyState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _VirtKeyState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _VirtKeyState value) $default,){
final _that = this;
switch (_that) {
case _VirtKeyState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _VirtKeyState value)? $default,){
final _that = this;
switch (_that) {
case _VirtKeyState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool ctrl, bool alt, bool shift)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _VirtKeyState() when $default != null:
return $default(_that.ctrl,_that.alt,_that.shift);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool ctrl, bool alt, bool shift) $default,) {final _that = this;
switch (_that) {
case _VirtKeyState():
return $default(_that.ctrl,_that.alt,_that.shift);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool ctrl, bool alt, bool shift)? $default,) {final _that = this;
switch (_that) {
case _VirtKeyState() when $default != null:
return $default(_that.ctrl,_that.alt,_that.shift);case _:
return null;
}
}
}
/// @nodoc
class _VirtKeyState implements VirtKeyState {
const _VirtKeyState({this.ctrl = false, this.alt = false, this.shift = false});
@override@JsonKey() final bool ctrl;
@override@JsonKey() final bool alt;
@override@JsonKey() final bool shift;
/// Create a copy of VirtKeyState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$VirtKeyStateCopyWith<_VirtKeyState> get copyWith => __$VirtKeyStateCopyWithImpl<_VirtKeyState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift));
}
@override
int get hashCode => Object.hash(runtimeType,ctrl,alt,shift);
@override
String toString() {
return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)';
}
}
/// @nodoc
abstract mixin class _$VirtKeyStateCopyWith<$Res> implements $VirtKeyStateCopyWith<$Res> {
factory _$VirtKeyStateCopyWith(_VirtKeyState value, $Res Function(_VirtKeyState) _then) = __$VirtKeyStateCopyWithImpl;
@override @useResult
$Res call({
bool ctrl, bool alt, bool shift
});
}
/// @nodoc
class __$VirtKeyStateCopyWithImpl<$Res>
implements _$VirtKeyStateCopyWith<$Res> {
__$VirtKeyStateCopyWithImpl(this._self, this._then);
final _VirtKeyState _self;
final $Res Function(_VirtKeyState) _then;
/// Create a copy of VirtKeyState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) {
return _then(_VirtKeyState(
ctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable
as bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable
as bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'virtual_keyboard.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(VirtKeyboard)
const virtKeyboardProvider = VirtKeyboardProvider._();
final class VirtKeyboardProvider
extends $NotifierProvider<VirtKeyboard, VirtKeyState> {
const VirtKeyboardProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'virtKeyboardProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$virtKeyboardHash();
@$internal
@override
VirtKeyboard create() => VirtKeyboard();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(VirtKeyState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<VirtKeyState>(value),
);
}
}
String _$virtKeyboardHash() => r'1327d412bfb0dd261f3b555f353a8852b4f753e5';
abstract class _$VirtKeyboard extends $Notifier<VirtKeyState> {
VirtKeyState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<VirtKeyState, VirtKeyState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<VirtKeyState, VirtKeyState>,
VirtKeyState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -3,6 +3,6 @@
abstract class BuildData {
static const String name = "ServerBox";
static const int build = 1220;
static const int script = 68;
static const int build = 1262;
static const int script = 69;
}

View File

@@ -124,6 +124,12 @@ abstract final class GithubIds {
'CreeperKong',
'zxf945',
'cnen2018',
'xiaomeng9597',
'mingzhao2019',
'HHXXYY123',
'Lancerys',
'yaziku',
'yeluosln',
};
}

View File

@@ -1,4 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:get_it/get_it.dart';
import 'package:server_box/data/store/connection_stats.dart';
import 'package:server_box/data/store/container.dart';
import 'package:server_box/data/store/history.dart';
import 'package:server_box/data/store/private_key.dart';
@@ -6,25 +8,37 @@ import 'package:server_box/data/store/server.dart';
import 'package:server_box/data/store/setting.dart';
import 'package:server_box/data/store/snippet.dart';
final GetIt getIt = GetIt.instance;
abstract final class Stores {
static final setting = SettingStore.instance;
static final server = ServerStore.instance;
static final container = ContainerStore.instance;
static final key = PrivateKeyStore.instance;
static final snippet = SnippetStore.instance;
static final history = HistoryStore.instance;
static SettingStore get setting => getIt<SettingStore>();
static ServerStore get server => getIt<ServerStore>();
static ContainerStore get container => getIt<ContainerStore>();
static PrivateKeyStore get key => getIt<PrivateKeyStore>();
static SnippetStore get snippet => getIt<SnippetStore>();
static HistoryStore get history => getIt<HistoryStore>();
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
/// All stores that need backup
static final List<HiveStore> _allBackup = [
SettingStore.instance,
ServerStore.instance,
ContainerStore.instance,
PrivateKeyStore.instance,
SnippetStore.instance,
HistoryStore.instance,
];
static List<HiveStore> get _allBackup => [
setting,
server,
container,
key,
snippet,
history,
connectionStats,
];
static Future<void> init() async {
getIt.registerLazySingleton<SettingStore>(() => SettingStore.instance);
getIt.registerLazySingleton<ServerStore>(() => ServerStore.instance);
getIt.registerLazySingleton<ContainerStore>(() => ContainerStore.instance);
getIt.registerLazySingleton<PrivateKeyStore>(() => PrivateKeyStore.instance);
getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);
getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);
getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);
await Future.wait(_allBackup.map((store) => store.init()));
}

View File

@@ -51,12 +51,31 @@ abstract final class TermSessionManager {
static void init() {
if (isAndroid) {
MethodChans.registerHandler((id) async {
_entries[id]?.disconnect?.call();
});
MethodChans.registerHandler(
(id) async {
_entries[id]?.disconnect?.call();
},
() {
// Stop all connections when notification "Stop All" is pressed
stopAllConnections();
},
);
}
}
/// Called when Android notification "Stop All" button is pressed
static void stopAllConnections() {
// Disconnect all sessions
final disconnectCallbacks = _entries.values.map((e) => e.disconnect).where((cb) => cb != null).toList();
for (final disconnect in disconnectCallbacks) {
disconnect!();
}
// Clear all entries
_entries.clear();
_activeId = null;
_sync();
}
/// Add a session record and push update to Android.
static void add({
required String id,

View File

@@ -0,0 +1,190 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
class ConnectionStatsStore extends HiveStore {
ConnectionStatsStore._() : super('connection_stats');
static final instance = ConnectionStatsStore._();
// Record a connection attempt
void recordConnection(ConnectionStat stat) {
final key = '${stat.serverId}_${ShortId.generate()}';
set(key, stat);
_cleanOldRecords(stat.serverId);
}
// Clean records older than 30 days for a specific server
void _cleanOldRecords(String serverId) {
final cutoffTime = DateTime.now().subtract(const Duration(days: 30));
final allKeys = keys().toList();
final keysToDelete = <String>[];
for (final key in allKeys) {
if (key.startsWith(serverId)) {
final parts = key.split('_');
if (parts.length >= 2) {
final timestamp = int.tryParse(parts.last);
if (timestamp != null) {
final recordTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
if (recordTime.isBefore(cutoffTime)) {
keysToDelete.add(key);
}
}
}
}
}
for (final key in keysToDelete) {
remove(key);
}
}
// Get connection stats for a specific server
ServerConnectionStats getServerStats(String serverId, String serverName) {
final allStats = getConnectionHistory(serverId);
if (allStats.isEmpty) {
return ServerConnectionStats(
serverId: serverId,
serverName: serverName,
totalAttempts: 0,
successCount: 0,
failureCount: 0,
recentConnections: [],
successRate: 0.0,
);
}
final totalAttempts = allStats.length;
final successCount = allStats.where((s) => s.result.isSuccess).length;
final failureCount = totalAttempts - successCount;
final successRate = totalAttempts > 0 ? (successCount / totalAttempts) : 0.0;
final successTimes = allStats
.where((s) => s.result.isSuccess)
.map((s) => s.timestamp)
.toList();
final failureTimes = allStats
.where((s) => !s.result.isSuccess)
.map((s) => s.timestamp)
.toList();
DateTime? lastSuccessTime;
DateTime? lastFailureTime;
if (successTimes.isNotEmpty) {
successTimes.sort((a, b) => b.compareTo(a));
lastSuccessTime = successTimes.first;
}
if (failureTimes.isNotEmpty) {
failureTimes.sort((a, b) => b.compareTo(a));
lastFailureTime = failureTimes.first;
}
// Get recent connections (last 20)
final recentConnections = allStats.take(20).toList();
return ServerConnectionStats(
serverId: serverId,
serverName: serverName,
totalAttempts: totalAttempts,
successCount: successCount,
failureCount: failureCount,
lastSuccessTime: lastSuccessTime,
lastFailureTime: lastFailureTime,
recentConnections: recentConnections,
successRate: successRate,
);
}
// Get connection history for a specific server
List<ConnectionStat> getConnectionHistory(String serverId) {
final allKeys = keys().where((key) => key.startsWith(serverId)).toList();
final stats = <ConnectionStat>[];
for (final key in allKeys) {
final stat = get<ConnectionStat>(
key,
fromObj: (val) {
if (val is ConnectionStat) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
return ConnectionStat.fromJson(map as Map<String, dynamic>);
} catch (e) {
dprint('Parsing ConnectionStat from JSON', e);
}
}
return null;
},
);
if (stat != null) {
stats.add(stat);
}
}
// Sort by timestamp, newest first
stats.sort((a, b) => b.timestamp.compareTo(a.timestamp));
return stats;
}
// Get all servers' stats
List<ServerConnectionStats> getAllServerStats() {
final serverIds = <String>{};
final serverNames = <String, String>{};
// Get all unique server IDs
for (final key in keys()) {
final parts = key.split('_');
if (parts.length >= 2) {
final serverId = parts[0];
serverIds.add(serverId);
// Try to get server name from the stored stat
final stat = get<ConnectionStat>(
key,
fromObj: (val) {
if (val is ConnectionStat) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
return ConnectionStat.fromJson(map as Map<String, dynamic>);
} catch (e) {
dprint('Parsing ConnectionStat from JSON', e);
}
}
return null;
},
);
if (stat != null) {
serverNames[serverId] = stat.serverName;
}
}
}
final allStats = <ServerConnectionStats>[];
for (final serverId in serverIds) {
final serverName = serverNames[serverId] ?? serverId;
final stats = getServerStats(serverId, serverName);
allStats.add(stats);
}
return allStats;
}
// Clear all connection stats
void clearAll() {
box.clear();
}
// Clear stats for a specific server
void clearServerStats(String serverId) {
final keysToDelete = keys().where((key) => key.startsWith(serverId)).toList();
for (final key in keysToDelete) {
remove(key);
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/menu/server_func.dart';
import 'package:server_box/data/model/app/net_view.dart';
import 'package:server_box/data/model/app/server_detail_card.dart';
import 'package:server_box/data/model/app/tab.dart';
import 'package:server_box/data/model/ssh/virtual_key.dart';
import 'package:server_box/data/res/default.dart';
@@ -22,10 +23,7 @@ class SettingStore extends HiveStore {
// late final launchPage = property('launchPage', Defaults.launchPageIdx);
/// Disk view: amount / IO
late final serverTabPreferDiskAmount = propertyDefault(
'serverTabPreferDiskAmount',
false,
);
late final serverTabPreferDiskAmount = propertyDefault('serverTabPreferDiskAmount', false);
/// Bigger for bigger font size
/// 1.0 means 100%
@@ -70,20 +68,14 @@ class SettingStore extends HiveStore {
late final locale = propertyDefault('locale', '');
// SSH virtual key (ctrl | alt) auto turn off
late final sshVirtualKeyAutoOff = propertyDefault(
'sshVirtualKeyAutoOff',
true,
);
late final sshVirtualKeyAutoOff = propertyDefault('sshVirtualKeyAutoOff', true);
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
// Editor theme
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
late final editorDarkTheme = propertyDefault(
'editorDarkTheme',
Defaults.editorDarkTheme,
);
late final editorDarkTheme = propertyDefault('editorDarkTheme', Defaults.editorDarkTheme);
late final fullScreen = propertyDefault('fullScreen', false);
@@ -113,29 +105,20 @@ class SettingStore extends HiveStore {
);
// Only valid on iOS
late final autoUpdateHomeWidget = propertyDefault(
'autoUpdateHomeWidget',
isIOS,
);
late final autoUpdateHomeWidget = propertyDefault('autoUpdateHomeWidget', isIOS);
late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true);
/// Display server tab function buttons on the bottom of each server card if [true]
///
/// Otherwise, display them on the top of server detail page
late final moveServerFuncs = propertyDefault(
'moveOutServerTabFuncBtns',
false,
);
late final moveServerFuncs = propertyDefault('moveOutServerTabFuncBtns', false);
/// Whether use `rm -r` to delete directory on SFTP
late final sftpRmrDir = propertyDefault('sftpRmrDir', false);
/// Whether use system's primary color as the app's primary color
late final useSystemPrimaryColor = propertyDefault(
'useSystemPrimaryColor',
false,
);
late final useSystemPrimaryColor = propertyDefault('useSystemPrimaryColor', false);
/// Only valid on iOS / Android / Windows
late final useBioAuth = propertyDefault('useBioAuth', false);
@@ -151,10 +134,7 @@ class SettingStore extends HiveStore {
late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true);
/// Show folders first in SFTP file browser
late final sftpShowFoldersFirst = propertyDefault(
'sftpShowFoldersFirst',
true,
);
late final sftpShowFoldersFirst = propertyDefault('sftpShowFoldersFirst', true);
/// Show tip of suspend
late final showSuspendTip = propertyDefault('showSuspendTip', true);
@@ -162,10 +142,7 @@ class SettingStore extends HiveStore {
/// Whether collapse UI items by default
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
late final serverFuncBtns = listProperty(
'serverBtns',
defaultValue: ServerFuncBtn.defaultIdxs,
);
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
/// Docker is more popular than podman, set to `false` to use docker
late final usePodman = propertyDefault('usePodman', false);
@@ -180,16 +157,10 @@ class SettingStore extends HiveStore {
late final containerParseStat = propertyDefault('containerParseStat', true);
/// Auto refresh container status
late final containerAutoRefresh = propertyDefault(
'containerAutoRefresh',
true,
);
late final containerAutoRefresh = propertyDefault('containerAutoRefresh', true);
/// Use double column servers page on Desktop
late final doubleColumnServersPage = propertyDefault(
'doubleColumnServersPage',
true,
);
late final doubleColumnServersPage = propertyDefault('doubleColumnServersPage', true);
/// Ignore local network device (eg: br-xxx, ovs-system...)
/// when building traffic view on server tab
@@ -244,8 +215,7 @@ class SettingStore extends HiveStore {
/// Record the position and size of the window.
late final windowState = property<WindowState>(
'windowState',
fromObj: (raw) =>
WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
fromObj: (raw) => WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
toObj: (state) => state == null ? null : jsonEncode(state.toJson()),
);
@@ -258,10 +228,7 @@ class SettingStore extends HiveStore {
late final sftpEditor = propertyDefault('sftpEditor', '');
/// Preferred terminal emulator command on desktop
late final desktopTerminal = propertyDefault(
'desktopTerminal',
'x-terminal-emulator',
);
late final desktopTerminal = propertyDefault('desktopTerminal', 'x-terminal-emulator');
/// Run foreground service on Android, if the SSH terminal is running
late final fgService = propertyDefault('fgService', false);
@@ -277,4 +244,17 @@ 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);
/// Tabs at home page
late final homeTabs = listProperty(
'homeTabs',
defaultValue: AppTab.values,
fromObj: AppTab.parseAppTabsFromObj,
toObj: (val) {
return val?.map((e) => e.name).toList() ?? [];
},
);
}

View File

@@ -155,6 +155,12 @@ abstract class AppLocalizations {
/// **'Already in last directory.'**
String get alreadyLastDir;
/// No description provided for @atLeastOneTab.
///
/// In en, this message translates to:
/// **'At least one tab must be selected'**
String get atLeastOneTab;
/// No description provided for @authFailTip.
///
/// In en, this message translates to:
@@ -185,35 +191,11 @@ abstract class AppLocalizations {
/// **'Automatic home widget update'**
String get autoUpdateHomeWidget;
/// No description provided for @backupTip.
/// No description provided for @availableTabs.
///
/// In en, this message translates to:
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
String get backupTip;
/// No description provided for @backupVersionNotMatch.
///
/// In en, this message translates to:
/// **'Backup version is not match.'**
String get backupVersionNotMatch;
/// No description provided for @backupPassword.
///
/// In en, this message translates to:
/// **'Backup password'**
String get backupPassword;
/// No description provided for @backupPasswordTip.
///
/// In en, this message translates to:
/// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**
String get backupPasswordTip;
/// No description provided for @backupPasswordWrong.
///
/// In en, this message translates to:
/// **'Incorrect backup password'**
String get backupPasswordWrong;
/// **'Available Tabs'**
String get availableTabs;
/// No description provided for @backupEncrypted.
///
@@ -227,11 +209,11 @@ abstract class AppLocalizations {
/// **'Backup is not encrypted'**
String get backupNotEncrypted;
/// No description provided for @backupPasswordSet.
/// No description provided for @backupPassword.
///
/// In en, this message translates to:
/// **'Backup password set'**
String get backupPasswordSet;
/// **'Backup password'**
String get backupPassword;
/// No description provided for @backupPasswordRemoved.
///
@@ -239,6 +221,36 @@ abstract class AppLocalizations {
/// **'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:
/// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**
String get backupPasswordTip;
/// No description provided for @backupPasswordWrong.
///
/// In en, this message translates to:
/// **'Incorrect backup password'**
String get backupPasswordWrong;
/// No description provided for @backupTip.
///
/// In en, this message translates to:
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
String get backupTip;
/// No description provided for @backupVersionNotMatch.
///
/// In en, this message translates to:
/// **'Backup version is not match.'**
String get backupVersionNotMatch;
/// No description provided for @battery.
///
/// In en, this message translates to:
@@ -257,6 +269,36 @@ abstract class AppLocalizations {
/// **'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\".'**
String get bgRunTip;
/// No description provided for @clearAllStatsContent.
///
/// In en, this message translates to:
/// **'Are you sure you want to clear all server connection statistics? This action cannot be undone.'**
String get clearAllStatsContent;
/// No description provided for @clearAllStatsTitle.
///
/// In en, this message translates to:
/// **'Clear All Statistics'**
String get clearAllStatsTitle;
/// No description provided for @clearServerStatsContent.
///
/// In en, this message translates to:
/// **'Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.'**
String clearServerStatsContent(String serverName);
/// No description provided for @clearServerStatsTitle.
///
/// In en, this message translates to:
/// **'Clear {serverName} Statistics'**
String clearServerStatsTitle(String serverName);
/// No description provided for @clearThisServerStats.
///
/// In en, this message translates to:
/// **'Clear This Server Statistics'**
String get clearThisServerStats;
/// No description provided for @closeAfterSave.
///
/// In en, this message translates to:
@@ -281,6 +323,24 @@ abstract class AppLocalizations {
/// **'Connection'**
String get conn;
/// No description provided for @connectionDetails.
///
/// In en, this message translates to:
/// **'Connection Details'**
String get connectionDetails;
/// No description provided for @connectionStats.
///
/// In en, this message translates to:
/// **'Connection Statistics'**
String get connectionStats;
/// No description provided for @connectionStatsDesc.
///
/// In en, this message translates to:
/// **'View server connection success rate and history'**
String get connectionStatsDesc;
/// No description provided for @container.
///
/// In en, this message translates to:
@@ -371,6 +431,30 @@ abstract class AppLocalizations {
/// **'Disconnected'**
String get disconnected;
/// No description provided for @discoverSshServers.
///
/// In en, this message translates to:
/// **'Discover SSH Servers'**
String get discoverSshServers;
/// No description provided for @discoveryFailed.
///
/// In en, this message translates to:
/// **'Discovery failed'**
String get discoveryFailed;
/// No description provided for @discoverySettings.
///
/// In en, this message translates to:
/// **'Discovery Settings'**
String get discoverySettings;
/// No description provided for @discoverySummary.
///
/// In en, this message translates to:
/// **'Discovery Summary'**
String get discoverySummary;
/// No description provided for @disk.
///
/// In en, this message translates to:
@@ -452,12 +536,6 @@ abstract class AppLocalizations {
/// **'Edit virtual keys'**
String get editVirtKeys;
/// No description provided for @editor.
///
/// In en, this message translates to:
/// **'Editor'**
String get editor;
/// No description provided for @editorHighlightTip.
///
/// In en, this message translates to:
@@ -470,6 +548,18 @@ abstract class AppLocalizations {
/// **'Emulator'**
String get emulator;
/// No description provided for @enableMdns.
///
/// In en, this message translates to:
/// **'Enable mDNS'**
String get enableMdns;
/// No description provided for @enableMdnsDesc.
///
/// In en, this message translates to:
/// **'Use mDNS/Bonjour to discover SSH services'**
String get enableMdnsDesc;
/// No description provided for @encode.
///
/// In en, this message translates to:
@@ -524,18 +614,18 @@ abstract class AppLocalizations {
/// **'File \'{file}\' too large {size}, max {sizeMax}'**
String fileTooLarge(Object file, Object size, Object sizeMax);
/// No description provided for @finishedAt.
///
/// In en, this message translates to:
/// **'Finished at'**
String get finishedAt;
/// No description provided for @followSystem.
///
/// In en, this message translates to:
/// **'Follow system'**
String get followSystem;
/// No description provided for @font.
///
/// In en, this message translates to:
/// **'Font'**
String get font;
/// No description provided for @fontSize.
///
/// In en, this message translates to:
@@ -596,6 +686,18 @@ abstract class AppLocalizations {
/// **'Code highlighting'**
String get highlight;
/// No description provided for @homeTabs.
///
/// In en, this message translates to:
/// **'Home Tabs'**
String get homeTabs;
/// No description provided for @homeTabsCustomizeDesc.
///
/// In en, this message translates to:
/// **'Customize which tabs appear on the home page and their order'**
String get homeTabsCustomizeDesc;
/// No description provided for @homeWidgetUrlConfig.
///
/// In en, this message translates to:
@@ -632,12 +734,6 @@ abstract class AppLocalizations {
/// **'Images list'**
String get imagesList;
/// No description provided for @init.
///
/// In en, this message translates to:
/// **'Initialize'**
String get init;
/// No description provided for @inner.
///
/// In en, this message translates to:
@@ -692,6 +788,18 @@ abstract class AppLocalizations {
/// **'Key Auth'**
String get keyAuth;
/// No description provided for @lastFailure.
///
/// In en, this message translates to:
/// **'Last Failure'**
String get lastFailure;
/// No description provided for @lastSuccess.
///
/// In en, this message translates to:
/// **'Last Success'**
String get lastSuccess;
/// No description provided for @letterCache.
///
/// In en, this message translates to:
@@ -704,12 +812,6 @@ abstract class AppLocalizations {
/// **'Recommended to disable, but after disabling, it will be impossible to input CJK characters.'**
String get letterCacheTip;
/// No description provided for @license.
///
/// In en, this message translates to:
/// **'License'**
String get license;
/// No description provided for @location.
///
/// In en, this message translates to:
@@ -728,18 +830,18 @@ abstract class AppLocalizations {
/// **'Made with ❤️ by {myGithub}'**
String madeWithLove(Object myGithub);
/// No description provided for @manual.
///
/// In en, this message translates to:
/// **'Manual'**
String get manual;
/// No description provided for @max.
///
/// In en, this message translates to:
/// **'max'**
String get max;
/// No description provided for @maxConcurrency.
///
/// In en, this message translates to:
/// **'Max Concurrency'**
String get maxConcurrency;
/// No description provided for @maxRetryCount.
///
/// In en, this message translates to:
@@ -812,6 +914,12 @@ abstract class AppLocalizations {
/// **'New container'**
String get newContainer;
/// No description provided for @noConnectionStatsData.
///
/// In en, this message translates to:
/// **'No connection statistics data'**
String get noConnectionStatsData;
/// No description provided for @noLineChart.
///
/// In en, this message translates to:
@@ -938,12 +1046,6 @@ abstract class AppLocalizations {
/// **'Prioritize displaying disk capacity'**
String get preferDiskAmount;
/// No description provided for @preview.
///
/// In en, this message translates to:
/// **'Preview'**
String get preview;
/// No description provided for @privateKey.
///
/// In en, this message translates to:
@@ -998,6 +1100,12 @@ abstract class AppLocalizations {
/// **'Reboot'**
String get reboot;
/// No description provided for @recentConnections.
///
/// In en, this message translates to:
/// **'Recent Connections'**
String get recentConnections;
/// No description provided for @rememberPwdInMem.
///
/// In en, this message translates to:
@@ -1118,6 +1226,18 @@ abstract class AppLocalizations {
/// **'Server order'**
String get serverOrder;
/// No description provided for @serverTabRequired.
///
/// In en, this message translates to:
/// **'Server tab cannot be removed'**
String get serverTabRequired;
/// No description provided for @servers.
///
/// In en, this message translates to:
/// **'servers'**
String get servers;
/// No description provided for @sftpDlPrepare.
///
/// In en, this message translates to:
@@ -1202,6 +1322,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:
@@ -1280,12 +1478,6 @@ abstract class AppLocalizations {
/// **'Switch to {val}'**
String switchTo(Object val);
/// No description provided for @sync.
///
/// In en, this message translates to:
/// **'Sync'**
String get sync;
/// No description provided for @syncTip.
///
/// In en, this message translates to:
@@ -1304,6 +1496,12 @@ abstract class AppLocalizations {
/// **'Tags'**
String get tag;
/// No description provided for @tapToStartDiscovery.
///
/// In en, this message translates to:
/// **'Tap the search button to discover SSH servers on your network'**
String get tapToStartDiscovery;
/// No description provided for @temperature.
///
/// In en, this message translates to:
@@ -1364,6 +1562,12 @@ abstract class AppLocalizations {
/// **'Total'**
String get total;
/// No description provided for @totalAttempts.
///
/// In en, this message translates to:
/// **'Total'**
String get totalAttempts;
/// No description provided for @traffic.
///
/// In en, this message translates to:
@@ -1412,12 +1616,6 @@ abstract class AppLocalizations {
/// **'Server status update interval'**
String get updateServerStatusInterval;
/// No description provided for @upload.
///
/// In en, this message translates to:
/// **'Upload'**
String get upload;
/// No description provided for @upsideDown.
///
/// In en, this message translates to:
@@ -1466,6 +1664,12 @@ abstract class AppLocalizations {
/// **'View'**
String get view;
/// No description provided for @viewDetails.
///
/// In en, this message translates to:
/// **'View Details'**
String get viewDetails;
/// No description provided for @viewErr.
///
/// In en, this message translates to:

View File

@@ -28,6 +28,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get alreadyLastDir => 'Bereits im letzten Verzeichnis.';
@override
String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein';
@override
String get authFailTip =>
'Authentifizierung fehlgeschlagen, bitte überprüfen Sie, ob das Passwort/Schlüssel/Host/Benutzer usw. falsch sind.';
@@ -46,16 +49,23 @@ 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 availableTabs => 'Verfügbare Tabs';
@override
String get backupVersionNotMatch =>
'Die Backup-Version stimmt nicht überein.';
String get backupEncrypted => 'Backup ist verschlüsselt';
@override
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 +74,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';
@@ -85,6 +91,26 @@ class AppLocalizationsDe extends AppLocalizations {
String get 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\".';
@override
String get clearAllStatsContent =>
'Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
@override
String get clearAllStatsTitle => 'Alle Statistiken löschen';
@override
String clearServerStatsContent(String serverName) {
return 'Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"$serverName\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
}
@override
String clearServerStatsTitle(String serverName) {
return '$serverName Statistiken löschen';
}
@override
String get clearThisServerStats => 'Statistiken dieses Servers löschen';
@override
String get closeAfterSave => 'Speichern und schließen';
@@ -98,6 +124,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get conn => 'Verbindung';
@override
String get connectionDetails => 'Verbindungsdetails';
@override
String get connectionStats => 'Verbindungsstatistiken';
@override
String get connectionStatsDesc =>
'Server-Verbindungserfolgsrate und Verlauf anzeigen';
@override
String get container => 'Container';
@@ -147,6 +183,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get disconnected => 'Disconnected';
@override
String get discoverSshServers => 'SSH-Server entdecken';
@override
String get discoveryFailed => 'Entdeckung fehlgeschlagen';
@override
String get discoverySettings => 'Entdeckungseinstellungen';
@override
String get discoverySummary => 'Entdeckungs-Zusammenfassung';
@override
String get disk => 'Festplatte';
@@ -199,9 +247,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get editVirtKeys => 'Virtuelle Tasten bearbeiten';
@override
String get editor => 'Editor';
@override
String get editorHighlightTip =>
'Die Leistung der aktuellen Codehervorhebung ist schlechter und kann zur Verbesserung optional ausgeschaltet werden.';
@@ -209,6 +254,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get emulator => 'Emulator';
@override
String get enableMdns => 'mDNS aktivieren';
@override
String get enableMdnsDesc =>
'mDNS/Bonjour verwenden, um SSH-Dienste zu entdecken';
@override
String get encode => 'Encode';
@@ -241,10 +293,10 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get followSystem => 'System verfolgen';
String get finishedAt => 'Beendet um';
@override
String get font => 'Schriftarten';
String get followSystem => 'System verfolgen';
@override
String get fontSize => 'Schriftgröße';
@@ -277,6 +329,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get highlight => 'Code highlight';
@override
String get homeTabs => 'Home-Tabs';
@override
String get homeTabsCustomizeDesc =>
'Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge';
@override
String get homeWidgetUrlConfig => 'Home-Widget-Link konfigurieren';
@@ -297,9 +356,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get imagesList => 'Images';
@override
String get init => 'Initialisieren';
@override
String get inner => 'Eingebaut';
@@ -329,6 +385,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get keyAuth => 'Schlüsselauthentifzierung';
@override
String get lastFailure => 'Letzter Fehler';
@override
String get lastSuccess => 'Letzter Erfolg';
@override
String get letterCache => 'Buchstaben-Caching';
@@ -336,9 +398,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get letterCacheTip =>
'Empfohlen, zu deaktivieren, aber nach dem Deaktivieren können keine CJK-Zeichen eingegeben werden.';
@override
String get license => 'Lizenzen';
@override
String get location => 'Standort';
@@ -351,10 +410,10 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get manual => 'Handbuch';
String get max => 'max';
@override
String get max => 'max';
String get maxConcurrency => 'Maximale Gleichzeitigkeit';
@override
String get maxRetryCount => 'Anzahl an Verbindungsversuchen';
@@ -395,6 +454,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get newContainer => 'Neuer Container';
@override
String get noConnectionStatsData => 'Keine Verbindungsstatistikdaten';
@override
String get noLineChart => 'Verwenden Sie keine Liniendiagramme';
@@ -465,9 +527,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get preferDiskAmount => 'Festplattenkapazität vorrangig anzeigen';
@override
String get preview => 'Vorschau';
@override
String get privateKey => 'Private Key';
@@ -498,6 +557,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get reboot => 'Neustart';
@override
String get recentConnections => 'Kürzliche Verbindungen';
@override
String get rememberPwdInMem => 'Passwort im Speicher behalten';
@@ -559,6 +621,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get serverOrder => 'Server-Bestellung';
@override
String get serverTabRequired => 'Server-Tab kann nicht entfernt werden';
@override
String get servers => 'Server';
@override
String get sftpDlPrepare => 'Verbindung vorbereiten...';
@@ -606,6 +674,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.';
@@ -653,9 +777,6 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Wechseln zu $val';
}
@override
String get sync => 'Sync';
@override
String get syncTip =>
'Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.';
@@ -666,6 +787,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get tag => 'Tags';
@override
String get tapToStartDiscovery =>
'Tippen Sie auf die Suche-Schaltfläche, um SSH-Server in Ihrem Netzwerk zu entdecken';
@override
String get temperature => 'Temperatur';
@@ -698,6 +823,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get total => 'Total';
@override
String get totalAttempts => 'Gesamt';
@override
String get traffic => 'Durchflussmenge';
@@ -724,9 +852,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get updateServerStatusInterval =>
'Aktualisierungsintervall des Serverstatus';
@override
String get upload => 'Hochladen';
@override
String get upsideDown => 'Upside Down';
@@ -752,6 +877,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get view => 'Ansicht';
@override
String get viewDetails => 'Details anzeigen';
@override
String get viewErr => 'Fehler anzeigen';

View File

@@ -28,6 +28,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get alreadyLastDir => 'Already in last directory.';
@override
String get atLeastOneTab => 'At least one tab must be selected';
@override
String get authFailTip =>
'Authentication failed, please check whether credentials are correct';
@@ -46,15 +49,23 @@ 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 availableTabs => 'Available Tabs';
@override
String get backupVersionNotMatch => 'Backup version is not match.';
String get backupEncrypted => 'Backup is encrypted';
@override
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 +74,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';
@@ -84,6 +90,26 @@ class AppLocalizationsEn extends AppLocalizations {
String get 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\".';
@override
String get clearAllStatsContent =>
'Are you sure you want to clear all server connection statistics? This action cannot be undone.';
@override
String get clearAllStatsTitle => 'Clear All Statistics';
@override
String clearServerStatsContent(String serverName) {
return 'Are you sure you want to clear connection statistics for server \"$serverName\"? This action cannot be undone.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Clear $serverName Statistics';
}
@override
String get clearThisServerStats => 'Clear This Server Statistics';
@override
String get closeAfterSave => 'Save and close';
@@ -97,6 +123,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get conn => 'Connection';
@override
String get connectionDetails => 'Connection Details';
@override
String get connectionStats => 'Connection Statistics';
@override
String get connectionStatsDesc =>
'View server connection success rate and history';
@override
String get container => 'Container';
@@ -146,6 +182,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get disconnected => 'Disconnected';
@override
String get discoverSshServers => 'Discover SSH Servers';
@override
String get discoveryFailed => 'Discovery failed';
@override
String get discoverySettings => 'Discovery Settings';
@override
String get discoverySummary => 'Discovery Summary';
@override
String get disk => 'Disk';
@@ -198,9 +246,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get editVirtKeys => 'Edit virtual keys';
@override
String get editor => 'Editor';
@override
String get editorHighlightTip =>
'The current code highlighting performance is not ideal and can be optionally turned off to improve.';
@@ -208,6 +253,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get emulator => 'Emulator';
@override
String get enableMdns => 'Enable mDNS';
@override
String get enableMdnsDesc => 'Use mDNS/Bonjour to discover SSH services';
@override
String get encode => 'Encode';
@@ -240,10 +291,10 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get followSystem => 'Follow system';
String get finishedAt => 'Finished at';
@override
String get font => 'Font';
String get followSystem => 'Follow system';
@override
String get fontSize => 'Font size';
@@ -276,6 +327,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get highlight => 'Code highlighting';
@override
String get homeTabs => 'Home Tabs';
@override
String get homeTabsCustomizeDesc =>
'Customize which tabs appear on the home page and their order';
@override
String get homeWidgetUrlConfig => 'Config home widget url';
@@ -296,9 +354,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get imagesList => 'Images list';
@override
String get init => 'Initialize';
@override
String get inner => 'Inner';
@@ -328,6 +383,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get keyAuth => 'Key Auth';
@override
String get lastFailure => 'Last Failure';
@override
String get lastSuccess => 'Last Success';
@override
String get letterCache => 'Letter caching';
@@ -335,9 +396,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get letterCacheTip =>
'Recommended to disable, but after disabling, it will be impossible to input CJK characters.';
@override
String get license => 'License';
@override
String get location => 'Location';
@@ -350,10 +408,10 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get manual => 'Manual';
String get max => 'max';
@override
String get max => 'max';
String get maxConcurrency => 'Max Concurrency';
@override
String get maxRetryCount => 'Number of server reconnections';
@@ -393,6 +451,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get newContainer => 'New container';
@override
String get noConnectionStatsData => 'No connection statistics data';
@override
String get noLineChart => 'Do not use line charts';
@@ -463,9 +524,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get preferDiskAmount => 'Prioritize displaying disk capacity';
@override
String get preview => 'Preview';
@override
String get privateKey => 'Private Key';
@@ -496,6 +554,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get reboot => 'Reboot';
@override
String get recentConnections => 'Recent Connections';
@override
String get rememberPwdInMem => 'Remember password in memory';
@@ -556,6 +617,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get serverOrder => 'Server order';
@override
String get serverTabRequired => 'Server tab cannot be removed';
@override
String get servers => 'servers';
@override
String get sftpDlPrepare => 'Preparing to connect...';
@@ -602,6 +669,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.';
@@ -648,9 +769,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Switch to $val';
}
@override
String get sync => 'Sync';
@override
String get syncTip =>
'A restart may be required for some changes to take effect.';
@@ -661,6 +779,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get tag => 'Tags';
@override
String get tapToStartDiscovery =>
'Tap the search button to discover SSH servers on your network';
@override
String get temperature => 'Temperature';
@@ -693,6 +815,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get total => 'Total';
@override
String get totalAttempts => 'Total';
@override
String get traffic => 'Traffic';
@@ -718,9 +843,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get updateServerStatusInterval => 'Server status update interval';
@override
String get upload => 'Upload';
@override
String get upsideDown => 'Upside Down';
@@ -746,6 +868,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get view => 'View';
@override
String get viewDetails => 'View Details';
@override
String get viewErr => 'See error';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get alreadyLastDir => 'Ya estás en el directorio superior';
@override
String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada';
@override
String get authFailTip =>
'La autenticación ha fallado, por favor verifica si la contraseña/llave/host/usuario, etc., son incorrectos.';
@@ -46,16 +49,23 @@ 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 availableTabs => 'Pestañas disponibles';
@override
String get backupVersionNotMatch =>
'La versión de la copia de seguridad no coincide, no se puede restaurar';
String get backupEncrypted => 'El respaldo está encriptado';
@override
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 +74,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';
@@ -85,6 +91,26 @@ class AppLocalizationsEs extends AppLocalizations {
String get 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”.';
@override
String get clearAllStatsContent =>
'¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.';
@override
String get clearAllStatsTitle => 'Limpiar todas las estadísticas';
@override
String clearServerStatsContent(String serverName) {
return '¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"$serverName\"? Esta acción no se puede deshacer.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Limpiar estadísticas de $serverName';
}
@override
String get clearThisServerStats => 'Limpiar estadísticas de este servidor';
@override
String get closeAfterSave => 'Guardar y cerrar';
@@ -98,6 +124,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get conn => 'Conectar';
@override
String get connectionDetails => 'Detalles de conexión';
@override
String get connectionStats => 'Estadísticas de conexión';
@override
String get connectionStatsDesc =>
'Ver la tasa de éxito de conexión del servidor e historial';
@override
String get container => 'Contenedor';
@@ -147,6 +183,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get disconnected => 'Desconectado';
@override
String get discoverSshServers => 'Descubrir servidores SSH';
@override
String get discoveryFailed => 'Falló el descubrimiento';
@override
String get discoverySettings => 'Configuración de descubrimiento';
@override
String get discoverySummary => 'Resumen del descubrimiento';
@override
String get disk => 'Disco';
@@ -199,9 +247,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get editVirtKeys => 'Editar teclas virtuales';
@override
String get editor => 'Editor';
@override
String get editorHighlightTip =>
'El rendimiento del resaltado de código es bastante pobre actualmente, puedes elegir desactivarlo para mejorar.';
@@ -209,6 +254,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get emulator => 'Emulador';
@override
String get enableMdns => 'Habilitar mDNS';
@override
String get enableMdnsDesc => 'Usar mDNS/Bonjour para descubrir servicios SSH';
@override
String get encode => 'Codificar';
@@ -241,10 +292,10 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get followSystem => 'Seguir al sistema';
String get finishedAt => 'Terminado en';
@override
String get font => 'Fuente';
String get followSystem => 'Seguir al sistema';
@override
String get fontSize => 'Tamaño de fuente';
@@ -277,6 +328,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get highlight => 'Resaltar código';
@override
String get homeTabs => 'Pestañas de inicio';
@override
String get homeTabsCustomizeDesc =>
'Personaliza qué pestañas aparecen en la página de inicio y su orden';
@override
String get homeWidgetUrlConfig => 'Configuración de URL del widget de inicio';
@@ -297,9 +355,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get imagesList => 'Lista de imágenes';
@override
String get init => 'Inicializar';
@override
String get inner => 'Interno';
@@ -329,6 +384,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get keyAuth => 'Autenticación con llave';
@override
String get lastFailure => 'Último fallo';
@override
String get lastSuccess => 'Último éxito';
@override
String get letterCache => 'Caché de letras';
@@ -336,9 +397,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get letterCacheTip =>
'Recomendado desactivar, pero después de desactivarlo, no se podrán ingresar caracteres CJK.';
@override
String get license => 'Licencia de código abierto';
@override
String get location => 'Ubicación';
@@ -351,10 +409,10 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get manual => 'Manual';
String get max => 'Máximo';
@override
String get max => 'Máximo';
String get maxConcurrency => 'Concurrencia máxima';
@override
String get maxRetryCount =>
@@ -395,6 +453,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get newContainer => 'Crear contenedor nuevo';
@override
String get noConnectionStatsData =>
'No hay datos de estadísticas de conexión';
@override
String get noLineChart => 'No utilice gráficos de líneas';
@@ -467,9 +529,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get preferDiskAmount =>
'Priorizar la visualización de la capacidad del disco';
@override
String get preview => 'Vista previa';
@override
String get privateKey => 'Llave privada';
@@ -500,6 +559,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get reboot => 'Reiniciar';
@override
String get recentConnections => 'Conexiones recientes';
@override
String get rememberPwdInMem => 'Recordar contraseña en la memoria';
@@ -562,6 +624,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get serverOrder => 'Orden del servidor';
@override
String get serverTabRequired =>
'La pestaña del servidor no se puede eliminar';
@override
String get servers => 'servidores';
@override
String get sftpDlPrepare => 'Preparando para conectar al servidor...';
@@ -609,6 +678,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.';
@@ -655,9 +779,6 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Cambiar a $val';
}
@override
String get sync => 'Sincronizar';
@override
String get syncTip =>
'Puede que necesites reiniciar para que algunos cambios tengan efecto.';
@@ -668,6 +789,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get tag => 'Etiqueta';
@override
String get tapToStartDiscovery =>
'Toca el botón de búsqueda para descubrir servidores SSH en tu red';
@override
String get temperature => 'Temperatura';
@@ -700,6 +825,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get total => 'Total';
@override
String get totalAttempts => 'Total';
@override
String get traffic => 'Tráfico';
@@ -726,9 +854,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get updateServerStatusInterval =>
'Intervalo de actualización del estado del servidor';
@override
String get upload => 'Subir';
@override
String get upsideDown => 'Invertir arriba por abajo';
@@ -754,6 +879,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get view => 'Vista';
@override
String get viewDetails => 'Ver detalles';
@override
String get viewErr => 'Ver error';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get alreadyLastDir => 'Déjà dans le dernier répertoire.';
@override
String get atLeastOneTab => 'Au moins un onglet doit être sélectionné';
@override
String get authFailTip =>
'Échec de l\'authentification. Veuillez vérifier si le mot de passe/clé/hôte/utilisateur, etc., est incorrect.';
@@ -46,16 +49,23 @@ 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 availableTabs => 'Onglets disponibles';
@override
String get backupVersionNotMatch =>
'La version de sauvegarde ne correspond pas.';
String get backupEncrypted => 'La sauvegarde est chiffrée';
@override
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 +74,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';
@@ -85,6 +91,26 @@ class AppLocalizationsFr extends AppLocalizations {
String get 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é ».';
@override
String get clearAllStatsContent =>
'Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.';
@override
String get clearAllStatsTitle => 'Effacer toutes les statistiques';
@override
String clearServerStatsContent(String serverName) {
return 'Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"$serverName\" ? Cette action ne peut pas être annulée.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Effacer les statistiques de $serverName';
}
@override
String get clearThisServerStats => 'Effacer les statistiques de ce serveur';
@override
String get closeAfterSave => 'Enregistrer et fermer';
@@ -98,6 +124,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get conn => 'Connexion';
@override
String get connectionDetails => 'Détails de connexion';
@override
String get connectionStats => 'Statistiques de connexion';
@override
String get connectionStatsDesc =>
'Voir le taux de réussite de connexion du serveur et l\'historique';
@override
String get container => 'Conteneur';
@@ -147,6 +183,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get disconnected => 'Déconnecté';
@override
String get discoverSshServers => 'Découvrir les serveurs SSH';
@override
String get discoveryFailed => 'Échec de la découverte';
@override
String get discoverySettings => 'Paramètres de découverte';
@override
String get discoverySummary => 'Résumé de la découverte';
@override
String get disk => 'Disque';
@@ -199,9 +247,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get editVirtKeys => 'Modifier les touches virtuelles';
@override
String get editor => 'Éditeur';
@override
String get editorHighlightTip =>
'La performance actuelle de mise en surbrillance du code est pire et peut être désactivée en option pour s\'améliorer.';
@@ -209,6 +254,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get emulator => 'Émulateur';
@override
String get enableMdns => 'Activer mDNS';
@override
String get enableMdnsDesc =>
'Utiliser mDNS/Bonjour pour découvrir les services SSH';
@override
String get encode => 'Encoder';
@@ -241,10 +293,10 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get followSystem => 'Suivre le système';
String get finishedAt => 'Terminé à';
@override
String get font => 'Police';
String get followSystem => 'Suivre le système';
@override
String get fontSize => 'Taille de la police';
@@ -277,6 +329,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get highlight => 'Mise en surbrillance du code';
@override
String get homeTabs => 'Onglets d\'accueil';
@override
String get homeTabsCustomizeDesc =>
'Personnalisez les onglets qui apparaissent sur la page d\'accueil et leur ordre';
@override
String get homeWidgetUrlConfig => 'Configurer l\'URL du widget d\'accueil';
@@ -297,9 +356,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get imagesList => 'Liste des images';
@override
String get init => 'Initialiser';
@override
String get inner => 'Interne';
@@ -329,6 +385,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get keyAuth => 'Authentification par clé';
@override
String get lastFailure => 'Dernier échec';
@override
String get lastSuccess => 'Dernier succès';
@override
String get letterCache => 'Mise en cache des lettres';
@@ -336,9 +398,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get letterCacheTip =>
'Recommandé de désactiver, mais après désactivation, il sera impossible de saisir des caractères CJK.';
@override
String get license => 'Licence';
@override
String get location => 'Emplacement';
@@ -351,10 +410,10 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get manual => 'Manuel';
String get max => 'max';
@override
String get max => 'max';
String get maxConcurrency => 'Concurrence maximale';
@override
String get maxRetryCount => 'Nombre de reconnexions au serveur';
@@ -394,6 +453,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get newContainer => 'Nouveau conteneur';
@override
String get noConnectionStatsData =>
'Aucune donnée de statistiques de connexion';
@override
String get noLineChart => 'Ne pas utiliser de graphiques linéaires';
@@ -468,9 +531,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get preferDiskAmount =>
'Prioriser laffichage de la capacité du disque';
@override
String get preview => 'Aperçu';
@override
String get privateKey => 'Clé privée';
@@ -501,6 +561,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get reboot => 'Redémarrer';
@override
String get recentConnections => 'Connexions récentes';
@override
String get rememberPwdInMem => 'Mémoriser le mot de passe en mémoire';
@@ -563,6 +626,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get serverOrder => 'Ordre du serveur';
@override
String get serverTabRequired => 'L\'onglet serveur ne peut pas être supprimé';
@override
String get servers => 'serveurs';
@override
String get sftpDlPrepare => 'Préparation de la connexion...';
@@ -610,6 +679,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.';
@@ -657,9 +782,6 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Passer à $val';
}
@override
String get sync => 'Sync';
@override
String get syncTip =>
'Un redémarrage peut être nécessaire pour que certains changements prennent effet.';
@@ -670,6 +792,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get tag => 'Étiquettes';
@override
String get tapToStartDiscovery =>
'Appuyez sur le bouton de recherche pour découvrir les serveurs SSH sur votre réseau';
@override
String get temperature => 'Température';
@@ -702,6 +828,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get total => 'Total';
@override
String get totalAttempts => 'Total';
@override
String get traffic => 'Trafic';
@@ -728,9 +857,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get updateServerStatusInterval =>
'Intervalle de mise à jour de l\'état du serveur';
@override
String get upload => 'Télécharger';
@override
String get upsideDown => 'À l\'envers';
@@ -756,6 +882,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get view => 'Vue';
@override
String get viewDetails => 'Voir les détails';
@override
String get viewErr => 'Voir erreur';
@@ -798,5 +927,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.';
}

View File

@@ -28,6 +28,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get alreadyLastDir => 'Sudah di direktori terakhir.';
@override
String get atLeastOneTab => 'Setidaknya satu tab harus dipilih';
@override
String get authFailTip =>
'Otentikasi gagal, silakan periksa apakah kata sandi/kunci/host/pengguna, dll, salah.';
@@ -46,15 +49,23 @@ 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 availableTabs => 'Tab Tersedia';
@override
String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';
String get backupEncrypted => 'Cadangan telah dienkripsi';
@override
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 +74,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';
@@ -84,6 +90,26 @@ class AppLocalizationsId extends AppLocalizations {
String get 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\".';
@override
String get clearAllStatsContent =>
'Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.';
@override
String get clearAllStatsTitle => 'Hapus Semua Statistik';
@override
String clearServerStatsContent(String serverName) {
return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"$serverName\"? Tindakan ini tidak dapat dibatalkan.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Hapus Statistik $serverName';
}
@override
String get clearThisServerStats => 'Hapus Statistik Server Ini';
@override
String get closeAfterSave => 'Simpan dan tutup';
@@ -97,6 +123,16 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get conn => 'Koneksi';
@override
String get connectionDetails => 'Detail Koneksi';
@override
String get connectionStats => 'Statistik Koneksi';
@override
String get connectionStatsDesc =>
'Lihat tingkat keberhasilan koneksi server dan riwayat';
@override
String get container => 'Wadah';
@@ -146,6 +182,18 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get disconnected => 'Terputus';
@override
String get discoverSshServers => 'Temukan Server SSH';
@override
String get discoveryFailed => 'Penemuan gagal';
@override
String get discoverySettings => 'Pengaturan Penemuan';
@override
String get discoverySummary => 'Ringkasan Penemuan';
@override
String get disk => 'Disk';
@@ -198,9 +246,6 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get editVirtKeys => 'Edit kunci virtual';
@override
String get editor => 'Editor';
@override
String get editorHighlightTip =>
'Performa penyorotan kode saat ini lebih buruk, dan dapat dimatikan secara opsional untuk perbaikan.';
@@ -208,6 +253,13 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get emulator => 'Emulator';
@override
String get enableMdns => 'Aktifkan mDNS';
@override
String get enableMdnsDesc =>
'Gunakan mDNS/Bonjour untuk menemukan layanan SSH';
@override
String get encode => 'Menyandi';
@@ -240,10 +292,10 @@ class AppLocalizationsId extends AppLocalizations {
}
@override
String get followSystem => 'Ikuti sistem';
String get finishedAt => 'Selesai pada';
@override
String get font => 'Font';
String get followSystem => 'Ikuti sistem';
@override
String get fontSize => 'Ukuran huruf';
@@ -276,6 +328,13 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get highlight => 'Sorotan kode';
@override
String get homeTabs => 'Tab Beranda';
@override
String get homeTabsCustomizeDesc =>
'Sesuaikan tab mana yang muncul di halaman beranda dan urutannya';
@override
String get homeWidgetUrlConfig => 'Konfigurasi URL Widget Rumah';
@@ -296,9 +355,6 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get imagesList => 'Daftar gambar';
@override
String get init => 'Menginisialisasi';
@override
String get inner => 'Batin';
@@ -328,6 +384,12 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get keyAuth => 'Auth kunci';
@override
String get lastFailure => 'Gagal Terakhir';
@override
String get lastSuccess => 'Sukses Terakhir';
@override
String get letterCache => 'Caching huruf';
@@ -335,9 +397,6 @@ class AppLocalizationsId extends AppLocalizations {
String get letterCacheTip =>
'Direkomendasikan untuk menonaktifkan, tetapi setelah dinonaktifkan, tidak mungkin untuk memasukkan karakter CJK.';
@override
String get license => 'Lisensi';
@override
String get location => 'Lokasi';
@@ -350,10 +409,10 @@ class AppLocalizationsId extends AppLocalizations {
}
@override
String get manual => 'Manual';
String get max => 'Max';
@override
String get max => 'Max';
String get maxConcurrency => 'Konkurensi Maksimum';
@override
String get maxRetryCount => 'Jumlah penyambungan kembali server';
@@ -393,6 +452,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get newContainer => 'Wadah baru';
@override
String get noConnectionStatsData => 'Tidak ada data statistik koneksi';
@override
String get noLineChart => 'Jangan gunakan grafik garis';
@@ -463,9 +525,6 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get preferDiskAmount => 'Prioritaskan tampilan kapasitas disk';
@override
String get preview => 'Pratinjau';
@override
String get privateKey => 'Kunci Pribadi';
@@ -496,6 +555,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get reboot => 'Reboot';
@override
String get recentConnections => 'Koneksi Terkini';
@override
String get rememberPwdInMem => 'Ingat kata sandi di dalam memori';
@@ -557,6 +619,12 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get serverOrder => 'Pesanan server';
@override
String get serverTabRequired => 'Tab server tidak dapat dihapus';
@override
String get servers => 'server';
@override
String get sftpDlPrepare => 'Bersiap untuk terhubung ...';
@@ -603,6 +671,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.';
@@ -648,9 +771,6 @@ class AppLocalizationsId extends AppLocalizations {
return 'Beralih ke $val';
}
@override
String get sync => 'Sinkronisasi';
@override
String get syncTip =>
'Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.';
@@ -661,6 +781,10 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get tag => 'Tag';
@override
String get tapToStartDiscovery =>
'Tekan tombol pencarian untuk menemukan server SSH di jaringan Anda';
@override
String get temperature => 'Suhu';
@@ -693,6 +817,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get total => 'Total';
@override
String get totalAttempts => 'Total';
@override
String get traffic => 'Lalu lintas';
@@ -718,9 +845,6 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get updateServerStatusInterval => 'Interval Pembaruan Status Server';
@override
String get upload => 'Mengunggah';
@override
String get upsideDown => 'Terbalik';
@@ -746,6 +870,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get view => 'Tampilan';
@override
String get viewDetails => 'Lihat Detail';
@override
String get viewErr => 'Lihat kesalahan';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get alreadyLastDir => 'すでに最上位のディレクトリです';
@override
String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります';
@override
String get authFailTip => '認証に失敗しました。パスワード/鍵/ホスト/ユーザーなどが間違っていないか確認してください。';
@@ -43,14 +46,23 @@ class AppLocalizationsJa extends AppLocalizations {
String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新';
@override
String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。';
String get availableTabs => '利用可能なタブ';
@override
String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';
String get backupEncrypted => 'バックアップは暗号化されています';
@override
String get backupNotEncrypted => 'バックアップは暗号化されていません';
@override
String get backupPassword => 'バックアップパスワード';
@override
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
@override
String get backupPasswordSet => 'バックアップパスワードが設定されました';
@override
String get backupPasswordTip =>
'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。';
@@ -59,16 +71,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 => 'バッテリー';
@@ -80,6 +86,25 @@ class AppLocalizationsJa extends AppLocalizations {
String get bgRunTip =>
'このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。';
@override
String get clearAllStatsContent => 'すべてのサーバー接続統計を削除してもよろしいですか?この操作は元に戻せません。';
@override
String get clearAllStatsTitle => 'すべての統計をクリア';
@override
String clearServerStatsContent(String serverName) {
return 'サーバー\"$serverName\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。';
}
@override
String clearServerStatsTitle(String serverName) {
return '$serverNameの統計をクリア';
}
@override
String get clearThisServerStats => 'このサーバーの統計をクリア';
@override
String get closeAfterSave => '保存して閉じる';
@@ -92,6 +117,15 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get conn => '接続';
@override
String get connectionDetails => '接続の詳細';
@override
String get connectionStats => '接続統計';
@override
String get connectionStatsDesc => 'サーバー接続成功率と履歴を表示';
@override
String get container => 'コンテナ';
@@ -139,6 +173,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get disconnected => '接続が切断されました';
@override
String get discoverSshServers => 'SSHサーバーの発見';
@override
String get discoveryFailed => '発見に失敗';
@override
String get discoverySettings => '発見設定';
@override
String get discoverySummary => '発見の概要';
@override
String get disk => 'ディスク';
@@ -191,9 +237,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get editVirtKeys => '仮想キーを編集';
@override
String get editor => 'エディター';
@override
String get editorHighlightTip =>
'現在のコードハイライトのパフォーマンスはかなり悪いため、改善するために無効にすることを選択できます。';
@@ -201,6 +244,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get emulator => 'エミュレーター';
@override
String get enableMdns => 'mDNSを有効化';
@override
String get enableMdnsDesc => 'mDNS/BonjourでSSHサービスを発見';
@override
String get encode => 'エンコード';
@@ -233,10 +282,10 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
String get followSystem => 'システムに従う';
String get finishedAt => '完了時刻';
@override
String get font => 'フォント';
String get followSystem => 'システムに従う';
@override
String get fontSize => 'フォントサイズ';
@@ -269,6 +318,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get highlight => 'コードハイライト';
@override
String get homeTabs => 'ホームタブ';
@override
String get homeTabsCustomizeDesc => 'ホームページに表示するタブとその順序をカスタマイズします';
@override
String get homeWidgetUrlConfig => 'ホームウィジェットURL設定';
@@ -289,9 +344,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get imagesList => 'イメージリスト';
@override
String get init => '初期化する';
@override
String get inner => '内蔵';
@@ -320,15 +372,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get keyAuth => 'キー認証';
@override
String get lastFailure => '最後の失敗';
@override
String get lastSuccess => '最後の成功';
@override
String get letterCache => '文字キャッシング';
@override
String get letterCacheTip => '無効にすることを推奨しますが、無効にした後はCJK文字を入力することができなくなります。';
@override
String get license => 'オープンソースライセンス';
@override
String get location => '場所';
@@ -341,10 +396,10 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
String get manual => 'マニュアル';
String get max => '最大';
@override
String get max => '最大';
String get maxConcurrency => '最大同時実行数';
@override
String get maxRetryCount => 'サーバーの再接続試行回数';
@@ -384,6 +439,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get newContainer => '新しいコンテナを作成';
@override
String get noConnectionStatsData => '接続統計データがありません';
@override
String get noLineChart => '折れ線グラフを使用しない';
@@ -449,9 +507,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get preferDiskAmount => 'ディスク容量を優先的に表示';
@override
String get preview => 'プレビュー';
@override
String get privateKey => '秘密鍵';
@@ -481,6 +536,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get reboot => '再起動';
@override
String get recentConnections => '最近の接続';
@override
String get rememberPwdInMem => 'メモリにパスワードを記憶する';
@@ -541,6 +599,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get serverOrder => 'サーバー順序';
@override
String get serverTabRequired => 'サーバータブは削除できません';
@override
String get servers => 'サーバー';
@override
String get sftpDlPrepare => 'サーバーへの接続を準備中...';
@@ -587,6 +651,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を開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。';
@@ -631,9 +745,6 @@ class AppLocalizationsJa extends AppLocalizations {
return '$valに切り替える';
}
@override
String get sync => '同期する';
@override
String get syncTip => '再起動が必要な場合があります。一部の変更はその後に有効になります。';
@@ -643,6 +754,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get tag => 'タグ';
@override
String get tapToStartDiscovery => '検索ボタンをタップしてネットワーク上のSSHサーバーを発見';
@override
String get temperature => '温度';
@@ -675,6 +789,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get total => '合計';
@override
String get totalAttempts => '総計';
@override
String get traffic => 'トラフィック';
@@ -700,9 +817,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get updateServerStatusInterval => 'サーバー状態の更新間隔';
@override
String get upload => 'アップロード';
@override
String get upsideDown => '上下逆転';
@@ -727,6 +841,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get view => 'ビュー';
@override
String get viewDetails => '詳細を表示';
@override
String get viewErr => 'エラーを表示';

View File

@@ -28,6 +28,10 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get alreadyLastDir => 'Al in de laatst gebruikte map.';
@override
String get atLeastOneTab =>
'Er moet minimaal één tabblad worden geselecteerd';
@override
String get authFailTip =>
'Authenticatie mislukt, controleer of het wachtwoord/sleutel/host/gebruiker, enz., incorrect zijn.';
@@ -46,15 +50,23 @@ 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 availableTabs => 'Beschikbare tabbladen';
@override
String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';
String get backupEncrypted => 'Back-up is versleuteld';
@override
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 +75,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';
@@ -84,6 +91,26 @@ class AppLocalizationsNl extends AppLocalizations {
String get 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\".';
@override
String get clearAllStatsContent =>
'Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
@override
String get clearAllStatsTitle => 'Alle statistieken wissen';
@override
String clearServerStatsContent(String serverName) {
return 'Weet u zeker dat u de verbindingsstatistieken voor server \"$serverName\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Statistieken van $serverName wissen';
}
@override
String get clearThisServerStats => 'Statistieken van deze server wissen';
@override
String get closeAfterSave => 'Opslaan en sluiten';
@@ -97,6 +124,16 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get conn => 'Verbinding';
@override
String get connectionDetails => 'Verbindingsdetails';
@override
String get connectionStats => 'Verbindingsstatistieken';
@override
String get connectionStatsDesc =>
'Bekijk server verbindingssucces ratio en geschiedenis';
@override
String get container => 'Container';
@@ -146,6 +183,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get disconnected => 'Verbroken';
@override
String get discoverSshServers => 'SSH-servers ontdekken';
@override
String get discoveryFailed => 'Ontdekking mislukt';
@override
String get discoverySettings => 'Ontdekkingsinstellingen';
@override
String get discoverySummary => 'Ontdekkingssamenvatting';
@override
String get disk => 'Schijf';
@@ -198,9 +247,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get editVirtKeys => 'Virtuele toetsen bewerken';
@override
String get editor => 'Editor';
@override
String get editorHighlightTip =>
'De huidige codehighlighting-prestaties zijn slechter en kunnen optioneel worden uitgeschakeld om te verbeteren.';
@@ -208,6 +254,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get emulator => 'Emulator';
@override
String get enableMdns => 'mDNS inschakelen';
@override
String get enableMdnsDesc =>
'Gebruik mDNS/Bonjour om SSH-services te ontdekken';
@override
String get encode => 'Coderen';
@@ -240,10 +293,10 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get followSystem => 'Volg systeem';
String get finishedAt => 'Voltooid om';
@override
String get font => 'Lettertype';
String get followSystem => 'Volg systeem';
@override
String get fontSize => 'Lettergrootte';
@@ -276,6 +329,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get highlight => 'Code-highlight';
@override
String get homeTabs => 'Home-tabbladen';
@override
String get homeTabsCustomizeDesc =>
'Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde';
@override
String get homeWidgetUrlConfig => 'Home-widget-url configureren';
@@ -296,9 +356,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get imagesList => 'Lijst met afbeeldingen';
@override
String get init => 'Initialiseren';
@override
String get inner => 'Intern';
@@ -328,6 +385,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get keyAuth => 'Sleutelauthenticatie';
@override
String get lastFailure => 'Laatst gefaald';
@override
String get lastSuccess => 'Laatst succesvol';
@override
String get letterCache => 'Lettercaching';
@@ -335,9 +398,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get letterCacheTip =>
'Aanbevolen om uit te schakelen, maar na het uitschakelen is het niet mogelijk om CJK-tekens in te voeren.';
@override
String get license => 'Licentie';
@override
String get location => 'Locatie';
@@ -350,10 +410,10 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get manual => 'Handleiding';
String get max => 'max';
@override
String get max => 'max';
String get maxConcurrency => 'Maximale gelijktijdigheid';
@override
String get maxRetryCount => 'Aantal serverherverbindingen';
@@ -393,6 +453,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get newContainer => 'Nieuwe container';
@override
String get noConnectionStatsData => 'Geen verbindingsstatistiekgegevens';
@override
String get noLineChart => 'lijndiagrammen gebruiken';
@@ -464,9 +527,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get preferDiskAmount =>
'Geef de schijfcapaciteit prioriteit bij weergave';
@override
String get preview => 'Voorbeeld';
@override
String get privateKey => 'Privésleutel';
@@ -497,6 +557,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get reboot => 'Herstart';
@override
String get recentConnections => 'Recente verbindingen';
@override
String get rememberPwdInMem => 'Wachtwoord onthouden in geheugen';
@@ -558,6 +621,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get serverOrder => 'Servervolgorde';
@override
String get serverTabRequired => 'Servertabblad kan niet worden verwijderd';
@override
String get servers => 'servers';
@override
String get sftpDlPrepare => 'Voorbereiden om verbinding te maken...';
@@ -605,6 +674,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.';
@@ -652,9 +776,6 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Overschakelen naar $val';
}
@override
String get sync => 'Sync';
@override
String get syncTip =>
'Een herstart kan nodig zijn voor sommige wijzigingen om van kracht te worden.';
@@ -665,6 +786,10 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get tag => 'Labels';
@override
String get tapToStartDiscovery =>
'Tik op de zoekknop om SSH-servers op uw netwerk te ontdekken';
@override
String get temperature => 'Temperatuur';
@@ -697,6 +822,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get total => 'Totaal';
@override
String get totalAttempts => 'Totaal';
@override
String get traffic => 'Verkeer';
@@ -723,9 +851,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get updateServerStatusInterval =>
'Interne server status bijwerking interval';
@override
String get upload => 'Upload';
@override
String get upsideDown => 'Ondersteboven';
@@ -751,6 +876,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get view => 'Weergave';
@override
String get viewDetails => 'Details bekijken';
@override
String get viewErr => 'Zie foutmelding';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get alreadyLastDir => 'Já é o diretório mais alto';
@override
String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada';
@override
String get authFailTip =>
'Autenticação falhou, por favor verifique se a senha/chave/host/usuário, etc., estão incorretos.';
@@ -46,16 +49,23 @@ 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 availableTabs => 'Abas disponíveis';
@override
String get backupVersionNotMatch =>
'Versão de backup não compatível, não é possível restaurar';
String get backupEncrypted => 'Backup está criptografado';
@override
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 +74,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';
@@ -85,6 +91,26 @@ class AppLocalizationsPt extends AppLocalizations {
String get 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\'.';
@override
String get clearAllStatsContent =>
'Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.';
@override
String get clearAllStatsTitle => 'Limpar todas as estatísticas';
@override
String clearServerStatsContent(String serverName) {
return 'Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"$serverName\"? Esta ação não pode ser desfeita.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Limpar estatísticas de $serverName';
}
@override
String get clearThisServerStats => 'Limpar estatísticas deste servidor';
@override
String get closeAfterSave => 'Salvar e fechar';
@@ -97,6 +123,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get conn => 'Conectar';
@override
String get connectionDetails => 'Detalhes da conexão';
@override
String get connectionStats => 'Estatísticas de conexão';
@override
String get connectionStatsDesc =>
'Ver taxa de sucesso de conexão do servidor e histórico';
@override
String get container => 'Contêiner';
@@ -146,6 +182,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get disconnected => 'Desconectado';
@override
String get discoverSshServers => 'Descobrir servidores SSH';
@override
String get discoveryFailed => 'Descoberta falhou';
@override
String get discoverySettings => 'Configurações de descoberta';
@override
String get discoverySummary => 'Resumo da descoberta';
@override
String get disk => 'Disco';
@@ -198,9 +246,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get editVirtKeys => 'Editar teclas virtuais';
@override
String get editor => 'Editor';
@override
String get editorHighlightTip =>
'O desempenho do destaque de código atualmente é ruim, pode optar por desativá-lo para melhorar.';
@@ -208,6 +253,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get emulator => 'Emulador';
@override
String get enableMdns => 'Ativar mDNS';
@override
String get enableMdnsDesc => 'Usar mDNS/Bonjour para descobrir serviços SSH';
@override
String get encode => 'Codificar';
@@ -240,10 +291,10 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get followSystem => 'Seguir sistema';
String get finishedAt => 'Terminado em';
@override
String get font => 'Fonte';
String get followSystem => 'Seguir sistema';
@override
String get fontSize => 'Tamanho da fonte';
@@ -276,6 +327,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get highlight => 'Destaque de código';
@override
String get homeTabs => 'Abas iniciais';
@override
String get homeTabsCustomizeDesc =>
'Personalize quais abas aparecem na página inicial e sua ordem';
@override
String get homeWidgetUrlConfig =>
'Configuração de URL do widget da tela inicial';
@@ -297,9 +355,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get imagesList => 'Lista de imagens';
@override
String get init => 'Inicializar';
@override
String get inner => 'Interno';
@@ -328,6 +383,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get keyAuth => 'Autenticação por chave';
@override
String get lastFailure => 'Última falha';
@override
String get lastSuccess => 'Último sucesso';
@override
String get letterCache => 'Cache de letras';
@@ -335,9 +396,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get letterCacheTip =>
'Recomendado desativar, mas após desativar, será impossível inserir caracteres CJK.';
@override
String get license => 'Licença de código aberto';
@override
String get location => 'Localização';
@@ -350,10 +408,10 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get manual => 'Manual';
String get max => 'Máximo';
@override
String get max => 'Máximo';
String get maxConcurrency => 'Concorrência máxima';
@override
String get maxRetryCount =>
@@ -394,6 +452,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get newContainer => 'Novo contêiner';
@override
String get noConnectionStatsData => 'Não há dados de estatísticas de conexão';
@override
String get noLineChart => 'Não usar gráficos de linha';
@@ -464,9 +525,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get preferDiskAmount => 'Priorizar a exibição da capacidade do disco';
@override
String get preview => 'Pré-visualização';
@override
String get privateKey => 'Chave privada';
@@ -497,6 +555,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get reboot => 'Reiniciar';
@override
String get recentConnections => 'Conexões recentes';
@override
String get rememberPwdInMem => 'Lembrar senha na memória';
@@ -558,6 +619,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get serverOrder => 'Ordem do servidor';
@override
String get serverTabRequired => 'A aba do servidor não pode ser removida';
@override
String get servers => 'servidores';
@override
String get sftpDlPrepare => 'Preparando para conectar ao servidor...';
@@ -604,6 +671,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.';
@@ -650,9 +772,6 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Mudar para $val';
}
@override
String get sync => 'Sincronizar';
@override
String get syncTip =>
'Pode ser necessário reiniciar para algumas mudanças surtirem efeito.';
@@ -663,6 +782,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get tag => 'Tag';
@override
String get tapToStartDiscovery =>
'Toque no botão de pesquisa para descobrir servidores SSH na sua rede';
@override
String get temperature => 'Temperatura';
@@ -695,6 +818,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get total => 'Total';
@override
String get totalAttempts => 'Total';
@override
String get traffic => 'Tráfego';
@@ -721,9 +847,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get updateServerStatusInterval =>
'Intervalo de atualização do estado do servidor';
@override
String get upload => 'Upload';
@override
String get upsideDown => 'Inverter verticalmente';
@@ -749,6 +872,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get view => 'Visualização';
@override
String get viewDetails => 'Ver detalhes';
@override
String get viewErr => 'Ver erro';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get alreadyLastDir => 'Уже в корневом каталоге';
@override
String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка';
@override
String get authFailTip =>
'Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.';
@@ -46,16 +49,23 @@ class AppLocalizationsRu extends AppLocalizations {
'Автоматическое обновление виджета на главном экране';
@override
String get backupTip =>
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
String get availableTabs => 'Доступные вкладки';
@override
String get backupVersionNotMatch =>
'Версия резервной копии не совпадает, восстановление невозможно';
String get backupEncrypted => 'Резервная копия зашифрована';
@override
String get backupNotEncrypted => 'Резервная копия не зашифрована';
@override
String get backupPassword => 'Пароль резервной копии';
@override
String get backupPasswordRemoved => 'Пароль резервной копии удален';
@override
String get backupPasswordSet => 'Пароль резервной копии установлен';
@override
String get backupPasswordTip =>
'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.';
@@ -64,16 +74,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 => 'Батарея';
@@ -85,6 +91,26 @@ class AppLocalizationsRu extends AppLocalizations {
String get bgRunTip =>
'Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».';
@override
String get clearAllStatsContent =>
'Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.';
@override
String get clearAllStatsTitle => 'Очистить всю статистику';
@override
String clearServerStatsContent(String serverName) {
return 'Вы уверены, что хотите очистить статистику соединений для сервера \"$serverName\"? Это действие не может быть отменено.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Очистить статистику $serverName';
}
@override
String get clearThisServerStats => 'Очистить статистику этого сервера';
@override
String get closeAfterSave => 'Сохранить и закрыть';
@@ -97,6 +123,16 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get conn => 'Подключение';
@override
String get connectionDetails => 'Детали соединения';
@override
String get connectionStats => 'Статистика соединений';
@override
String get connectionStatsDesc =>
'Просмотр коэффициента успешности подключения к серверу и истории';
@override
String get container => 'Контейнер';
@@ -146,6 +182,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get disconnected => 'Отключено';
@override
String get discoverSshServers => 'Обнаружить SSH серверы';
@override
String get discoveryFailed => 'Обнаружение не удалось';
@override
String get discoverySettings => 'Настройки обнаружения';
@override
String get discoverySummary => 'Сводка обнаружения';
@override
String get disk => 'Диск';
@@ -198,9 +246,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get editVirtKeys => 'Редактировать виртуальные клавиши';
@override
String get editor => 'Редактор';
@override
String get editorHighlightTip =>
'Текущая производительность подсветки кода неудовлетворительна, можно отключить для улучшения.';
@@ -208,6 +253,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get emulator => 'Эмулятор';
@override
String get enableMdns => 'Включить mDNS';
@override
String get enableMdnsDesc =>
'Использовать mDNS/Bonjour для обнаружения SSH служб';
@override
String get encode => 'Кодировать';
@@ -240,10 +292,10 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
String get followSystem => 'Следовать за системой';
String get finishedAt => 'Завершено в';
@override
String get font => 'Шрифт';
String get followSystem => 'Следовать за системой';
@override
String get fontSize => 'Размер шрифта';
@@ -276,6 +328,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get highlight => 'Подсветка кода';
@override
String get homeTabs => 'Вкладки дома';
@override
String get homeTabsCustomizeDesc =>
'Настройте, какие вкладки появляются на главной странице и их порядок';
@override
String get homeWidgetUrlConfig => 'Конфигурация URL виджета домашнего экрана';
@@ -296,9 +355,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get imagesList => 'Список образов';
@override
String get init => 'Инициализировать';
@override
String get inner => 'Встроенный';
@@ -328,6 +384,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get keyAuth => 'Аутентификация по ключу';
@override
String get lastFailure => 'Последний сбой';
@override
String get lastSuccess => 'Последний успех';
@override
String get letterCache => 'Кэширование букв';
@@ -335,9 +397,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get letterCacheTip =>
'Рекомендуется отключить, но после отключения будет невозможно вводить символы CJK.';
@override
String get license => 'Лицензия';
@override
String get location => 'Местоположение';
@@ -350,10 +409,10 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
String get manual => 'Вручную';
String get max => 'максимум';
@override
String get max => 'максимум';
String get maxConcurrency => 'Максимальная параллельность';
@override
String get maxRetryCount =>
@@ -395,6 +454,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get newContainer => 'Создать контейнер';
@override
String get noConnectionStatsData => 'Нет данных статистики соединений';
@override
String get noLineChart => 'Не использовать линейные графики';
@@ -465,9 +527,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get preferDiskAmount => 'Приоритетное отображение объёма диска';
@override
String get preview => 'Предпросмотр';
@override
String get privateKey => 'Приватный ключ';
@@ -498,6 +557,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get reboot => 'Перезагрузка';
@override
String get recentConnections => 'Недавние соединения';
@override
String get rememberPwdInMem => 'Запомнить пароль в памяти';
@@ -560,6 +622,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get serverOrder => 'Порядок серверов';
@override
String get serverTabRequired => 'Вкладку сервера нельзя удалить';
@override
String get servers => 'серверов';
@override
String get sftpDlPrepare => 'Подготовка подключения...';
@@ -607,6 +675,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. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.';
@@ -653,9 +775,6 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Переключиться на $val';
}
@override
String get sync => 'Синхронизировать';
@override
String get syncTip =>
'Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.';
@@ -666,6 +785,10 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get tag => 'Теги';
@override
String get tapToStartDiscovery =>
'Нажмите кнопку поиска, чтобы обнаружить SSH серверы в вашей сети';
@override
String get temperature => 'Температура';
@@ -698,6 +821,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get total => 'Всего';
@override
String get totalAttempts => 'Общее';
@override
String get traffic => 'Трафик';
@@ -724,9 +850,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get updateServerStatusInterval =>
'Интервал обновления статуса сервера';
@override
String get upload => 'Загрузить';
@override
String get upsideDown => 'Перевернуть';
@@ -752,6 +875,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get view => 'Вид';
@override
String get viewDetails => 'Просмотр деталей';
@override
String get viewErr => 'Просмотр ошибок';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get alreadyLastDir => 'Zaten son dizindesiniz.';
@override
String get atLeastOneTab => 'En az bir sekme seçilmelidir';
@override
String get authFailTip =>
'Kimlik doğrulama başarısız oldu, lütfen kimlik bilgilerinin doğru olup olmadığını kontrol edin';
@@ -45,15 +48,23 @@ 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 availableTabs => 'Mevcut Sekmeler';
@override
String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';
String get backupEncrypted => 'Yedekleme şifrelenmiş';
@override
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 +73,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';
@@ -83,6 +89,26 @@ class AppLocalizationsTr extends AppLocalizations {
String get 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.';
@override
String get clearAllStatsContent =>
'Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
@override
String get clearAllStatsTitle => 'Tüm İstatistikleri Temizle';
@override
String clearServerStatsContent(String serverName) {
return '\"$serverName\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
}
@override
String clearServerStatsTitle(String serverName) {
return '$serverName İstatistiklerini Temizle';
}
@override
String get clearThisServerStats => 'Bu Sunucu İstatistiklerini Temizle';
@override
String get closeAfterSave => 'Kaydet ve kapat';
@@ -96,6 +122,16 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get conn => 'Bağlantı';
@override
String get connectionDetails => 'Bağlantı Detayları';
@override
String get connectionStats => 'Bağlantı İstatistikleri';
@override
String get connectionStatsDesc =>
'Sunucu bağlantı başarı oranını ve geçmişi görüntüle';
@override
String get container => 'Konteyner';
@@ -145,6 +181,18 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get disconnected => 'Bağlantı kesildi';
@override
String get discoverSshServers => 'SSH Sunucularını Keşfet';
@override
String get discoveryFailed => 'Keşif başarısız';
@override
String get discoverySettings => 'Keşif Ayarları';
@override
String get discoverySummary => 'Keşif Özeti';
@override
String get disk => 'Disk';
@@ -197,9 +245,6 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get editVirtKeys => 'Sanal tuşları düzenle';
@override
String get editor => 'Düzenleyici';
@override
String get editorHighlightTip =>
'Mevcut kod vurgulama performansı ideal değil ve isteğe bağlı olarak kapatılabilir.';
@@ -207,6 +252,13 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get emulator => 'Emülatör';
@override
String get enableMdns => 'mDNS\'yi Etkinleştir';
@override
String get enableMdnsDesc =>
'SSH hizmetlerini keşfetmek için mDNS/Bonjour kullan';
@override
String get encode => 'Kodla';
@@ -239,10 +291,10 @@ class AppLocalizationsTr extends AppLocalizations {
}
@override
String get followSystem => 'Sistemi takip et';
String get finishedAt => 'Tamamlandı:';
@override
String get font => 'Yazı tipi';
String get followSystem => 'Sistemi takip et';
@override
String get fontSize => 'Yazı tipi boyutu';
@@ -275,6 +327,13 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get highlight => 'Kod vurgulama';
@override
String get homeTabs => 'Ana Sayfa Sekmeleri';
@override
String get homeTabsCustomizeDesc =>
'Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin';
@override
String get homeWidgetUrlConfig => 'Ana ekran bileşeni URL\'sini yapılandır';
@@ -295,9 +354,6 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get imagesList => 'Görüntü listesi';
@override
String get init => 'Başlat';
@override
String get inner => 'İç';
@@ -327,6 +383,12 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get keyAuth => 'Anahtar Kimlik Doğrulama';
@override
String get lastFailure => 'Son Başarısızlık';
@override
String get lastSuccess => 'Son Başarı';
@override
String get letterCache => 'Harf önbelleği';
@@ -334,9 +396,6 @@ class AppLocalizationsTr extends AppLocalizations {
String get letterCacheTip =>
'Devre dışı bırakılması önerilir, ancak devre dışı bırakıldığında CJK karakterlerini girmek mümkün olmayacaktır.';
@override
String get license => 'Lisans';
@override
String get location => 'Konum';
@@ -349,10 +408,10 @@ class AppLocalizationsTr extends AppLocalizations {
}
@override
String get manual => 'Manuel';
String get max => 'maks';
@override
String get max => 'maks';
String get maxConcurrency => 'Maksimum Eşzamanlılık';
@override
String get maxRetryCount => 'Sunucu yeniden bağlantı sayısı';
@@ -392,6 +451,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get newContainer => 'Yeni konteyner';
@override
String get noConnectionStatsData => 'Bağlantı istatistik verisi yok';
@override
String get noLineChart => 'Çizgi grafikleri kullanma';
@@ -462,9 +524,6 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get preferDiskAmount => 'Disk kapasitesini öncelikli olarak göster';
@override
String get preview => 'Önizleme';
@override
String get privateKey => 'Özel Anahtar';
@@ -495,6 +554,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get reboot => 'Yeniden başlat';
@override
String get recentConnections => 'Son Bağlantılar';
@override
String get rememberPwdInMem => 'Şifreyi bellekte hatırla';
@@ -556,6 +618,12 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get serverOrder => 'Sunucu sırası';
@override
String get serverTabRequired => 'Sunucu sekmesi kaldırılamaz';
@override
String get servers => 'sunucu';
@override
String get sftpDlPrepare => 'Bağlantı hazırlanıyor...';
@@ -603,6 +671,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.';
@@ -649,9 +771,6 @@ class AppLocalizationsTr extends AppLocalizations {
return '$val\'a geç';
}
@override
String get sync => 'Senkronize et';
@override
String get syncTip =>
'Bazı değişikliklerin etkili olması için yeniden başlatma gerekebilir.';
@@ -662,6 +781,10 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get tag => 'Etiketler';
@override
String get tapToStartDiscovery =>
'ınızdaki SSH sunucularını keşfetmek için arama düğmesine dokunun';
@override
String get temperature => 'Sıcaklık';
@@ -694,6 +817,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get total => 'Toplam';
@override
String get totalAttempts => 'Toplam';
@override
String get traffic => 'Trafik';
@@ -719,9 +845,6 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get updateServerStatusInterval => 'Sunucu durumu güncelleme aralığı';
@override
String get upload => 'Yükle';
@override
String get upsideDown => 'Başaşağı';
@@ -747,6 +870,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get view => 'Görünüm';
@override
String get viewDetails => 'Detayları Görüntüle';
@override
String get viewErr => 'Hatayı gör';

View File

@@ -27,6 +27,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get alreadyLastDir => 'Вже в останньому каталозі.';
@override
String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку';
@override
String get authFailTip =>
'Авторизація не вдалася, будь ласка, перевірте правильність облікових даних';
@@ -46,16 +49,23 @@ class AppLocalizationsUk extends AppLocalizations {
'Автоматичне оновлення віджетів на головному екрані';
@override
String get backupTip =>
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
String get availableTabs => 'Доступні вкладки';
@override
String get backupVersionNotMatch =>
'Версія резервного копіювання не збіглася.';
String get backupEncrypted => 'Резервна копія зашифрована';
@override
String get backupNotEncrypted => 'Резервна копія не зашифрована';
@override
String get backupPassword => 'Пароль резервного копіювання';
@override
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
@override
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
@override
String get backupPasswordTip =>
'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.';
@@ -64,16 +74,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 => 'Акумулятор';
@@ -85,6 +91,26 @@ class AppLocalizationsUk extends AppLocalizations {
String get bgRunTip =>
'Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".';
@override
String get clearAllStatsContent =>
'Ви впевнені, що хочете очистити всю статистику з\'єднань сервера? Цю дію не можна скасувати.';
@override
String get clearAllStatsTitle => 'Очистити всю статистику';
@override
String clearServerStatsContent(String serverName) {
return 'Ви впевнені, що хочете очистити статистику з\'єднань для сервера \"$serverName\"? Цю дію не можна скасувати.';
}
@override
String clearServerStatsTitle(String serverName) {
return 'Очистити статистику $serverName';
}
@override
String get clearThisServerStats => 'Очистити статистику цього сервера';
@override
String get closeAfterSave => 'Зберегти та закрити';
@@ -98,6 +124,16 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get conn => 'З\'єднання';
@override
String get connectionDetails => 'Деталі з\'єднання';
@override
String get connectionStats => 'Статистика з\'єднань';
@override
String get connectionStatsDesc =>
'Переглянути коефіцієнт успішності підключення до сервера та історію';
@override
String get container => 'Контейнер';
@@ -147,6 +183,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get disconnected => 'Відключено';
@override
String get discoverSshServers => 'Виявити SSH сервери';
@override
String get discoveryFailed => 'Виявлення не вдалось';
@override
String get discoverySettings => 'Налаштування виявлення';
@override
String get discoverySummary => 'Підсумок виявлення';
@override
String get disk => 'Диск';
@@ -199,9 +247,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get editVirtKeys => 'Редагувати віртуальні клавіші';
@override
String get editor => 'Редактор';
@override
String get editorHighlightTip =>
'Поточна підсвітка коду не ідеальна і може бути вимкнена для покращення.';
@@ -209,6 +254,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get emulator => 'Емулятор';
@override
String get enableMdns => 'Увімкнути mDNS';
@override
String get enableMdnsDesc =>
'Використовувати mDNS/Bonjour для виявлення SSH сервісів';
@override
String get encode => 'Кодувати';
@@ -241,10 +293,10 @@ class AppLocalizationsUk extends AppLocalizations {
}
@override
String get followSystem => 'Слідувати системі';
String get finishedAt => 'Завершено о';
@override
String get font => 'Шрифт';
String get followSystem => 'Слідувати системі';
@override
String get fontSize => 'Розмір шрифту';
@@ -277,6 +329,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get highlight => 'Підсвітка коду';
@override
String get homeTabs => 'Домашні вкладки';
@override
String get homeTabsCustomizeDesc =>
'Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок';
@override
String get homeWidgetUrlConfig =>
'Налаштувати URL віджета на головному екрані';
@@ -298,9 +357,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get imagesList => 'Список зображень';
@override
String get init => 'Ініціалізувати';
@override
String get inner => 'Внутрішній';
@@ -330,6 +386,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get keyAuth => 'Аутентифікація ключем';
@override
String get lastFailure => 'Остання помилка';
@override
String get lastSuccess => 'Останній успіх';
@override
String get letterCache => 'Кешування букв';
@@ -337,9 +399,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get letterCacheTip =>
'Рекомендується відключити, але після вимкнення стане неможливим введення CJK (китайських, японських, корейських) символів.';
@override
String get license => 'Ліцензія';
@override
String get location => 'Місцезнаходження';
@@ -352,10 +411,10 @@ class AppLocalizationsUk extends AppLocalizations {
}
@override
String get manual => 'Посібник';
String get max => 'макс.';
@override
String get max => 'макс.';
String get maxConcurrency => 'Максимальна паралельність';
@override
String get maxRetryCount =>
@@ -397,6 +456,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get newContainer => 'Новий контейнер';
@override
String get noConnectionStatsData => 'Немає даних статистики з\'єднань';
@override
String get noLineChart => 'Не використовувати лінійні діаграми';
@@ -467,9 +529,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get preferDiskAmount => 'Пріоритетно показувати ємність диска';
@override
String get preview => 'Попередній перегляд';
@override
String get privateKey => 'Приватний ключ';
@@ -500,6 +559,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get reboot => 'Перезавантажити';
@override
String get recentConnections => 'Останні з\'єднання';
@override
String get rememberPwdInMem => 'Запам\'ятати пароль у пам\'яті';
@@ -561,6 +623,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get serverOrder => 'Порядок сервера';
@override
String get serverTabRequired => 'Вкладку сервера не можна видалити';
@override
String get servers => 'серверів';
@override
String get sftpDlPrepare => 'Підготовка до підключення...';
@@ -608,6 +676,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. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.';
@@ -654,9 +776,6 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Переключитися на $val';
}
@override
String get sync => 'Синхронізація';
@override
String get syncTip =>
'Може знадобитися перезапуск, щоб деякі зміни набрали чинності.';
@@ -667,6 +786,10 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get tag => 'Теги';
@override
String get tapToStartDiscovery =>
'Натисніть кнопку пошуку, щоб виявити SSH сервери у вашій мережі';
@override
String get temperature => 'Температура';
@@ -699,6 +822,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get total => 'Всього';
@override
String get totalAttempts => 'Загальна кількість';
@override
String get traffic => 'Трафік';
@@ -724,9 +850,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get updateServerStatusInterval => 'Інтервал оновлення статусу сервера';
@override
String get upload => 'Завантаження';
@override
String get upsideDown => 'Доверху дном';
@@ -752,6 +875,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get view => 'Переглянути';
@override
String get viewDetails => 'Переглянути деталі';
@override
String get viewErr => 'Переглянути помилку';

View File

@@ -26,6 +26,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get alreadyLastDir => '已是顶级目录';
@override
String get atLeastOneTab => '至少需要选择一个标签';
@override
String get authFailTip => '认证失败,请检查连接信息是否正确';
@@ -42,19 +45,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get autoUpdateHomeWidget => '自动更新桌面小部件';
@override
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
@override
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
@override
String get backupPassword => '备份密码';
@override
String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';
@override
String get backupPasswordWrong => '备份密码错误';
String get availableTabs => '可用标签';
@override
String get backupEncrypted => '备份已加密';
@@ -63,11 +54,26 @@ class AppLocalizationsZh extends AppLocalizations {
String get backupNotEncrypted => '备份未加密';
@override
String get backupPasswordSet => '备份密码已设置';
String get backupPassword => '备份密码';
@override
String get backupPasswordRemoved => '备份密码已移除';
@override
String get backupPasswordSet => '备份密码已设置';
@override
String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';
@override
String get backupPasswordWrong => '备份密码错误';
@override
String get backupTip => '导出数据可通过密码加密,请妥善保管。';
@override
String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
@override
String get battery => '电池';
@@ -78,6 +84,25 @@ class AppLocalizationsZh extends AppLocalizations {
String get bgRunTip =>
'此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”MIUI / HyperOS 请将省电策略改为“无限制”。';
@override
String get clearAllStatsContent => '确定要清空所有服务器的连接统计数据吗?此操作无法撤销。';
@override
String get clearAllStatsTitle => '清空所有统计';
@override
String clearServerStatsContent(String serverName) {
return '确定要清空服务器 \"$serverName\" 的连接统计数据吗?此操作无法撤销。';
}
@override
String clearServerStatsTitle(String serverName) {
return '清空 $serverName 统计';
}
@override
String get clearThisServerStats => '清空此服务器统计';
@override
String get closeAfterSave => '保存后关闭';
@@ -90,6 +115,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get conn => '连接';
@override
String get connectionDetails => '连接详情';
@override
String get connectionStats => '连接统计';
@override
String get connectionStatsDesc => '查看服务器连接成功率和历史记录';
@override
String get container => '容器';
@@ -137,6 +171,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get disconnected => '已断开连接';
@override
String get discoverSshServers => '发现SSH服务器';
@override
String get discoveryFailed => '发现失败';
@override
String get discoverySettings => '发现设置';
@override
String get discoverySummary => '发现摘要';
@override
String get disk => '磁盘';
@@ -188,15 +234,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get editVirtKeys => '编辑虚拟按键';
@override
String get editor => '编辑器';
@override
String get editorHighlightTip => '代码高亮功能可能影响性能,可选择关闭。';
@override
String get emulator => '模拟器';
@override
String get enableMdns => '启用mDNS';
@override
String get enableMdnsDesc => '使用mDNS/Bonjour发现SSH服务';
@override
String get encode => '编码';
@@ -228,10 +277,10 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
String get followSystem => '跟随系统';
String get finishedAt => '完成于';
@override
String get font => '字体';
String get followSystem => '跟随系统';
@override
String get fontSize => '字体大小';
@@ -263,6 +312,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get highlight => '代码高亮';
@override
String get homeTabs => '主页标签';
@override
String get homeTabsCustomizeDesc => '自定义主页上显示的标签及其顺序';
@override
String get homeWidgetUrlConfig => '桌面部件链接配置';
@@ -283,9 +338,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get imagesList => '镜像列表';
@override
String get init => '初始化';
@override
String get inner => '内置';
@@ -314,15 +366,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get keyAuth => '密钥认证';
@override
String get lastFailure => '最后失败';
@override
String get lastSuccess => '最后成功';
@override
String get letterCache => '输入法字符缓存';
@override
String get letterCacheTip => '推荐关闭,但是关闭后无法输入 CJK 等文字';
@override
String get license => '证书';
@override
String get location => '位置';
@@ -335,10 +390,10 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
String get manual => '手动';
String get max => '最大';
@override
String get max => '最大';
String get maxConcurrency => '最大并发数';
@override
String get maxRetryCount => '服务器尝试重连次数';
@@ -378,6 +433,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get newContainer => '新建容器';
@override
String get noConnectionStatsData => '暂无连接统计数据';
@override
String get noLineChart => '不使用折线图';
@@ -443,9 +501,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get preferDiskAmount => '优先显示硬盘容量';
@override
String get preview => '预览';
@override
String get privateKey => '私钥';
@@ -473,6 +528,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get reboot => '重启';
@override
String get recentConnections => '最近连接记录';
@override
String get rememberPwdInMem => '在内存中记住密码';
@@ -533,6 +591,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get serverOrder => '服务器顺序';
@override
String get serverTabRequired => '服务器标签不能被移除';
@override
String get servers => '服务器';
@override
String get sftpDlPrepare => '准备连接至服务器...';
@@ -578,6 +642,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。剪切板按钮会在有选中文字时复制内容在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。';
@@ -622,9 +735,6 @@ class AppLocalizationsZh extends AppLocalizations {
return '切换到 $val';
}
@override
String get sync => '同步';
@override
String get syncTip => '可能需要重新启动,某些更改才能生效。';
@@ -634,6 +744,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get tag => '标签';
@override
String get tapToStartDiscovery => '点击搜索按钮发现网络中的SSH服务器';
@override
String get temperature => '温度';
@@ -664,6 +777,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get total => '总共';
@override
String get totalAttempts => '总次数';
@override
String get traffic => '流量';
@@ -688,9 +804,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get updateServerStatusInterval => '服务器状态刷新间隔';
@override
String get upload => '上传';
@override
String get upsideDown => '上下交换';
@@ -715,6 +828,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get view => '视图';
@override
String get viewDetails => '查看详情';
@override
String get viewErr => '查看错误';
@@ -778,6 +894,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get alreadyLastDir => '已是頂層目錄';
@override
String get atLeastOneTab => '至少需要選擇一個標籤';
@override
String get authFailTip => '認證失敗,請檢查連線資訊是否正確';
@@ -794,19 +913,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get autoUpdateHomeWidget => '自動更新桌面小工具';
@override
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
@override
String get backupVersionNotMatch => '備份版本不相容,無法還原';
@override
String get backupPassword => '備份密碼';
@override
String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';
@override
String get backupPasswordWrong => '備份密碼錯誤';
String get availableTabs => '可用標籤';
@override
String get backupEncrypted => '備份已加密';
@@ -815,11 +922,26 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get backupNotEncrypted => '備份未加密';
@override
String get backupPasswordSet => '備份密碼已設定';
String get backupPassword => '備份密碼';
@override
String get backupPasswordRemoved => '備份密碼已移除';
@override
String get backupPasswordSet => '備份密碼已設定';
@override
String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';
@override
String get backupPasswordWrong => '備份密碼錯誤';
@override
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
@override
String get backupVersionNotMatch => '備份版本不相容,無法還原';
@override
String get battery => '電池';
@@ -830,6 +952,25 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get bgRunTip =>
'此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。';
@override
String get clearAllStatsContent => '確定要清空所有伺服器的連線統計資料嗎?此操作無法撤銷。';
@override
String get clearAllStatsTitle => '清空所有統計';
@override
String clearServerStatsContent(String serverName) {
return '確定要清空伺服器 \"$serverName\" 的連線統計資料嗎?此操作無法撤銷。';
}
@override
String clearServerStatsTitle(String serverName) {
return '清空 $serverName 統計';
}
@override
String get clearThisServerStats => '清空此伺服器統計';
@override
String get closeAfterSave => '儲存後關閉';
@@ -842,6 +983,15 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get conn => '連線';
@override
String get connectionDetails => '連線詳情';
@override
String get connectionStats => '連線統計';
@override
String get connectionStatsDesc => '檢視伺服器連線成功率和歷史記錄';
@override
String get container => '容器';
@@ -889,6 +1039,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get disconnected => '已中斷連線';
@override
String get discoverSshServers => '發現SSH服務器';
@override
String get discoveryFailed => '發現失敗';
@override
String get discoverySettings => '發現設定';
@override
String get discoverySummary => '發現摘要';
@override
String get disk => '磁碟';
@@ -940,15 +1102,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get editVirtKeys => '編輯虛擬按鍵';
@override
String get editor => '編輯器';
@override
String get editorHighlightTip => '程式碼高亮功能可能影響效能,可選擇性關閉。';
@override
String get emulator => '模擬器';
@override
String get enableMdns => '啟用mDNS';
@override
String get enableMdnsDesc => '使用mDNS/Bonjour發現SSH服務';
@override
String get encode => '編碼';
@@ -980,10 +1145,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
}
@override
String get followSystem => '跟隨系統';
String get finishedAt => '完成於';
@override
String get font => '字型';
String get followSystem => '跟隨系統';
@override
String get fontSize => '字型大小';
@@ -1015,6 +1180,12 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get highlight => '程式碼標記';
@override
String get homeTabs => '主頁標籤';
@override
String get homeTabsCustomizeDesc => '自訂主頁上顯示的標籤及其順序';
@override
String get homeWidgetUrlConfig => '桌面小工具連結配置';
@@ -1035,9 +1206,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get imagesList => '映像檔列表';
@override
String get init => '初始化';
@override
String get inner => '內建';
@@ -1066,15 +1234,18 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get keyAuth => '金鑰認證';
@override
String get lastFailure => '最後失敗';
@override
String get lastSuccess => '最後成功';
@override
String get letterCache => '輸入法字符快取';
@override
String get letterCacheTip => '建議關閉,但關閉後將無法輸入 CJK 等文字。';
@override
String get license => '憑證';
@override
String get location => '位置';
@@ -1087,10 +1258,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
}
@override
String get manual => '手動';
String get max => '最大';
@override
String get max => '最大';
String get maxConcurrency => '最大並發數';
@override
String get maxRetryCount => '伺服器嘗試重連次數';
@@ -1130,6 +1301,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get newContainer => '新建容器';
@override
String get noConnectionStatsData => '暫無連線統計資料';
@override
String get noLineChart => '不使用折線圖';
@@ -1195,9 +1369,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get preferDiskAmount => '優先顯示硬碟容量';
@override
String get preview => '預覽';
@override
String get privateKey => '私鑰';
@@ -1225,6 +1396,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get reboot => '重開';
@override
String get recentConnections => '最近連線記錄';
@override
String get rememberPwdInMem => '在記憶體中記住密碼';
@@ -1285,6 +1459,12 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get serverOrder => '伺服器順序';
@override
String get serverTabRequired => '服務器標籤不能被移除';
@override
String get servers => '服務器';
@override
String get sftpDlPrepare => '準備連線至伺服器...';
@@ -1330,6 +1510,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。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。';
@@ -1374,9 +1603,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
return '切換到 $val';
}
@override
String get sync => '同步';
@override
String get syncTip => '可能需要重新啟動,某些更改才能生效。';
@@ -1386,6 +1612,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get tag => '標籤';
@override
String get tapToStartDiscovery => '點擊搜尋按鈕發現網路中的SSH服務器';
@override
String get temperature => '溫度';
@@ -1416,6 +1645,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get total => '總共';
@override
String get totalAttempts => '總次數';
@override
String get traffic => '流量';
@@ -1440,9 +1672,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get updateServerStatusInterval => '伺服器狀態更新間隔';
@override
String get upload => '上傳';
@override
String get upsideDown => '上下交換';
@@ -1467,6 +1696,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get view => '檢視';
@override
String get viewDetails => '檢視詳情';
@override
String get viewErr => '查看錯誤';

View File

@@ -3,12 +3,18 @@
// Check in to version control
import 'package:hive_ce/hive.dart';
import 'package:server_box/data/model/app/tab.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/hive/hive_adapters.dart';
extension HiveRegistrar on HiveInterface {
void registerAdapters() {
registerAdapter(AppTabAdapter());
registerAdapter(ConnectionResultAdapter());
registerAdapter(ConnectionStatAdapter());
registerAdapter(NetViewTypeAdapter());
registerAdapter(PrivateKeyInfoAdapter());
registerAdapter(ServerConnectionStatsAdapter());
registerAdapter(ServerCustomAdapter());
registerAdapter(ServerFuncBtnAdapter());
registerAdapter(SnippetAdapter());
@@ -21,8 +27,12 @@ extension HiveRegistrar on HiveInterface {
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
void registerAdapters() {
registerAdapter(AppTabAdapter());
registerAdapter(ConnectionResultAdapter());
registerAdapter(ConnectionStatAdapter());
registerAdapter(NetViewTypeAdapter());
registerAdapter(PrivateKeyInfoAdapter());
registerAdapter(ServerConnectionStatsAdapter());
registerAdapter(ServerCustomAdapter());
registerAdapter(ServerFuncBtnAdapter());
registerAdapter(SnippetAdapter());

View File

@@ -31,45 +31,8 @@ final class _IntroPage extends StatelessWidget {
padding: _introListPad,
children: [
SizedBox(height: padTop),
IntroPage.title(text: l10n.init, big: true),
IntroPage.title(text: libL10n.init, big: true),
SizedBox(height: padTop),
// Prompt to set backup password after migration or on first launch
ListTile(
leading: const Icon(Icons.lock),
title: Text(l10n.backupPassword),
subtitle: Text(l10n.backupPasswordTip, style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
final currentPwd = await SecureStoreProps.bakPwd.read();
final controller = TextEditingController(text: currentPwd ?? '');
final result = await ctx.showRoundDialog<bool>(
title: l10n.backupPassword,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.backupPasswordTip, style: UIs.textGrey),
UIs.height13,
Input(
label: l10n.backupPassword,
controller: controller,
obscureText: true,
onSubmitted: (_) => ctx.pop(true),
),
],
),
actions: Btnx.oks,
);
if (result == true) {
final pwd = controller.text.trim();
if (pwd.isEmpty) {
ctx.showSnackBar(libL10n.empty);
return;
}
await SecureStoreProps.bakPwd.write(pwd);
ctx.showSnackBar(l10n.backupPasswordSet);
}
},
).cardx,
ListTile(
leading: const Icon(IonIcons.language),
title: Text(libL10n.language),
@@ -121,7 +84,7 @@ final class _IntroPage extends StatelessWidget {
IntroPage.title(text: l10n.backupPassword, big: true),
SizedBox(height: padTop * 0.5),
Text(
'${l10n.backupTip}\n\n${l10n.backupPasswordTip}',
l10n.backupTip,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
@@ -148,10 +111,7 @@ final class _IntroPage extends StatelessWidget {
),
],
),
actions: [
TextButton(onPressed: () => ctx.pop(false), child: Text(libL10n.cancel)),
TextButton(onPressed: () => ctx.pop(true), child: Text(libL10n.ok)),
],
actions: Btnx.cancelOk,
);
if (result == true) {
final pwd = controller.text.trim();

View File

@@ -1,32 +1,56 @@
{
"@@locale": "de",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\n",
"acceptBeta": "Akzeptieren Sie Testversion-Updates",
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
"added2List": "Zur Aufgabenliste hinzugefügt",
"addr": "Adresse",
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
"atLeastOneTab": "Mindestens ein Tab muss ausgewählt sein",
"authFailTip": "Authentifizierung fehlgeschlagen, bitte überprüfen Sie, ob das Passwort/Schlüssel/Host/Benutzer usw. falsch sind.",
"autoBackupConflict": "Es kann nur eine automatische Sicherung gleichzeitig aktiviert werden.",
"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",
"availableTabs": "Verfügbare Tabs",
"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\".",
"clearAllStatsContent": "Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"clearAllStatsTitle": "Alle Statistiken löschen",
"clearServerStatsContent": "Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"{serverName}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"clearServerStatsTitle": "{serverName} Statistiken löschen",
"clearThisServerStats": "Statistiken dieses Servers löschen",
"closeAfterSave": "Speichern und schließen",
"cmd": "Command",
"collapseUITip": "Ob lange Listen in der Benutzeroberfläche standardmäßig eingeklappt werden sollen oder nicht",
"conn": "Verbindung",
"connectionDetails": "Verbindungsdetails",
"connectionStats": "Verbindungsstatistiken",
"connectionStatsDesc": "Server-Verbindungserfolgsrate und Verlauf anzeigen",
"container": "Container",
"containerTrySudoTip": "Zum Beispiel: In der App ist der Benutzer auf aaa eingestellt, aber Docker ist unter dem Root-Benutzer installiert. In diesem Fall müssen Sie diese Option aktivieren",
"convert": "Konvertieren",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Befehl zum Öffnen des Terminal-Emulators beim Starten von SSH-Sitzungen.",
"dirEmpty": "Stelle sicher, dass der Ordner leer ist.",
"disconnected": "Disconnected",
"discoverSshServers": "SSH-Server entdecken",
"discoveryFailed": "Entdeckung fehlgeschlagen",
"discoverySettings": "Entdeckungseinstellungen",
"discoverySummary": "Entdeckungs-Zusammenfassung",
"disk": "Festplatte",
"diskHealth": "Festplattengesundheit",
"diskIgnorePath": "Pfad für Datenträger ignorieren",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Doppelspaltiger Modus",
"doubleColumnTip": "Diese Option aktiviert nur die Funktion, ob sie tatsächlich aktiviert werden kann, hängt auch von der Breite des Geräts ab",
"editVirtKeys": "Virtuelle Tasten bearbeiten",
"editor": "Editor",
"editorHighlightTip": "Die Leistung der aktuellen Codehervorhebung ist schlechter und kann zur Verbesserung optional ausgeschaltet werden.",
"emulator": "Emulator",
"enableMdns": "mDNS aktivieren",
"enableMdnsDesc": "mDNS/Bonjour verwenden, um SSH-Dienste zu entdecken",
"encode": "Encode",
"envVars": "Umgebungsvariable",
"experimentalFeature": "Experimentelles Feature",
@@ -67,8 +96,8 @@
"fgService": "Vordergrund-Dienst",
"fgServiceTip": "Nach dem Einschalten kann es bei einigen Gerätemodellen zu Abstürzen kommen. Das Ausschalten kann bei einigen Modellen dazu führen, dass SSH-Verbindungen im Hintergrund nicht aufrechterhalten werden können. Bitte erlauben Sie ServerBox in den Systemeinstellungen Benachrichtigungsrechte, Hintergrundausführung und Selbstaktivierung.",
"fileTooLarge": "Datei '{file}' ist zu groß {size}, max {sizeMax}",
"finishedAt": "Beendet um",
"followSystem": "System verfolgen",
"font": "Schriftarten",
"fontSize": "Schriftgröße",
"force": "freiwillig",
"fullScreen": "Vollbildmodus",
@@ -79,13 +108,14 @@
"goto": "Pfad öffnen",
"hideTitleBar": "Titelleiste ausblenden",
"highlight": "Code highlight",
"homeTabs": "Home-Tabs",
"homeTabsCustomizeDesc": "Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge",
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
"host": "Host",
"httpFailedWithCode": "Anfrage fehlgeschlagen, Statuscode: {code}",
"ignoreCert": "Zertifikat ignorieren",
"image": "Image",
"imagesList": "Images",
"init": "Initialisieren",
"inner": "Eingebaut",
"install": "install",
"installDockerWithUrl": "Bitte installiere docker zuerst. https://docs.docker.com/engine/install",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Den letzten Serverstatus beibehalten",
"keepStatusWhenErrTip": "Nur im Fehlerfall während der Ausführung des Skripts",
"keyAuth": "Schlüsselauthentifzierung",
"lastFailure": "Letzter Fehler",
"lastSuccess": "Letzter Erfolg",
"letterCache": "Buchstaben-Caching",
"letterCacheTip": "Empfohlen, zu deaktivieren, aber nach dem Deaktivieren können keine CJK-Zeichen eingegeben werden.",
"license": "Lizenzen",
"location": "Standort",
"loss": "loss",
"madeWithLove": "Erstellt mit ❤️ von {myGithub}",
"manual": "Handbuch",
"max": "max",
"maxConcurrency": "Maximale Gleichzeitigkeit",
"maxRetryCount": "Anzahl an Verbindungsversuchen",
"maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server",
"min": "min",
@@ -115,6 +146,7 @@
"net": "Netzwerk",
"netViewType": "Netzwerkansicht Typ",
"newContainer": "Neuer Container",
"noConnectionStatsData": "Keine Verbindungsstatistikdaten",
"noLineChart": "Verwenden Sie keine Liniendiagramme",
"noLineChartForCpu": "Verwenden Sie keine Liniendiagramme für CPU",
"noPrivateKeyTip": "Der private Schlüssel existiert nicht, möglicherweise wurde er gelöscht oder es liegt ein Konfigurationsfehler vor.",
@@ -136,7 +168,6 @@
"plugInType": "Einfügetyp",
"port": "Port",
"preferDiskAmount": "Festplattenkapazität vorrangig anzeigen",
"preview": "Vorschau",
"privateKey": "Private Key",
"process": "Prozess",
"prune": "Beschneiden",
@@ -146,6 +177,7 @@
"pveVersionLow": "Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.",
"read": "Lesen",
"reboot": "Neustart",
"recentConnections": "Kürzliche Verbindungen",
"rememberPwdInMem": "Passwort im Speicher behalten",
"rememberPwdInMemTip": "Für Container, Aufhängen usw.",
"rememberWindowSize": "Fenstergröße merken",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Reihenfolge der Widgets auf der Detailseite",
"serverFuncBtns": "Server-Funktionsschaltflächen",
"serverOrder": "Server-Bestellung",
"serverTabRequired": "Server-Tab kann nicht entfernt werden",
"servers": "Server",
"sftpDlPrepare": "Verbindung vorbereiten...",
"sftpEditorTip": "Wenn leer, verwenden Sie den im App integrierten Dateieditor. Wenn ein Wert vorhanden ist, wird der Editor des Remote-Servers verwendet, z.B. `vim` (es wird empfohlen, automatisch gemäß `EDITOR` zu ermitteln).",
"sftpRmrDirSummary": "Verwenden Sie \"rm -r\", um das Verzeichnis in SFTP zu löschen.",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Suspend",
"suspendTip": "Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.",
"switchTo": "Wechseln zu {val}",
"sync": "Sync",
"syncTip": "Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.",
"system": "Systeme",
"tag": "Tags",
"tapToStartDiscovery": "Tippen Sie auf die Suche-Schaltfläche, um SSH-Server in Ihrem Netzwerk zu entdecken",
"temperature": "Temperatur",
"termFontSizeTip": "Diese Einstellung beeinflusst die Größe des Terminals (Breite und Höhe). Sie können die Terminalseite zoomen, um die Schriftgröße der aktuellen Sitzung anzupassen.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Zeit",
"times": "x",
"total": "Total",
"totalAttempts": "Gesamt",
"traffic": "Durchflussmenge",
"trySudo": "Versuche es mit sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Update",
"updateIntervalEqual0": "Wenn du den Wert 0 einstellst, wird nicht automatisch aktualisiert.\nDer CPU-Status kann nicht berechnet werden.",
"updateServerStatusInterval": "Aktualisierungsintervall des Serverstatus",
"upload": "Hochladen",
"upsideDown": "Upside Down",
"uptime": "Betriebszeit",
"useCdn": "Verwenden von CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Standardmäßige Verwendung von Podman",
"used": "Gebraucht",
"view": "Ansicht",
"viewDetails": "Details anzeigen",
"viewErr": "Fehler anzeigen",
"virtKeyHelpClipboard": "In die Zwischenablage kopieren, wenn das ausgewählte Terminal nicht leer ist, andernfalls den Inhalt der Zwischenablage in das Terminal einfügen.",
"virtKeyHelpIME": "Tastatur ein-/ausschalten",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "en",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Thanks to the following people who participated in.",
"acceptBeta": "Accept beta version updates",
"addSystemPrivateKeyTip": "Currently private keys don't exist, do you want to add the one that comes with the system (~/.ssh/id_rsa)?",
"added2List": "Added to task list",
"addr": "Address",
"alreadyLastDir": "Already in last directory.",
"atLeastOneTab": "At least one tab must be selected",
"authFailTip": "Authentication failed, please check whether credentials are correct",
"autoBackupConflict": "Only one automatic backup can be turned on at the same time.",
"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",
"availableTabs": "Available Tabs",
"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\".",
"clearAllStatsContent": "Are you sure you want to clear all server connection statistics? This action cannot be undone.",
"clearAllStatsTitle": "Clear All Statistics",
"clearServerStatsContent": "Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.",
"clearServerStatsTitle": "Clear {serverName} Statistics",
"clearThisServerStats": "Clear This Server Statistics",
"closeAfterSave": "Save and close",
"cmd": "Command",
"collapseUITip": "Whether to collapse long lists present in the UI by default",
"conn": "Connection",
"connectionDetails": "Connection Details",
"connectionStats": "Connection Statistics",
"connectionStatsDesc": "View server connection success rate and history",
"container": "Container",
"containerTrySudoTip": "For example: In the app, the user is set to aaa, but Docker is installed under the root user. In this case, you need to enable this option.",
"convert": "Convert",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Command used to open the terminal emulator when launching SSH sessions.",
"dirEmpty": "Make sure the folder is empty.",
"disconnected": "Disconnected",
"discoverSshServers": "Discover SSH Servers",
"discoveryFailed": "Discovery failed",
"discoverySettings": "Discovery Settings",
"discoverySummary": "Discovery Summary",
"disk": "Disk",
"diskHealth": "Disk Health",
"diskIgnorePath": "Ignore path for disk",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Double column mode",
"doubleColumnTip": "This option only enables the feature, whether it can actually be enabled depends on the width of the device",
"editVirtKeys": "Edit virtual keys",
"editor": "Editor",
"editorHighlightTip": "The current code highlighting performance is not ideal and can be optionally turned off to improve.",
"emulator": "Emulator",
"enableMdns": "Enable mDNS",
"enableMdnsDesc": "Use mDNS/Bonjour to discover SSH services",
"encode": "Encode",
"envVars": "Environment variable",
"experimentalFeature": "Experimental feature",
@@ -67,8 +96,8 @@
"fgService": "Foreground Service",
"fgServiceTip": "After enabling, some device models may crash. Disabling it may cause some models to be unable to maintain SSH connections in the background. Please allow ServerBox notification permissions, background running, and self-wake-up in system settings.",
"fileTooLarge": "File '{file}' too large {size}, max {sizeMax}",
"finishedAt": "Finished at",
"followSystem": "Follow system",
"font": "Font",
"fontSize": "Font size",
"force": "Force",
"fullScreen": "Full screen mode",
@@ -79,13 +108,14 @@
"goto": "Go to",
"hideTitleBar": "Hide title bar",
"highlight": "Code highlighting",
"homeTabs": "Home Tabs",
"homeTabsCustomizeDesc": "Customize which tabs appear on the home page and their order",
"homeWidgetUrlConfig": "Config home widget url",
"host": "Host",
"httpFailedWithCode": "request failed, status code: {code}",
"ignoreCert": "Ignore certificate",
"image": "Image",
"imagesList": "Images list",
"init": "Initialize",
"inner": "Inner",
"install": "install",
"installDockerWithUrl": "Please https://docs.docker.com/engine/install docker first.",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Preserve the last server state",
"keepStatusWhenErrTip": "Only in the event of an error during script execution",
"keyAuth": "Key Auth",
"lastFailure": "Last Failure",
"lastSuccess": "Last Success",
"letterCache": "Letter caching",
"letterCacheTip": "Recommended to disable, but after disabling, it will be impossible to input CJK characters.",
"license": "License",
"location": "Location",
"loss": "loss",
"madeWithLove": "Made with ❤️ by {myGithub}",
"manual": "Manual",
"max": "max",
"maxConcurrency": "Max Concurrency",
"maxRetryCount": "Number of server reconnections",
"maxRetryCountEqual0": "Will retry again and again.",
"min": "min",
@@ -115,6 +146,7 @@
"net": "Network",
"netViewType": "Network view type",
"newContainer": "New container",
"noConnectionStatsData": "No connection statistics data",
"noLineChart": "Do not use line charts",
"noLineChartForCpu": "Do not use line charts for CPU",
"noPrivateKeyTip": "The private key does not exist, it may have been deleted or there is a configuration error.",
@@ -136,7 +168,6 @@
"plugInType": "Insertion Type",
"port": "Port",
"preferDiskAmount": "Prioritize displaying disk capacity",
"preview": "Preview",
"privateKey": "Private Key",
"process": "Process",
"prune": "Prune",
@@ -146,6 +177,7 @@
"pveVersionLow": "This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.",
"read": "Read",
"reboot": "Reboot",
"recentConnections": "Recent Connections",
"rememberPwdInMem": "Remember password in memory",
"rememberPwdInMemTip": "Used for containers, suspending, etc.",
"rememberWindowSize": "Remember window size",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Detail page widget order",
"serverFuncBtns": "Server function buttons",
"serverOrder": "Server order",
"serverTabRequired": "Server tab cannot be removed",
"servers": "servers",
"sftpDlPrepare": "Preparing to connect...",
"sftpEditorTip": "If empty, use the built-in file editor of the app. If a value is present, use the remote servers editor, e.g., `vim` (recommended to automatically detect according to `EDITOR`).",
"sftpRmrDirSummary": "Use `rm -r` to delete a folder in SFTP.",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Suspend",
"suspendTip": "The suspend function requires root permission and systemd support.",
"switchTo": "Switch to {val}",
"sync": "Sync",
"syncTip": "A restart may be required for some changes to take effect.",
"system": "System",
"tag": "Tags",
"tapToStartDiscovery": "Tap the search button to discover SSH servers on your network",
"temperature": "Temperature",
"termFontSizeTip": "This setting will affect the terminal size (width and height). You can zoom in on the terminal page to adjust the font size of the current session.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Time",
"times": "Times",
"total": "Total",
"totalAttempts": "Total",
"traffic": "Traffic",
"trySudo": "Try using sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Update",
"updateIntervalEqual0": "You set to 0, will not update automatically.\nCan't calculate CPU status.",
"updateServerStatusInterval": "Server status update interval",
"upload": "Upload",
"upsideDown": "Upside Down",
"uptime": "Uptime",
"useCdn": "Using CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Use Podman by default",
"used": "Used",
"view": "View",
"viewDetails": "View Details",
"viewErr": "See error",
"virtKeyHelpClipboard": "Copy to the clipboard if the selected terminal is not empty, otherwise paste the content of the clipboard to the terminal.",
"virtKeyHelpIME": "Turn on/off the keyboard",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "es",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Gracias a los siguientes participantes.",
"acceptBeta": "Aceptar actualizaciones de la versión de prueba",
"addSystemPrivateKeyTip": "Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?",
"added2List": "Añadido a la lista de tareas",
"addr": "Dirección",
"alreadyLastDir": "Ya estás en el directorio superior",
"atLeastOneTab": "Al menos una pestaña debe estar seleccionada",
"authFailTip": "La autenticación ha fallado, por favor verifica si la contraseña/llave/host/usuario, etc., son incorrectos.",
"autoBackupConflict": "Solo se puede activar una copia de seguridad automática a la vez",
"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",
"availableTabs": "Pestañas disponibles",
"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”.",
"clearAllStatsContent": "¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.",
"clearAllStatsTitle": "Limpiar todas las estadísticas",
"clearServerStatsContent": "¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"{serverName}\"? Esta acción no se puede deshacer.",
"clearServerStatsTitle": "Limpiar estadísticas de {serverName}",
"clearThisServerStats": "Limpiar estadísticas de este servidor",
"closeAfterSave": "Guardar y cerrar",
"cmd": "Comando",
"collapseUITip": "¿Colapsar por defecto las listas largas en la UI?",
"conn": "Conectar",
"connectionDetails": "Detalles de conexión",
"connectionStats": "Estadísticas de conexión",
"connectionStatsDesc": "Ver la tasa de éxito de conexión del servidor e historial",
"container": "Contenedor",
"containerTrySudoTip": "Por ejemplo: si configuras el usuario dentro de la app como aaa, pero Docker está instalado bajo el usuario root, entonces necesitarás habilitar esta opción",
"convert": "Convertir",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Comando utilizado para abrir el emulador de terminal al iniciar sesiones SSH.",
"dirEmpty": "Asegúrate de que el directorio esté vacío",
"disconnected": "Desconectado",
"discoverSshServers": "Descubrir servidores SSH",
"discoveryFailed": "Falló el descubrimiento",
"discoverySettings": "Configuración de descubrimiento",
"discoverySummary": "Resumen del descubrimiento",
"disk": "Disco",
"diskHealth": "Salud del disco",
"diskIgnorePath": "Rutas de disco ignoradas",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Modo de doble columna",
"doubleColumnTip": "Esta opción solo habilita la función, si se puede activar o no depende del ancho del dispositivo",
"editVirtKeys": "Editar teclas virtuales",
"editor": "Editor",
"editorHighlightTip": "El rendimiento del resaltado de código es bastante pobre actualmente, puedes elegir desactivarlo para mejorar.",
"emulator": "Emulador",
"enableMdns": "Habilitar mDNS",
"enableMdnsDesc": "Usar mDNS/Bonjour para descubrir servicios SSH",
"encode": "Codificar",
"envVars": "Variable de entorno",
"experimentalFeature": "Función experimental",
@@ -67,8 +96,8 @@
"fgService": "Servicio en primer plano",
"fgServiceTip": "Después de activarlo, algunos modelos de dispositivos pueden bloquearse. Desactivarlo puede hacer que algunos modelos no puedan mantener las conexiones SSH en segundo plano. Por favor, permita los permisos de notificación de ServerBox, la ejecución en segundo plano y el auto-despertar en la configuración del sistema.",
"fileTooLarge": "El archivo '{file}' es demasiado grande '{size}', supera el {sizeMax}",
"finishedAt": "Terminado en",
"followSystem": "Seguir al sistema",
"font": "Fuente",
"fontSize": "Tamaño de fuente",
"force": "Forzar",
"fullScreen": "Modo pantalla completa",
@@ -79,13 +108,14 @@
"goto": "Ir a",
"hideTitleBar": "Ocultar barra de título",
"highlight": "Resaltar código",
"homeTabs": "Pestañas de inicio",
"homeTabsCustomizeDesc": "Personaliza qué pestañas aparecen en la página de inicio y su orden",
"homeWidgetUrlConfig": "Configuración de URL del widget de inicio",
"host": "Anfitrión",
"httpFailedWithCode": "Fallo en la solicitud, código de estado: {code}",
"ignoreCert": "Ignorar certificado",
"image": "Imagen",
"imagesList": "Lista de imágenes",
"init": "Inicializar",
"inner": "Interno",
"install": "Instalar",
"installDockerWithUrl": "Por favor instala Docker primero desde https://docs.docker.com/engine/install",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Mantener el estado anterior del servidor",
"keepStatusWhenErrTip": "Solo aplica cuando hay errores al ejecutar scripts",
"keyAuth": "Autenticación con llave",
"lastFailure": "Último fallo",
"lastSuccess": "Último éxito",
"letterCache": "Caché de letras",
"letterCacheTip": "Recomendado desactivar, pero después de desactivarlo, no se podrán ingresar caracteres CJK.",
"license": "Licencia de código abierto",
"location": "Ubicación",
"loss": "Tasa de pérdida",
"madeWithLove": "Hecho con ❤️ por {myGithub}",
"manual": "Manual",
"max": "Máximo",
"maxConcurrency": "Concurrencia máxima",
"maxRetryCount": "Número máximo de reintentos de conexión al servidor",
"maxRetryCountEqual0": "Reintentará infinitamente",
"min": "Mínimo",
@@ -115,6 +146,7 @@
"net": "Red",
"netViewType": "Tipo de vista de red",
"newContainer": "Crear contenedor nuevo",
"noConnectionStatsData": "No hay datos de estadísticas de conexión",
"noLineChart": "No utilice gráficos de líneas",
"noLineChartForCpu": "No utilice gráficos lineales para la CPU",
"noPrivateKeyTip": "La clave privada no existe, puede haber sido eliminada o hay un error de configuración.",
@@ -136,7 +168,6 @@
"plugInType": "Tipo de inserción",
"port": "Puerto",
"preferDiskAmount": "Priorizar la visualización de la capacidad del disco",
"preview": "Vista previa",
"privateKey": "Llave privada",
"process": "Proceso",
"prune": "Podar",
@@ -146,6 +177,7 @@
"pveVersionLow": "Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.",
"read": "Leer",
"reboot": "Reiniciar",
"recentConnections": "Conexiones recientes",
"rememberPwdInMem": "Recordar contraseña en la memoria",
"rememberPwdInMemTip": "Utilizado para contenedores, suspensión, etc.",
"rememberWindowSize": "Recordar el tamaño de la ventana",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Orden de los componentes en la página de detalles del servidor",
"serverFuncBtns": "Botones de función del servidor",
"serverOrder": "Orden del servidor",
"serverTabRequired": "La pestaña del servidor no se puede eliminar",
"servers": "servidores",
"sftpDlPrepare": "Preparando para conectar al servidor...",
"sftpEditorTip": "Si está vacío, use el editor de archivos incorporado de la aplicación. Si hay un valor, use el editor del servidor remoto, por ejemplo, `vim` (se recomienda detectar automáticamente según `EDITOR`).",
"sftpRmrDirSummary": "Usar `rm -r` en SFTP para eliminar directorios",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Suspender",
"suspendTip": "La función de suspender necesita permisos de root y soporte de systemd.",
"switchTo": "Cambiar a {val}",
"sync": "Sincronizar",
"syncTip": "Puede que necesites reiniciar para que algunos cambios tengan efecto.",
"system": "Sistema",
"tag": "Etiqueta",
"tapToStartDiscovery": "Toca el botón de búsqueda para descubrir servidores SSH en tu red",
"temperature": "Temperatura",
"termFontSizeTip": "Este ajuste afectará el tamaño del terminal (ancho y alto). Puedes hacer zoom en la página del terminal para ajustar el tamaño de fuente de la sesión actual.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Tiempo",
"times": "Veces",
"total": "Total",
"totalAttempts": "Total",
"traffic": "Tráfico",
"trySudo": "Intentar con sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Actualizar",
"updateIntervalEqual0": "Si configuras esto a 0, el estado del servidor no se refrescará automáticamente.\nY no se podrá calcular el uso de CPU.",
"updateServerStatusInterval": "Intervalo de actualización del estado del servidor",
"upload": "Subir",
"upsideDown": "Invertir arriba por abajo",
"uptime": "Tiempo de actividad",
"useCdn": "Usando CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Usar Podman por defecto",
"used": "Usado",
"view": "Vista",
"viewDetails": "Ver detalles",
"viewErr": "Ver error",
"virtKeyHelpClipboard": "Si el terminal tiene caracteres seleccionados, entonces copiará los caracteres seleccionados al portapapeles, de lo contrario, pegará el contenido del portapapeles al terminal.",
"virtKeyHelpIME": "Encender/apagar el teclado",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "fr",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Merci aux personnes suivantes qui ont participé.",
"acceptBeta": "Accepter les mises à jour de la version de test",
"addSystemPrivateKeyTip": "Actuellement, vous n'avez aucune clé privée. Souhaitez-vous ajouter celle qui vient avec le système (~/.ssh/id_rsa) ?",
"added2List": "Ajouté à la liste des tâches",
"addr": "Adresse",
"alreadyLastDir": "Déjà dans le dernier répertoire.",
"atLeastOneTab": "Au moins un onglet doit être sélectionné",
"authFailTip": "Échec de l'authentification. Veuillez vérifier si le mot de passe/clé/hôte/utilisateur, etc., est incorrect.",
"autoBackupConflict": "Un seul sauvegarde automatique peut être activé en même temps.",
"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",
"availableTabs": "Onglets disponibles",
"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é ».",
"clearAllStatsContent": "Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.",
"clearAllStatsTitle": "Effacer toutes les statistiques",
"clearServerStatsContent": "Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"{serverName}\" ? Cette action ne peut pas être annulée.",
"clearServerStatsTitle": "Effacer les statistiques de {serverName}",
"clearThisServerStats": "Effacer les statistiques de ce serveur",
"closeAfterSave": "Enregistrer et fermer",
"cmd": "Commande",
"collapseUITip": "Indique si les longues listes présentées dans l'interface utilisateur doivent être réduites par défaut.",
"conn": "Connexion",
"connectionDetails": "Détails de connexion",
"connectionStats": "Statistiques de connexion",
"connectionStatsDesc": "Voir le taux de réussite de connexion du serveur et l'historique",
"container": "Conteneur",
"containerTrySudoTip": "Par exemple : Dans l'application, l'utilisateur est défini comme aaa, mais Docker est installé sous l'utilisateur root. Dans ce cas, vous devez activer cette option.",
"convert": "Convertir",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Commande utilisée pour ouvrir lémulateur de terminal lors du lancement de sessions SSH.",
"dirEmpty": "Assurez-vous que le répertoire est vide.",
"disconnected": "Déconnecté",
"discoverSshServers": "Découvrir les serveurs SSH",
"discoveryFailed": "Échec de la découverte",
"discoverySettings": "Paramètres de découverte",
"discoverySummary": "Résumé de la découverte",
"disk": "Disque",
"diskHealth": "Santé du disque",
"diskIgnorePath": "Chemin à ignorer pour le disque",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Mode double colonne",
"doubleColumnTip": "Cette option n'active que la fonctionnalité, qu'elle puisse être activée dépend de la largeur de l'appareil.",
"editVirtKeys": "Modifier les touches virtuelles",
"editor": "Éditeur",
"editorHighlightTip": "La performance actuelle de mise en surbrillance du code est pire et peut être désactivée en option pour s'améliorer.",
"emulator": "Émulateur",
"enableMdns": "Activer mDNS",
"enableMdnsDesc": "Utiliser mDNS/Bonjour pour découvrir les services SSH",
"encode": "Encoder",
"envVars": "Variable denvironnement",
"experimentalFeature": "Fonctionnalité expérimentale",
@@ -67,8 +96,8 @@
"fgService": "Service de premier plan",
"fgServiceTip": "Après l'activation, certains modèles d'appareils peuvent planter. La désactivation peut empêcher certains modèles de maintenir les connexions SSH en arrière-plan. Veuillez autoriser les permissions de notification ServerBox, l'exécution en arrière-plan et l'auto-réveil dans les paramètres système.",
"fileTooLarge": "Fichier '{file}' trop volumineux {size}, max {sizeMax}",
"finishedAt": "Terminé à",
"followSystem": "Suivre le système",
"font": "Police",
"fontSize": "Taille de la police",
"force": "Forcer",
"fullScreen": "Mode plein écran",
@@ -79,13 +108,14 @@
"goto": "Aller à",
"hideTitleBar": "Masquer la barre de titre",
"highlight": "Mise en surbrillance du code",
"homeTabs": "Onglets d'accueil",
"homeTabsCustomizeDesc": "Personnalisez les onglets qui apparaissent sur la page d'accueil et leur ordre",
"homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil",
"host": "Hôte",
"httpFailedWithCode": "Échec de la requête, code d'état : {code}",
"ignoreCert": "Ignorer le certificat",
"image": "Image",
"imagesList": "Liste des images",
"init": "Initialiser",
"inner": "Interne",
"install": "Installer",
"installDockerWithUrl": "Veuillez d'abord installer docker depuis https://docs.docker.com/engine/install.",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Conserver l'état du dernier serveur",
"keepStatusWhenErrTip": "Uniquement en cas d'erreur lors de l'exécution du script",
"keyAuth": "Authentification par clé",
"lastFailure": "Dernier échec",
"lastSuccess": "Dernier succès",
"letterCache": "Mise en cache des lettres",
"letterCacheTip": "Recommandé de désactiver, mais après désactivation, il sera impossible de saisir des caractères CJK.",
"license": "Licence",
"location": "Emplacement",
"loss": "Perte",
"madeWithLove": "Fabriqué avec ❤️ par {myGithub}",
"manual": "Manuel",
"max": "max",
"maxConcurrency": "Concurrence maximale",
"maxRetryCount": "Nombre de reconnexions au serveur",
"maxRetryCountEqual0": "Il va réessayer encore et encore.",
"min": "min",
@@ -115,6 +146,7 @@
"net": "Réseau",
"netViewType": "Type de vue réseau",
"newContainer": "Nouveau conteneur",
"noConnectionStatsData": "Aucune donnée de statistiques de connexion",
"noLineChart": "Ne pas utiliser de graphiques linéaires",
"noLineChartForCpu": "Ne pas utiliser de graphiques linéaires pour l'unité centrale",
"noPrivateKeyTip": "La clé privée n'existe pas, elle a peut-être été supprimée ou il y a une erreur de configuration.",
@@ -136,7 +168,6 @@
"plugInType": "Type d'insertion",
"port": "Port",
"preferDiskAmount": "Prioriser laffichage de la capacité du disque",
"preview": "Aperçu",
"privateKey": "Clé privée",
"process": "Processus",
"prune": "Élaguer",
@@ -146,6 +177,7 @@
"pveVersionLow": "Cette fonctionnalité est actuellement en phase de test et n'a été testée que sur PVE 8+. Veuillez l'utiliser avec prudence.",
"read": "Lire",
"reboot": "Redémarrer",
"recentConnections": "Connexions récentes",
"rememberPwdInMem": "Mémoriser le mot de passe en mémoire",
"rememberPwdInMemTip": "Utilisé pour les conteneurs, la suspension, etc.",
"rememberWindowSize": "Se souvenir de la taille de la fenêtre",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Ordre des widgets de la page de détails du serveur",
"serverFuncBtns": "Boutons de fonction du serveur",
"serverOrder": "Ordre du serveur",
"serverTabRequired": "L'onglet serveur ne peut pas être supprimé",
"servers": "serveurs",
"sftpDlPrepare": "Préparation de la connexion...",
"sftpEditorTip": "Si vide, utilisez léditeur de fichiers intégré de lapplication. Si une valeur est présente, utilisez léditeur du serveur distant, par exemple `vim` (il est recommandé de détecter automatiquement selon `EDITOR`).",
"sftpRmrDirSummary": "Utilisez `rm -r` pour supprimer un dossier en SFTP.",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Suspendre",
"suspendTip": "La fonction de suspension nécessite des privilèges root et le support de systemd.",
"switchTo": "Passer à {val}",
"sync": "Sync",
"syncTip": "Un redémarrage peut être nécessaire pour que certains changements prennent effet.",
"system": "Système",
"tag": "Étiquettes",
"tapToStartDiscovery": "Appuyez sur le bouton de recherche pour découvrir les serveurs SSH sur votre réseau",
"temperature": "Température",
"termFontSizeTip": "Ce paramètre affectera la taille du terminal (largeur et hauteur). Vous pouvez zoomer sur la page du terminal pour ajuster la taille de la police de la session en cours.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Temps",
"times": "Fois",
"total": "Total",
"totalAttempts": "Total",
"traffic": "Trafic",
"trySudo": "Essayer d'utiliser sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Mettre à jour",
"updateIntervalEqual0": "Vous avez défini à 0, la mise à jour ne se fera pas automatiquement.\nImpossible de calculer l'état du CPU.",
"updateServerStatusInterval": "Intervalle de mise à jour de l'état du serveur",
"upload": "Télécharger",
"upsideDown": "À l'envers",
"uptime": "Temps d'activité",
"useCdn": "Utiliser CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Par défaut avec Podman",
"used": "Utilisé",
"view": "Vue",
"viewDetails": "Voir les détails",
"viewErr": "Voir erreur",
"virtKeyHelpClipboard": "Copiez dans le presse-papiers si le terminal sélectionné n'est pas vide, sinon collez le contenu du presse-papiers dans le terminal.",
"virtKeyHelpIME": "Activer/désactiver le clavier",
@@ -236,5 +284,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."
}

View File

@@ -1,32 +1,56 @@
{
"@@locale": "id",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Terima kasih kepada orang -orang berikut yang berpartisipasi.",
"acceptBeta": "Terima pembaruan versi uji coba",
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
"added2List": "Ditambahkan ke Daftar Tugas",
"addr": "Alamat",
"alreadyLastDir": "Sudah di direktori terakhir.",
"atLeastOneTab": "Setidaknya satu tab harus dipilih",
"authFailTip": "Otentikasi gagal, silakan periksa apakah kata sandi/kunci/host/pengguna, dll, salah.",
"autoBackupConflict": "Hanya satu pencadangan otomatis yang dapat diaktifkan pada saat yang bersamaan.",
"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",
"availableTabs": "Tab Tersedia",
"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\".",
"clearAllStatsContent": "Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.",
"clearAllStatsTitle": "Hapus Semua Statistik",
"clearServerStatsContent": "Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"{serverName}\"? Tindakan ini tidak dapat dibatalkan.",
"clearServerStatsTitle": "Hapus Statistik {serverName}",
"clearThisServerStats": "Hapus Statistik Server Ini",
"closeAfterSave": "Simpan dan tutup",
"cmd": "Memerintah",
"collapseUITip": "Apakah akan menciutkan daftar panjang yang ada di UI secara default atau tidak",
"conn": "Koneksi",
"connectionDetails": "Detail Koneksi",
"connectionStats": "Statistik Koneksi",
"connectionStatsDesc": "Lihat tingkat keberhasilan koneksi server dan riwayat",
"container": "Wadah",
"containerTrySudoTip": "Contohnya: Di dalam aplikasi, pengguna diatur sebagai aaa, tetapi Docker diinstal di bawah pengguna root. Dalam kasus ini, Anda perlu mengaktifkan opsi ini.",
"convert": "Mengubah",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Perintah yang digunakan untuk membuka emulator terminal saat memulai sesi SSH.",
"dirEmpty": "Pastikan dir kosong.",
"disconnected": "Terputus",
"discoverSshServers": "Temukan Server SSH",
"discoveryFailed": "Penemuan gagal",
"discoverySettings": "Pengaturan Penemuan",
"discoverySummary": "Ringkasan Penemuan",
"disk": "Disk",
"diskHealth": "Kesehatan disk",
"diskIgnorePath": "Abaikan jalan untuk disk",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Mode kolom ganda",
"doubleColumnTip": "Opsi ini hanya mengaktifkan fitur, apakah itu benar-benar dapat diaktifkan tergantung pada lebar perangkat",
"editVirtKeys": "Edit kunci virtual",
"editor": "Editor",
"editorHighlightTip": "Performa penyorotan kode saat ini lebih buruk, dan dapat dimatikan secara opsional untuk perbaikan.",
"emulator": "Emulator",
"enableMdns": "Aktifkan mDNS",
"enableMdnsDesc": "Gunakan mDNS/Bonjour untuk menemukan layanan SSH",
"encode": "Menyandi",
"envVars": "Variabel lingkungan",
"experimentalFeature": "Fitur eksperimental",
@@ -67,8 +96,8 @@
"fgService": "Layanan Latar Depan",
"fgServiceTip": "Setelah diaktifkan, beberapa model perangkat mungkin crash. Menonaktifkannya dapat menyebabkan beberapa model tidak dapat mempertahankan koneksi SSH di latar belakang. Harap izinkan perizinan notifikasi ServerBox, menjalankan di latar belakang, dan bangun mandiri di pengaturan sistem.",
"fileTooLarge": "File '{file}' terlalu besar {size}, max {sizeMax}",
"finishedAt": "Selesai pada",
"followSystem": "Ikuti sistem",
"font": "Font",
"fontSize": "Ukuran huruf",
"force": "sukarela",
"fullScreen": "Mode Layar Penuh",
@@ -79,13 +108,14 @@
"goto": "Pergi ke",
"hideTitleBar": "Sembunyikan bilah judul",
"highlight": "Sorotan kode",
"homeTabs": "Tab Beranda",
"homeTabsCustomizeDesc": "Sesuaikan tab mana yang muncul di halaman beranda dan urutannya",
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
"host": "Host",
"httpFailedWithCode": "Permintaan gagal, kode status: {code}",
"ignoreCert": "Abaikan sertifikat",
"image": "Gambar",
"imagesList": "Daftar gambar",
"init": "Menginisialisasi",
"inner": "Batin",
"install": "Install",
"installDockerWithUrl": "Silakan https://docs.docker.com/engine/install Docker pertama.",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Menyimpan status server terakhir",
"keepStatusWhenErrTip": "Hanya ketika terjadi kesalahan saat menjalankan skrip",
"keyAuth": "Auth kunci",
"lastFailure": "Gagal Terakhir",
"lastSuccess": "Sukses Terakhir",
"letterCache": "Caching huruf",
"letterCacheTip": "Direkomendasikan untuk menonaktifkan, tetapi setelah dinonaktifkan, tidak mungkin untuk memasukkan karakter CJK.",
"license": "Lisensi",
"location": "Lokasi",
"loss": "kehilangan",
"madeWithLove": "Dibuat dengan ❤️ oleh {myGithub}",
"manual": "Manual",
"max": "Max",
"maxConcurrency": "Konkurensi Maksimum",
"maxRetryCount": "Jumlah penyambungan kembali server",
"maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.",
"min": "Min",
@@ -115,6 +146,7 @@
"net": "Jaringan",
"netViewType": "Jenis tampilan bersih",
"newContainer": "Wadah baru",
"noConnectionStatsData": "Tidak ada data statistik koneksi",
"noLineChart": "Jangan gunakan grafik garis",
"noLineChartForCpu": "Jangan gunakan diagram garis untuk CPU",
"noPrivateKeyTip": "Kunci privat tidak ada, mungkin telah dihapus atau ada kesalahan konfigurasi.",
@@ -136,7 +168,6 @@
"plugInType": "Jenis Penyisipan",
"port": "Port",
"preferDiskAmount": "Prioritaskan tampilan kapasitas disk",
"preview": "Pratinjau",
"privateKey": "Kunci Pribadi",
"process": "Proses",
"prune": "Pangkas",
@@ -146,6 +177,7 @@
"pveVersionLow": "Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.",
"read": "Baca",
"reboot": "Reboot",
"recentConnections": "Koneksi Terkini",
"rememberPwdInMem": "Ingat kata sandi di dalam memori",
"rememberPwdInMemTip": "Digunakan untuk kontainer, menangguhkan, dll.",
"rememberWindowSize": "Ingat ukuran jendela",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Detail pesanan widget halaman",
"serverFuncBtns": "Tombol fungsi server",
"serverOrder": "Pesanan server",
"serverTabRequired": "Tab server tidak dapat dihapus",
"servers": "server",
"sftpDlPrepare": "Bersiap untuk terhubung ...",
"sftpEditorTip": "Jika kosong, gunakan editor file bawaan aplikasi. Jika ada nilai, gunakan editor server jarak jauh, misalnya `vim` (disarankan untuk mendeteksi secara otomatis sesuai `EDITOR`).",
"sftpRmrDirSummary": "Gunakan `rm -r` untuk menghapus dir di SFTP",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Suspend",
"suspendTip": "Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.",
"switchTo": "Beralih ke {val}",
"sync": "Sinkronisasi",
"syncTip": "Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.",
"system": "Sistem",
"tag": "Tag",
"tapToStartDiscovery": "Tekan tombol pencarian untuk menemukan server SSH di jaringan Anda",
"temperature": "Suhu",
"termFontSizeTip": "Pengaturan ini akan memengaruhi ukuran terminal (lebar dan tinggi). Anda dapat melakukan zoom pada halaman terminal untuk menyesuaikan ukuran font sesi saat ini.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Waktu",
"times": "Waktu",
"total": "Total",
"totalAttempts": "Total",
"traffic": "Lalu lintas",
"trySudo": "Cobalah menggunakan sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Memperbarui",
"updateIntervalEqual0": "Anda mengatur ke 0, tidak akan memperbarui secara otomatis.\nTidak dapat menghitung status CPU.",
"updateServerStatusInterval": "Interval Pembaruan Status Server",
"upload": "Mengunggah",
"upsideDown": "Terbalik",
"uptime": "Uptime",
"useCdn": "Menggunakan CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Menggunakan Podman sebagai bawaan",
"used": "Digunakan",
"view": "Tampilan",
"viewDetails": "Lihat Detail",
"viewErr": "Lihat kesalahan",
"virtKeyHelpClipboard": "Salin ke clipboard jika terminal yang dipilih tidak kosong, jika tidak, tempel isi clipboard ke terminal.",
"virtKeyHelpIME": "Menyalakan/mematikan keyboard",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "ja",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "以下の参加者に感謝します。",
"acceptBeta": "テストバージョンの更新を受け入れる",
"addSystemPrivateKeyTip": "現在秘密鍵がありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか?",
"added2List": "タスクリストに追加されました",
"addr": "アドレス",
"alreadyLastDir": "すでに最上位のディレクトリです",
"atLeastOneTab": "少なくとも1つのタブを選択する必要があります",
"authFailTip": "認証に失敗しました。パスワード/鍵/ホスト/ユーザーなどが間違っていないか確認してください。",
"autoBackupConflict": "自動バックアップは一度に一つしか開始できません",
"autoConnect": "自動接続",
"autoRun": "自動実行",
"autoUpdateHomeWidget": "ホームウィジェットを自動更新",
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
"backupPassword": "バックアップパスワード",
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
"backupPasswordWrong": "バックアップパスワードが間違っています",
"availableTabs": "利用可能なタブ",
"backupEncrypted": "バックアップは暗号化されています",
"backupNotEncrypted": "バックアップは暗号化されていません",
"backupPasswordSet": "バックアップパスワードが設定されました",
"backupPassword": "バックアップパスワード",
"backupPasswordRemoved": "バックアップパスワードが削除されました",
"backupPasswordSet": "バックアップパスワードが設定されました",
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
"backupPasswordWrong": "バックアップパスワードが間違っています",
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
"battery": "バッテリー",
"bgRun": "バックグラウンド実行",
"bgRunTip": "このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。",
"clearAllStatsContent": "すべてのサーバー接続統計を削除してもよろしいですか?この操作は元に戻せません。",
"clearAllStatsTitle": "すべての統計をクリア",
"clearServerStatsContent": "サーバー\"{serverName}\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。",
"clearServerStatsTitle": "{serverName}の統計をクリア",
"clearThisServerStats": "このサーバーの統計をクリア",
"closeAfterSave": "保存して閉じる",
"cmd": "コマンド",
"collapseUITip": "UIの長いリストをデフォルトで折りたたむかどうか",
"conn": "接続",
"connectionDetails": "接続の詳細",
"connectionStats": "接続統計",
"connectionStatsDesc": "サーバー接続成功率と履歴を表示",
"container": "コンテナ",
"containerTrySudoTip": "例アプリ内でユーザーをaaaに設定しているが、Dockerがrootユーザーでインストールされている場合、このオプションを有効にする必要があります",
"convert": "変換",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "SSHセッションを起動する際に使用されるターミナルエミュレーターを開くコマンド。",
"dirEmpty": "フォルダーが空であることを確認してください",
"disconnected": "接続が切断されました",
"discoverSshServers": "SSHサーバーの発見",
"discoveryFailed": "発見に失敗",
"discoverySettings": "発見設定",
"discoverySummary": "発見の概要",
"disk": "ディスク",
"diskHealth": "ディスクの健康状態",
"diskIgnorePath": "無視されたディスクパス",
@@ -55,9 +83,10 @@
"doubleColumnMode": "ダブルカラムモード",
"doubleColumnTip": "このオプションは機能を有効にするだけで、実際に有効にできるかどうかはデバイスの幅に依存します",
"editVirtKeys": "仮想キーを編集",
"editor": "エディター",
"editorHighlightTip": "現在のコードハイライトのパフォーマンスはかなり悪いため、改善するために無効にすることを選択できます。",
"emulator": "エミュレーター",
"enableMdns": "mDNSを有効化",
"enableMdnsDesc": "mDNS/BonjourでSSHサービスを発見",
"encode": "エンコード",
"envVars": "環境変数",
"experimentalFeature": "実験的な機能",
@@ -67,8 +96,8 @@
"fgService": "フォアグラウンドサービス",
"fgServiceTip": "有効にすると、一部の機種でクラッシュする可能性があります。無効にすると、一部の機種でバックグラウンドでのSSH接続を維持できなくなる可能性があります。システム設定でServerBoxの通知権限、バックグラウンド実行、自己起動を許可してください。",
"fileTooLarge": "ファイル '{file}' は大きすぎます '{size}'、{sizeMax} を超えています",
"finishedAt": "完了時刻",
"followSystem": "システムに従う",
"font": "フォント",
"fontSize": "フォントサイズ",
"force": "強制",
"fullScreen": "フルスクリーンモード",
@@ -79,13 +108,14 @@
"goto": "移動",
"hideTitleBar": "タイトルバーを非表示にする",
"highlight": "コードハイライト",
"homeTabs": "ホームタブ",
"homeTabsCustomizeDesc": "ホームページに表示するタブとその順序をカスタマイズします",
"homeWidgetUrlConfig": "ホームウィジェットURL設定",
"host": "ホスト",
"httpFailedWithCode": "リクエスト失敗、ステータスコード: {code}",
"ignoreCert": "証明書を無視する",
"image": "イメージ",
"imagesList": "イメージリスト",
"init": "初期化する",
"inner": "内蔵",
"install": "インストール",
"installDockerWithUrl": "最初に https://docs.docker.com/engine/install dockerをインストールしてください",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "エラー時に前回のサーバーステータスを保持",
"keepStatusWhenErrTip": "スクリプトの実行エラーに限ります",
"keyAuth": "キー認証",
"lastFailure": "最後の失敗",
"lastSuccess": "最後の成功",
"letterCache": "文字キャッシング",
"letterCacheTip": "無効にすることを推奨しますが、無効にした後はCJK文字を入力することができなくなります。",
"license": "オープンソースライセンス",
"location": "場所",
"loss": "パケットロス",
"madeWithLove": "{myGithub}によって❤️で作成済み",
"manual": "マニュアル",
"max": "最大",
"maxConcurrency": "最大同時実行数",
"maxRetryCount": "サーバーの再接続試行回数",
"maxRetryCountEqual0": "無限に再試行します",
"min": "最小",
@@ -115,6 +146,7 @@
"net": "ネットワーク",
"netViewType": "ネットワークビュータイプ",
"newContainer": "新しいコンテナを作成",
"noConnectionStatsData": "接続統計データがありません",
"noLineChart": "折れ線グラフを使用しない",
"noLineChartForCpu": "CPUに折れ線グラフを使わない",
"noPrivateKeyTip": "秘密鍵が存在しません。削除されたか、設定ミスがある可能性があります。",
@@ -136,7 +168,6 @@
"plugInType": "挿入タイプ",
"port": "ポート",
"preferDiskAmount": "ディスク容量を優先的に表示",
"preview": "プレビュー",
"privateKey": "秘密鍵",
"process": "プロセス",
"prune": "剪定する",
@@ -146,6 +177,7 @@
"pveVersionLow": "この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。",
"read": "読み取り",
"reboot": "再起動",
"recentConnections": "最近の接続",
"rememberPwdInMem": "メモリにパスワードを記憶する",
"rememberPwdInMemTip": "コンテナ、一時停止などに使用されます。",
"rememberWindowSize": "ウィンドウサイズを記憶する",
@@ -166,6 +198,8 @@
"serverDetailOrder": "詳細ページのウィジェット順序",
"serverFuncBtns": "サーバー機能ボタン",
"serverOrder": "サーバー順序",
"serverTabRequired": "サーバータブは削除できません",
"servers": "サーバー",
"sftpDlPrepare": "サーバーへの接続を準備中...",
"sftpEditorTip": "空の場合は、アプリ内蔵のファイルエディタを使用します。値がある場合は、リモートサーバーのエディタ(例:`vim`)を使用します(`EDITOR` に従って自動検出することをお勧めします)。",
"sftpRmrDirSummary": "SFTPで`rm -r`を使用してフォルダーを削除",
@@ -180,6 +214,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": "仮想キーの自動オフ",
@@ -193,10 +240,10 @@
"suspend": "中断",
"suspendTip": "suspend機能はroot権限とsystemdのサポートが必要です。",
"switchTo": "{val}に切り替える",
"sync": "同期する",
"syncTip": "再起動が必要な場合があります。一部の変更はその後に有効になります。",
"system": "システム",
"tag": "タグ",
"tapToStartDiscovery": "検索ボタンをタップしてネットワーク上のSSHサーバーを発見",
"temperature": "温度",
"termFontSizeTip": "この設定は端末のサイズ(幅と高さ)に影響します。現在のセッションのフォントサイズを調整するために、端末ページを拡大縮小できます。",
"terminal": "ターミナル",
@@ -207,6 +254,7 @@
"time": "時間",
"times": "回",
"total": "合計",
"totalAttempts": "総計",
"traffic": "トラフィック",
"trySudo": "sudoを試みる",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "更新",
"updateIntervalEqual0": "0に設定すると、サーバーの状態は自動的に更新されず、CPU使用率も計算できません。",
"updateServerStatusInterval": "サーバー状態の更新間隔",
"upload": "アップロード",
"upsideDown": "上下逆転",
"uptime": "稼働時間",
"useCdn": "CDNの使用",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "デフォルトでPodmanを使用",
"used": "使用済み",
"view": "ビュー",
"viewDetails": "詳細を表示",
"viewErr": "エラーを表示",
"virtKeyHelpClipboard": "端末に選択された文字がある場合は、選択された文字をクリップボードにコピーします。そうでない場合は、クリップボードの内容を端末に貼り付けます。",
"virtKeyHelpIME": "キーボードのオン/オフ",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "nl",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Met dank aan de volgende mensen die hebben deelgenomen aan.",
"acceptBeta": "Accepteer testversie-updates",
"addSystemPrivateKeyTip": "Er is momenteel geen privésleutel, wilt u degene toevoegen die bij het systeem wordt geleverd (~/.ssh/id_rsa)?",
"added2List": "Toegevoegd aan takenlijst",
"addr": "Adres",
"alreadyLastDir": "Al in de laatst gebruikte map.",
"atLeastOneTab": "Er moet minimaal één tabblad worden geselecteerd",
"authFailTip": "Authenticatie mislukt, controleer of het wachtwoord/sleutel/host/gebruiker, enz., incorrect zijn.",
"autoBackupConflict": "Er kan slechts één automatische back-up tegelijk worden ingeschakeld.",
"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",
"availableTabs": "Beschikbare tabbladen",
"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\".",
"clearAllStatsContent": "Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.",
"clearAllStatsTitle": "Alle statistieken wissen",
"clearServerStatsContent": "Weet u zeker dat u de verbindingsstatistieken voor server \"{serverName}\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.",
"clearServerStatsTitle": "Statistieken van {serverName} wissen",
"clearThisServerStats": "Statistieken van deze server wissen",
"closeAfterSave": "Opslaan en sluiten",
"cmd": "Opdracht",
"collapseUITip": "Of lange lijsten in de UI standaard moeten worden ingeklapt",
"conn": "Verbinding",
"connectionDetails": "Verbindingsdetails",
"connectionStats": "Verbindingsstatistieken",
"connectionStatsDesc": "Bekijk server verbindingssucces ratio en geschiedenis",
"container": "Container",
"containerTrySudoTip": "Bijvoorbeeld: in de app is de gebruiker ingesteld op aaa, maar Docker is geïnstalleerd onder de rootgebruiker. In dit geval moet u deze optie inschakelen.",
"convert": "Converteren",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Opdracht die wordt gebruikt om de terminalemulator te openen bij het starten van SSH-sessies.",
"dirEmpty": "Zorg ervoor dat de map leeg is.",
"disconnected": "Verbroken",
"discoverSshServers": "SSH-servers ontdekken",
"discoveryFailed": "Ontdekking mislukt",
"discoverySettings": "Ontdekkingsinstellingen",
"discoverySummary": "Ontdekkingssamenvatting",
"disk": "Schijf",
"diskHealth": "Schijfgezondheid",
"diskIgnorePath": "Pad negeren voor schijf",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Dubbele kolommodus",
"doubleColumnTip": "Deze optie schakelt alleen de functie in, of deze daadwerkelijk kan worden ingeschakeld, hangt af van de breedte van het apparaat",
"editVirtKeys": "Virtuele toetsen bewerken",
"editor": "Editor",
"editorHighlightTip": "De huidige codehighlighting-prestaties zijn slechter en kunnen optioneel worden uitgeschakeld om te verbeteren.",
"emulator": "Emulator",
"enableMdns": "mDNS inschakelen",
"enableMdnsDesc": "Gebruik mDNS/Bonjour om SSH-services te ontdekken",
"encode": "Coderen",
"envVars": "Omgevingsvariabele",
"experimentalFeature": "Experimentele functie",
@@ -67,8 +96,8 @@
"fgService": "Voorgrondservice",
"fgServiceTip": "Na het inschakelen kunnen sommige apparaatmodellen crashen. Uitschakelen kan ertoe leiden dat sommige modellen SSH-verbindingen niet op de achtergrond kunnen behouden. Sta ServerBox notificatierechten, achtergronduitvoering en zelf-ontwaken toe in systeeminstellingen.",
"fileTooLarge": "Bestand '{file}' te groot {size}, max {sizeMax}",
"finishedAt": "Voltooid om",
"followSystem": "Volg systeem",
"font": "Lettertype",
"fontSize": "Lettergrootte",
"force": "Forceer",
"fullScreen": "Volledig schermmodus",
@@ -79,13 +108,14 @@
"goto": "Ga naar",
"hideTitleBar": "Titelbalk verbergen",
"highlight": "Code-highlight",
"homeTabs": "Home-tabbladen",
"homeTabsCustomizeDesc": "Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde",
"homeWidgetUrlConfig": "Home-widget-url configureren",
"host": "Host",
"httpFailedWithCode": "verzoek mislukt, statuscode: {code}",
"ignoreCert": "Certificaat negeren",
"image": "Afbeelding",
"imagesList": "Lijst met afbeeldingen",
"init": "Initialiseren",
"inner": "Intern",
"install": "Installeren",
"installDockerWithUrl": "Installeer eerst docker via https://docs.docker.com/engine/install.",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Behoud de laatste serverstatus",
"keepStatusWhenErrTip": "Alleen in geval van een fout tijdens de scriptuitvoering",
"keyAuth": "Sleutelauthenticatie",
"lastFailure": "Laatst gefaald",
"lastSuccess": "Laatst succesvol",
"letterCache": "Lettercaching",
"letterCacheTip": "Aanbevolen om uit te schakelen, maar na het uitschakelen is het niet mogelijk om CJK-tekens in te voeren.",
"license": "Licentie",
"location": "Locatie",
"loss": "verlies",
"madeWithLove": "Gemaakt met ❤️ door {myGithub}",
"manual": "Handleiding",
"max": "max",
"maxConcurrency": "Maximale gelijktijdigheid",
"maxRetryCount": "Aantal serverherverbindingen",
"maxRetryCountEqual0": "Zal opnieuw blijven proberen.",
"min": "min",
@@ -115,6 +146,7 @@
"net": "Netwerk",
"netViewType": "Netweergavetype",
"newContainer": "Nieuwe container",
"noConnectionStatsData": "Geen verbindingsstatistiekgegevens",
"noLineChart": "lijndiagrammen gebruiken",
"noLineChartForCpu": "Gebruik geen lijndiagrammen voor CPU",
"noPrivateKeyTip": "De privésleutel bestaat niet, deze is mogelijk verwijderd of er is een configuratiefout.",
@@ -136,7 +168,6 @@
"plugInType": "Invoegingstype",
"port": "Poort",
"preferDiskAmount": "Geef de schijfcapaciteit prioriteit bij weergave",
"preview": "Voorbeeld",
"privateKey": "Privésleutel",
"process": "Proces",
"prune": "Snoeien",
@@ -146,6 +177,7 @@
"pveVersionLow": "Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.",
"read": "Lezen",
"reboot": "Herstart",
"recentConnections": "Recente verbindingen",
"rememberPwdInMem": "Wachtwoord onthouden in geheugen",
"rememberPwdInMemTip": "Gebruikt voor containers, opschorting, enz.",
"rememberWindowSize": "Venstergrootte onthouden",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Volgorde van widget op detailpagina",
"serverFuncBtns": "Server functieknoppen",
"serverOrder": "Servervolgorde",
"serverTabRequired": "Servertabblad kan niet worden verwijderd",
"servers": "servers",
"sftpDlPrepare": "Voorbereiden om verbinding te maken...",
"sftpEditorTip": "Indien leeg, gebruik de ingebouwde bestandseditor van de app. Indien een waarde aanwezig is, gebruik de editor van de externe server, bijvoorbeeld `vim` (aanbevolen om automatisch te detecteren volgens `EDITOR`).",
"sftpRmrDirSummary": "Gebruik `rm -r` om een map te verwijderen in SFTP.",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Ophangen",
"suspendTip": "De opschortfunctie vereist rootrechten en systemd-ondersteuning.",
"switchTo": "Overschakelen naar {val}",
"sync": "Sync",
"syncTip": "Een herstart kan nodig zijn voor sommige wijzigingen om van kracht te worden.",
"system": "Systeem",
"tag": "Labels",
"tapToStartDiscovery": "Tik op de zoekknop om SSH-servers op uw netwerk te ontdekken",
"temperature": "Temperatuur",
"termFontSizeTip": "Deze instelling heeft invloed op de terminalgrootte (breedte en hoogte). U kunt inzoomen op de terminalpagina om de lettergrootte van de huidige sessie aan te passen.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Tijd",
"times": "Keer",
"total": "Totaal",
"totalAttempts": "Totaal",
"traffic": "Verkeer",
"trySudo": "Probeer sudo te gebruiken",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Bijwerken",
"updateIntervalEqual0": "Het staat op 0, het zal niet automatisch bijwerken\nCPU status kan niet berekend worden.",
"updateServerStatusInterval": "Interne server status bijwerking interval",
"upload": "Upload",
"upsideDown": "Ondersteboven",
"uptime": "Uptime",
"useCdn": "Gebruikt CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Valt terug op Podman",
"used": "Gebruikt",
"view": "Weergave",
"viewDetails": "Details bekijken",
"viewErr": "Zie foutmelding",
"virtKeyHelpClipboard": "Kopiëren naar het klembord als de geselecteerde terminal niet leeg is, anders de inhoud van het klembord plakken in de terminal.",
"virtKeyHelpIME": "Toetsenbord aan/uit zetten",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "pt",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Agradecimentos a todos os participantes.",
"acceptBeta": "Aceitar atualizações da versão de teste",
"addSystemPrivateKeyTip": "Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?",
"added2List": "Adicionado à lista de tarefas",
"addr": "Endereço",
"alreadyLastDir": "Já é o diretório mais alto",
"atLeastOneTab": "Pelo menos uma aba deve ser selecionada",
"authFailTip": "Autenticação falhou, por favor verifique se a senha/chave/host/usuário, etc., estão incorretos.",
"autoBackupConflict": "Apenas um backup automático pode ser ativado por vez",
"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",
"availableTabs": "Abas disponíveis",
"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'.",
"clearAllStatsContent": "Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.",
"clearAllStatsTitle": "Limpar todas as estatísticas",
"clearServerStatsContent": "Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"{serverName}\"? Esta ação não pode ser desfeita.",
"clearServerStatsTitle": "Limpar estatísticas de {serverName}",
"clearThisServerStats": "Limpar estatísticas deste servidor",
"closeAfterSave": "Salvar e fechar",
"cmd": "Comando",
"collapseUITip": "Deve colapsar listas longas na UI por padrão?",
"conn": "Conectar",
"connectionDetails": "Detalhes da conexão",
"connectionStats": "Estatísticas de conexão",
"connectionStatsDesc": "Ver taxa de sucesso de conexão do servidor e histórico",
"container": "Contêiner",
"containerTrySudoTip": "Por exemplo: se o usuário for definido como aaa dentro do app, mas o Docker estiver instalado sob o usuário root, esta opção precisará ser ativada",
"convert": "Converter",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Comando usado para abrir o emulador de terminal ao iniciar sessões SSH.",
"dirEmpty": "Certifique-se de que a pasta está vazia",
"disconnected": "Desconectado",
"discoverSshServers": "Descobrir servidores SSH",
"discoveryFailed": "Descoberta falhou",
"discoverySettings": "Configurações de descoberta",
"discoverySummary": "Resumo da descoberta",
"disk": "Disco",
"diskHealth": "Saúde do disco",
"diskIgnorePath": "Caminhos de disco ignorados",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Modo de coluna dupla",
"doubleColumnTip": "Esta opção apenas ativa a funcionalidade, se ela será ativada depende também da largura do dispositivo",
"editVirtKeys": "Editar teclas virtuais",
"editor": "Editor",
"editorHighlightTip": "O desempenho do destaque de código atualmente é ruim, pode optar por desativá-lo para melhorar.",
"emulator": "Emulador",
"enableMdns": "Ativar mDNS",
"enableMdnsDesc": "Usar mDNS/Bonjour para descobrir serviços SSH",
"encode": "Codificar",
"envVars": "Variável de ambiente",
"experimentalFeature": "Recurso experimental",
@@ -67,8 +96,8 @@
"fgService": "Serviço em primeiro plano",
"fgServiceTip": "Após ativar, alguns modelos de dispositivos podem travar. Desativar pode fazer com que alguns modelos não consigam manter conexões SSH em segundo plano. Por favor, permita as permissões de notificação do ServerBox, execução em segundo plano e auto-despertar nas configurações do sistema.",
"fileTooLarge": "Arquivo '{file}' muito grande '{size}', excedendo {sizeMax}",
"finishedAt": "Terminado em",
"followSystem": "Seguir sistema",
"font": "Fonte",
"fontSize": "Tamanho da fonte",
"force": "Forçar",
"fullScreen": "Modo tela cheia",
@@ -79,13 +108,14 @@
"goto": "Ir para",
"hideTitleBar": "Ocultar barra de título",
"highlight": "Destaque de código",
"homeTabs": "Abas iniciais",
"homeTabsCustomizeDesc": "Personalize quais abas aparecem na página inicial e sua ordem",
"homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial",
"host": "Host",
"httpFailedWithCode": "Falha na solicitação, código de status: {code}",
"ignoreCert": "Ignorar certificado",
"image": "Imagem",
"imagesList": "Lista de imagens",
"init": "Inicializar",
"inner": "Interno",
"install": "Instalar",
"installDockerWithUrl": "Por favor, instale o Docker primeiro em https://docs.docker.com/engine/install",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Manter o status anterior do servidor",
"keepStatusWhenErrTip": "Limitado a erros de execução de scripts",
"keyAuth": "Autenticação por chave",
"lastFailure": "Última falha",
"lastSuccess": "Último sucesso",
"letterCache": "Cache de letras",
"letterCacheTip": "Recomendado desativar, mas após desativar, será impossível inserir caracteres CJK.",
"license": "Licença de código aberto",
"location": "Localização",
"loss": "Taxa de perda",
"madeWithLove": "Feito com ❤️ por {myGithub}",
"manual": "Manual",
"max": "Máximo",
"maxConcurrency": "Concorrência máxima",
"maxRetryCount": "Número de tentativas de reconexão com o servidor",
"maxRetryCountEqual0": "Irá tentar indefinidamente",
"min": "Mínimo",
@@ -115,6 +146,7 @@
"net": "Rede",
"netViewType": "Tipo de visualização de rede",
"newContainer": "Novo contêiner",
"noConnectionStatsData": "Não há dados de estatísticas de conexão",
"noLineChart": "Não usar gráficos de linha",
"noLineChartForCpu": "Não utilizar gráficos de linhas para a CPU",
"noPrivateKeyTip": "A chave privada não existe, pode ter sido deletada ou há um erro de configuração.",
@@ -136,7 +168,6 @@
"plugInType": "Tipo de Inserção",
"port": "Porta",
"preferDiskAmount": "Priorizar a exibição da capacidade do disco",
"preview": "Pré-visualização",
"privateKey": "Chave privada",
"process": "Processo",
"prune": "Podar",
@@ -146,6 +177,7 @@
"pveVersionLow": "Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.",
"read": "Leitura",
"reboot": "Reiniciar",
"recentConnections": "Conexões recentes",
"rememberPwdInMem": "Lembrar senha na memória",
"rememberPwdInMemTip": "Usado para contêineres, suspensão, etc.",
"rememberWindowSize": "Lembrar o tamanho da janela",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Ordem dos componentes na página de detalhes do servidor",
"serverFuncBtns": "Botões de função do servidor",
"serverOrder": "Ordem do servidor",
"serverTabRequired": "A aba do servidor não pode ser removida",
"servers": "servidores",
"sftpDlPrepare": "Preparando para conectar ao servidor...",
"sftpEditorTip": "Se vazio, use o editor de arquivos integrado do aplicativo. Se houver um valor, use o editor do servidor remoto, por exemplo, `vim` (recomendado detectar automaticamente de acordo com `EDITOR`).",
"sftpRmrDirSummary": "Usar `rm -r` em SFTP para excluir pastas",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Suspender",
"suspendTip": "A função de suspensão requer permissões de root e suporte do systemd.",
"switchTo": "Mudar para {val}",
"sync": "Sincronizar",
"syncTip": "Pode ser necessário reiniciar para algumas mudanças surtirem efeito.",
"system": "Sistema",
"tag": "Tag",
"tapToStartDiscovery": "Toque no botão de pesquisa para descobrir servidores SSH na sua rede",
"temperature": "Temperatura",
"termFontSizeTip": "Esta configuração afetará o tamanho do terminal (largura e altura). Você pode dar zoom na página do terminal para ajustar o tamanho da fonte da sessão atual.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Tempo",
"times": "Vezes",
"total": "Total",
"totalAttempts": "Total",
"traffic": "Tráfego",
"trySudo": "Tentar usar sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Atualizar",
"updateIntervalEqual0": "Se definido como 0, o estado do servidor não será atualizado automaticamente.\nE o uso da CPU não poderá ser calculado.",
"updateServerStatusInterval": "Intervalo de atualização do estado do servidor",
"upload": "Upload",
"upsideDown": "Inverter verticalmente",
"uptime": "Tempo de atividade",
"useCdn": "Utilizando CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Usar Podman por padrão",
"used": "Usado",
"view": "Visualização",
"viewDetails": "Ver detalhes",
"viewErr": "Ver erro",
"virtKeyHelpClipboard": "Se houver texto selecionado no terminal, copia para a área de transferência, caso contrário, cola o conteúdo da área de transferência no terminal.",
"virtKeyHelpIME": "Ligar/desligar o teclado",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "ru",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Благодарности всем участникам.",
"acceptBeta": "Принять обновления тестовой версии",
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
"added2List": "Добавлено в список задач",
"addr": "Адрес",
"alreadyLastDir": "Уже в корневом каталоге",
"atLeastOneTab": "Должна быть выбрана хотя бы одна вкладка",
"authFailTip": "Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.",
"autoBackupConflict": "Может быть включено только одно автоматическое резервное копирование",
"autoConnect": "Автоматическое подключение",
"autoRun": "Автозапуск",
"autoUpdateHomeWidget": "Автоматическое обновление виджета на главном экране",
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
"backupPassword": "Пароль резервной копии",
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
"backupPasswordWrong": "Неверный пароль резервной копии",
"availableTabs": "Доступные вкладки",
"backupEncrypted": "Резервная копия зашифрована",
"backupNotEncrypted": "Резервная копия не зашифрована",
"backupPasswordSet": "Пароль резервной копии установлен",
"backupPassword": "Пароль резервной копии",
"backupPasswordRemoved": "Пароль резервной копии удален",
"backupPasswordSet": "Пароль резервной копии установлен",
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
"backupPasswordWrong": "Неверный пароль резервной копии",
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
"battery": "Батарея",
"bgRun": "Работа в фоновом режиме",
"bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».",
"clearAllStatsContent": "Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.",
"clearAllStatsTitle": "Очистить всю статистику",
"clearServerStatsContent": "Вы уверены, что хотите очистить статистику соединений для сервера \"{serverName}\"? Это действие не может быть отменено.",
"clearServerStatsTitle": "Очистить статистику {serverName}",
"clearThisServerStats": "Очистить статистику этого сервера",
"closeAfterSave": "Сохранить и закрыть",
"cmd": "Команда",
"collapseUITip": "Свернуть длинные списки в UI по умолчанию",
"conn": "Подключение",
"connectionDetails": "Детали соединения",
"connectionStats": "Статистика соединений",
"connectionStatsDesc": "Просмотр коэффициента успешности подключения к серверу и истории",
"container": "Контейнер",
"containerTrySudoTip": "Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию",
"convert": "Конвертировать",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Команда для открытия эмулятора терминала при запуске SSH-сеансов.",
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
"disconnected": "Отключено",
"discoverSshServers": "Обнаружить SSH серверы",
"discoveryFailed": "Обнаружение не удалось",
"discoverySettings": "Настройки обнаружения",
"discoverySummary": "Сводка обнаружения",
"disk": "Диск",
"diskHealth": "Состояние диска",
"diskIgnorePath": "Игнорировать путь к диску",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Режим двойной колонки",
"doubleColumnTip": "Эта опция лишь включает функцию; фактическое применение зависит от ширины устройства",
"editVirtKeys": "Редактировать виртуальные клавиши",
"editor": "Редактор",
"editorHighlightTip": "Текущая производительность подсветки кода неудовлетворительна, можно отключить для улучшения.",
"emulator": "Эмулятор",
"enableMdns": "Включить mDNS",
"enableMdnsDesc": "Использовать mDNS/Bonjour для обнаружения SSH служб",
"encode": "Кодировать",
"envVars": "Переменная окружения",
"experimentalFeature": "Экспериментальная функция",
@@ -67,8 +96,8 @@
"fgService": "Сервис переднего плана",
"fgServiceTip": "После включения некоторые модели устройств могут вылетать. Отключение может привести к тому, что некоторые модели не смогут поддерживать SSH-соединения в фоновом режиме. Пожалуйста, разрешите ServerBox права на уведомления, фоновую работу и самопробуждение в системных настройках.",
"fileTooLarge": "Файл '{file}' слишком большой '{size}', превышает {sizeMax}",
"finishedAt": "Завершено в",
"followSystem": "Следовать за системой",
"font": "Шрифт",
"fontSize": "Размер шрифта",
"force": "Принудительно",
"fullScreen": "Полноэкранный режим",
@@ -79,13 +108,14 @@
"goto": "Перейти к",
"hideTitleBar": "Скрыть заголовок",
"highlight": "Подсветка кода",
"homeTabs": "Вкладки дома",
"homeTabsCustomizeDesc": "Настройте, какие вкладки появляются на главной странице и их порядок",
"homeWidgetUrlConfig": "Конфигурация URL виджета домашнего экрана",
"host": "Хост",
"httpFailedWithCode": "ошибка запроса, код: {code}",
"ignoreCert": "Игнорировать сертификат",
"image": "Образ",
"imagesList": "Список образов",
"init": "Инициализировать",
"inner": "Встроенный",
"install": "установить",
"installDockerWithUrl": "Пожалуйста, сначала установите Docker по адресу https://docs.docker.com/engine/install",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Сохранять статус сервера при ошибке",
"keepStatusWhenErrTip": "Применимо только в случае ошибки выполнения скрипта",
"keyAuth": "Аутентификация по ключу",
"lastFailure": "Последний сбой",
"lastSuccess": "Последний успех",
"letterCache": "Кэширование букв",
"letterCacheTip": "Рекомендуется отключить, но после отключения будет невозможно вводить символы CJK.",
"license": "Лицензия",
"location": "Местоположение",
"loss": "Потери пакетов",
"madeWithLove": "Создано с ❤️ by {myGithub}",
"manual": "Вручную",
"max": "максимум",
"maxConcurrency": "Максимальная параллельность",
"maxRetryCount": "Максимальное количество попыток переподключения к серверу",
"maxRetryCountEqual0": "Будет бесконечно пытаться переподключиться",
"min": "минимум",
@@ -115,6 +146,7 @@
"net": "Сеть",
"netViewType": "Тип визуализации сети",
"newContainer": "Создать контейнер",
"noConnectionStatsData": "Нет данных статистики соединений",
"noLineChart": "Не использовать линейные графики",
"noLineChartForCpu": "Не используйте линейные графики для ЦП",
"noPrivateKeyTip": "Приватный ключ не существует, возможно, он был удален или есть ошибка в настройках.",
@@ -136,7 +168,6 @@
"plugInType": "Тип вставки",
"port": "Порт",
"preferDiskAmount": "Приоритетное отображение объёма диска",
"preview": "Предпросмотр",
"privateKey": "Приватный ключ",
"process": "Процесс",
"prune": "Обрезать",
@@ -146,6 +177,7 @@
"pveVersionLow": "Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.",
"read": "Чтение",
"reboot": "Перезагрузка",
"recentConnections": "Недавние соединения",
"rememberPwdInMem": "Запомнить пароль в памяти",
"rememberPwdInMemTip": "Используется для контейнеров, приостановки и т. д.",
"rememberWindowSize": "Запомнить размер окна",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Порядок элементов на странице деталей сервера",
"serverFuncBtns": "Кнопки функций сервера",
"serverOrder": "Порядок серверов",
"serverTabRequired": "Вкладку сервера нельзя удалить",
"servers": "серверов",
"sftpDlPrepare": "Подготовка подключения...",
"sftpEditorTip": "Если пусто, используйте встроенный редактор файлов приложения. Если значение указано, используйте редактор удаленного сервера, например, `vim` (рекомендуется автоматически определять согласно `EDITOR`).",
"sftpRmrDirSummary": "Использовать `rm -r` в SFTP для удаления папок",
@@ -180,6 +214,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": "Автоматическое переключение виртуальных клавиш",
@@ -193,10 +240,10 @@
"suspend": "Приостановить",
"suspendTip": "Функция приостановки требует прав root и поддержки systemd.",
"switchTo": "Переключиться на {val}",
"sync": "Синхронизировать",
"syncTip": "Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.",
"system": "Система",
"tag": "Теги",
"tapToStartDiscovery": "Нажмите кнопку поиска, чтобы обнаружить SSH серверы в вашей сети",
"temperature": "Температура",
"termFontSizeTip": "Эта настройка повлияет на размер терминала (ширина и высота). Вы можете масштабировать страницу терминала, чтобы调整 размер шрифта текущей сессии.",
"terminal": "Терминал",
@@ -207,6 +254,7 @@
"time": "Время",
"times": "Раз",
"total": "Всего",
"totalAttempts": "Общее",
"traffic": "Трафик",
"trySudo": "Попробовать использовать sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Обновление",
"updateIntervalEqual0": "Если установлено 0, статус сервера не будет автоматически обновляться.\nТакже не будет рассчитано использование ЦП.",
"updateServerStatusInterval": "Интервал обновления статуса сервера",
"upload": "Загрузить",
"upsideDown": "Перевернуть",
"uptime": "Время работы",
"useCdn": "Использование CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Использовать Podman по умолчанию",
"used": "Использовано",
"view": "Вид",
"viewDetails": "Просмотр деталей",
"viewErr": "Просмотр ошибок",
"virtKeyHelpClipboard": "Если в терминале выделен текст, то он копируется в буфер обмена, в противном случае содержимое буфера вставляется в терминал.",
"virtKeyHelpIME": "Включить/выключить клавиатуру",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "tr",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Aşağıdaki katılımcılara teşekkürler.",
"acceptBeta": "Beta sürüm güncellemelerini kabul et",
"addSystemPrivateKeyTip": "Şu anda özel anahtarlar mevcut değil, sistemle birlikte gelen anahtarı (~/.ssh/id_rsa) eklemek ister misiniz?",
"added2List": "Görev listesine eklendi",
"addr": "Adres",
"alreadyLastDir": "Zaten son dizindesiniz.",
"atLeastOneTab": "En az bir sekme seçilmelidir",
"authFailTip": "Kimlik doğrulama başarısız oldu, lütfen kimlik bilgilerinin doğru olup olmadığını kontrol edin",
"autoBackupConflict": "Aynı anda yalnızca bir otomatik yedekleme açık olabilir.",
"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ı",
"availableTabs": "Mevcut Sekmeler",
"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.",
"clearAllStatsContent": "Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"clearAllStatsTitle": "Tüm İstatistikleri Temizle",
"clearServerStatsContent": "\"{serverName}\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"clearServerStatsTitle": "{serverName} İstatistiklerini Temizle",
"clearThisServerStats": "Bu Sunucu İstatistiklerini Temizle",
"closeAfterSave": "Kaydet ve kapat",
"cmd": "Komut",
"collapseUITip": "Arayüzde uzun listelerin varsayılan olarak daraltılıp daraltılmayacağı",
"conn": "Bağlantı",
"connectionDetails": "Bağlantı Detayları",
"connectionStats": "Bağlantı İstatistikleri",
"connectionStatsDesc": "Sunucu bağlantı başarı oranını ve geçmişi görüntüle",
"container": "Konteyner",
"containerTrySudoTip": "Örneğin: Uygulamada kullanıcı aaa olarak ayarlanmış, ancak Docker root kullanıcısı altında kurulmuş. Bu durumda bu seçeneği etkinleştirmeniz gerekir.",
"convert": "Dönüştür",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "SSH oturumları başlatılırken terminal öykünücüsünü açmak için kullanılan komut.",
"dirEmpty": "Klasörün boş olduğundan emin olun.",
"disconnected": "Bağlantı kesildi",
"discoverSshServers": "SSH Sunucularını Keşfet",
"discoveryFailed": "Keşif başarısız",
"discoverySettings": "Keşif Ayarları",
"discoverySummary": "Keşif Özeti",
"disk": "Disk",
"diskHealth": "Disk sağlığı",
"diskIgnorePath": "Disk için yok sayılan yol",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Çift sütun modu",
"doubleColumnTip": "Bu seçenek yalnızca özelliği etkinleştirir, gerçekten etkinleşip etkinleşmeyeceği cihazın genişliğine bağlıdır",
"editVirtKeys": "Sanal tuşları düzenle",
"editor": "Düzenleyici",
"editorHighlightTip": "Mevcut kod vurgulama performansı ideal değil ve isteğe bağlı olarak kapatılabilir.",
"emulator": "Emülatör",
"enableMdns": "mDNS'yi Etkinleştir",
"enableMdnsDesc": "SSH hizmetlerini keşfetmek için mDNS/Bonjour kullan",
"encode": "Kodla",
"envVars": "Ortam değişkeni",
"experimentalFeature": "Deneysel özellik",
@@ -67,8 +96,8 @@
"fgService": "Ön Plan Servisi",
"fgServiceTip": "Etkinleştirildikten sonra bazı cihaz modellerinde çökme olabilir. Devre dışı bırakmak, bazı modellerde SSH bağlantılarının arka planda sürdürülememesine neden olabilir. Lütfen sistem ayarlarında ServerBox bildirim izinlerini, arka planda çalışmayı ve otomatik uyanmayı etkinleştirin.",
"fileTooLarge": "'{file}' dosyası çok büyük {size}, maksimum {sizeMax}",
"finishedAt": "Tamamlandı:",
"followSystem": "Sistemi takip et",
"font": "Yazı tipi",
"fontSize": "Yazı tipi boyutu",
"force": "Zorla",
"fullScreen": "Tam ekran modu",
@@ -79,13 +108,14 @@
"goto": "Git",
"hideTitleBar": "Başlık çubuğunu gizle",
"highlight": "Kod vurgulama",
"homeTabs": "Ana Sayfa Sekmeleri",
"homeTabsCustomizeDesc": "Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin",
"homeWidgetUrlConfig": "Ana ekran bileşeni URL'sini yapılandır",
"host": "Ana bilgisayar",
"httpFailedWithCode": "İstek başarısız oldu, durum kodu: {code}",
"ignoreCert": "Sertifikayı yok say",
"image": "Görüntü",
"imagesList": "Görüntü listesi",
"init": "Başlat",
"inner": "İç",
"install": "Kur",
"installDockerWithUrl": "Lütfen önce https://docs.docker.com/engine/install adresinden Docker'ı kurun.",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Son sunucu durumunu koru",
"keepStatusWhenErrTip": "Yalnızca betik yürütülmesi sırasında bir hata olduğunda",
"keyAuth": "Anahtar Kimlik Doğrulama",
"lastFailure": "Son Başarısızlık",
"lastSuccess": "Son Başarı",
"letterCache": "Harf önbelleği",
"letterCacheTip": "Devre dışı bırakılması önerilir, ancak devre dışı bırakıldığında CJK karakterlerini girmek mümkün olmayacaktır.",
"license": "Lisans",
"location": "Konum",
"loss": "Kayıp",
"madeWithLove": "{myGithub} tarafından ❤️ ile yapıldı",
"manual": "Manuel",
"max": "maks",
"maxConcurrency": "Maksimum Eşzamanlılık",
"maxRetryCount": "Sunucu yeniden bağlantı sayısı",
"maxRetryCountEqual0": "Tekrar tekrar deneyecek.",
"min": "min",
@@ -115,6 +146,7 @@
"net": "Ağ",
"netViewType": "Ağ görüntüleme türü",
"newContainer": "Yeni konteyner",
"noConnectionStatsData": "Bağlantı istatistik verisi yok",
"noLineChart": "Çizgi grafikleri kullanma",
"noLineChartForCpu": "CPU için çizgi grafikleri kullanma",
"noPrivateKeyTip": "Özel anahtar mevcut değil, silinmiş olabilir veya yapılandırma hatası vardır.",
@@ -136,7 +168,6 @@
"plugInType": "Eklenti Türü",
"port": "Port",
"preferDiskAmount": "Disk kapasitesini öncelikli olarak göster",
"preview": "Önizleme",
"privateKey": "Özel Anahtar",
"process": "İşlem",
"prune": "Budamak",
@@ -146,6 +177,7 @@
"pveVersionLow": "Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.",
"read": "Oku",
"reboot": "Yeniden başlat",
"recentConnections": "Son Bağlantılar",
"rememberPwdInMem": "Şifreyi bellekte hatırla",
"rememberPwdInMemTip": "Konteynerler, askıya alma vb. için kullanılır.",
"rememberWindowSize": "Pencere boyutunu hatırla",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Ayrıntı sayfası bileşen sırası",
"serverFuncBtns": "Sunucu işlev düğmeleri",
"serverOrder": "Sunucu sırası",
"serverTabRequired": "Sunucu sekmesi kaldırılamaz",
"servers": "sunucu",
"sftpDlPrepare": "Bağlantı hazırlanıyor...",
"sftpEditorTip": "Boşsa, uygulamanın yerleşik dosya düzenleyicisi kullanılır. Bir değer varsa, uzak sunucunun düzenleyicisi kullanılır, örn. `vim` (otomatik olarak `EDITOR`'a göre algılanması önerilir).",
"sftpRmrDirSummary": "SFTP'de bir klasörü silmek için `rm -r` kullan.",
@@ -180,6 +214,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",
@@ -193,10 +240,10 @@
"suspend": "Askıya al",
"suspendTip": "Askıya alma işlevi, root izni ve systemd desteği gerektirir.",
"switchTo": "{val}'a geç",
"sync": "Senkronize et",
"syncTip": "Bazı değişikliklerin etkili olması için yeniden başlatma gerekebilir.",
"system": "Sistem",
"tag": "Etiketler",
"tapToStartDiscovery": "Ağınızdaki SSH sunucularını keşfetmek için arama düğmesine dokunun",
"temperature": "Sıcaklık",
"termFontSizeTip": "Bu ayar terminal boyutunu (genişlik ve yükseklik) etkiler. Terminal sayfasında yakınlaştırarak mevcut oturumun yazı tipi boyutunu ayarlayabilirsiniz.",
"terminal": "Terminal",
@@ -207,6 +254,7 @@
"time": "Zaman",
"times": "Kez",
"total": "Toplam",
"totalAttempts": "Toplam",
"traffic": "Trafik",
"trySudo": "Sudo ile dene",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Güncelle",
"updateIntervalEqual0": "0 olarak ayarladınız, otomatik güncelleme yapılmayacak.\nCPU durumu hesaplanamaz.",
"updateServerStatusInterval": "Sunucu durumu güncelleme aralığı",
"upload": "Yükle",
"upsideDown": "Başaşağı",
"uptime": "Çalışma süresi",
"useCdn": "CDN kullan",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Varsayılan olarak Podman kullan",
"used": "Kullanılan",
"view": "Görünüm",
"viewDetails": "Detayları Görüntüle",
"viewErr": "Hatayı gör",
"virtKeyHelpClipboard": "Seçili terminal boş değilse panoya kopyala, aksi takdirde panodaki içeriği terminale yapıştır.",
"virtKeyHelpIME": "Klavyeyi aç/kapat",

View File

@@ -1,32 +1,56 @@
{
"@@locale": "uk",
"@clearServerStatsContent": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"@clearServerStatsTitle": {
"placeholders": {
"serverName": {
"type": "String"
}
}
},
"aboutThanks": "Дякуємо наступним особам, які взяли участь.",
"acceptBeta": "Прийняти оновлення бета-версії",
"addSystemPrivateKeyTip": "Наразі приватних ключів нема, хочете додати той, що йде з системою (~/.ssh/id_rsa)?",
"added2List": "Додано до списку завдань",
"addr": "Адреса",
"alreadyLastDir": "Вже в останньому каталозі.",
"atLeastOneTab": "Потрібно вибрати принаймні одну вкладку",
"authFailTip": "Авторизація не вдалася, будь ласка, перевірте правильність облікових даних",
"autoBackupConflict": "Тільки одне автоматичне резервне копіювання може бути активне одночасно.",
"autoConnect": "Авто підключення",
"autoRun": "Авто запуск",
"autoUpdateHomeWidget": "Автоматичне оновлення віджетів на головному екрані",
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
"backupPassword": "Пароль резервного копіювання",
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
"availableTabs": "Доступні вкладки",
"backupEncrypted": "Резервна копія зашифрована",
"backupNotEncrypted": "Резервна копія не зашифрована",
"backupPasswordSet": "Пароль резервного копіювання встановлено",
"backupPassword": "Пароль резервного копіювання",
"backupPasswordRemoved": "Пароль резервного копіювання видалено",
"backupPasswordSet": "Пароль резервного копіювання встановлено",
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
"battery": "Акумулятор",
"bgRun": "Запуск у фоновому режимі",
"bgRunTip": "Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".",
"clearAllStatsContent": "Ви впевнені, що хочете очистити всю статистику з'єднань сервера? Цю дію не можна скасувати.",
"clearAllStatsTitle": "Очистити всю статистику",
"clearServerStatsContent": "Ви впевнені, що хочете очистити статистику з'єднань для сервера \"{serverName}\"? Цю дію не можна скасувати.",
"clearServerStatsTitle": "Очистити статистику {serverName}",
"clearThisServerStats": "Очистити статистику цього сервера",
"closeAfterSave": "Зберегти та закрити",
"cmd": "Команда",
"collapseUITip": "Сховати довгі списки, що є у UI за замовчуванням",
"conn": "З'єднання",
"connectionDetails": "Деталі з'єднання",
"connectionStats": "Статистика з'єднань",
"connectionStatsDesc": "Переглянути коефіцієнт успішності підключення до сервера та історію",
"container": "Контейнер",
"containerTrySudoTip": "Наприклад: У застосунку користувач це aaa, але Docker встановлений під користувачем root. У цьому випадку вам потрібно активувати цю опцію.",
"convert": "Конвертувати",
@@ -42,6 +66,10 @@
"desktopTerminalTip": "Команда для відкриття емулятора термінала під час запуску SSH-сеансів.",
"dirEmpty": "Переконайтеся, що директорія пуста.",
"disconnected": "Відключено",
"discoverSshServers": "Виявити SSH сервери",
"discoveryFailed": "Виявлення не вдалось",
"discoverySettings": "Налаштування виявлення",
"discoverySummary": "Підсумок виявлення",
"disk": "Диск",
"diskHealth": "Стан диска",
"diskIgnorePath": "Ігнорувати шлях для диска",
@@ -55,9 +83,10 @@
"doubleColumnMode": "Режим подвійної колонки",
"doubleColumnTip": "Ця опція лише активує функцію, чи можна її насправді включити, залежить від ширини пристрою",
"editVirtKeys": "Редагувати віртуальні клавіші",
"editor": "Редактор",
"editorHighlightTip": "Поточна підсвітка коду не ідеальна і може бути вимкнена для покращення.",
"emulator": "Емулятор",
"enableMdns": "Увімкнути mDNS",
"enableMdnsDesc": "Використовувати mDNS/Bonjour для виявлення SSH сервісів",
"encode": "Кодувати",
"envVars": "Змінні середовища",
"experimentalFeature": "Експериментальна функція",
@@ -67,8 +96,8 @@
"fgService": "Служба переднього плану",
"fgServiceTip": "Після увімкнення деякі моделі пристроїв можуть вилітати. Вимкнення може призвести до того, що деякі моделі не зможуть підтримувати SSH-з'єднання у фоновому режимі. Будь ласка, дозвольте ServerBox права на сповіщення, фонову роботу та самопробудження в системних налаштуваннях.",
"fileTooLarge": "Файл '{file}' занадто великий ({size}), макс {sizeMax}",
"finishedAt": "Завершено о",
"followSystem": "Слідувати системі",
"font": "Шрифт",
"fontSize": "Розмір шрифту",
"force": "Примусово",
"fullScreen": "Повноекранний режим",
@@ -79,13 +108,14 @@
"goto": "Перейти до",
"hideTitleBar": "Сховати заголовок",
"highlight": "Підсвітка коду",
"homeTabs": "Домашні вкладки",
"homeTabsCustomizeDesc": "Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок",
"homeWidgetUrlConfig": "Налаштувати URL віджета на головному екрані",
"host": "Хост",
"httpFailedWithCode": "Запит не вдався, код статусу: {code}",
"ignoreCert": "Ігнорувати сертифікат",
"image": "Зображення",
"imagesList": "Список зображень",
"init": "Ініціалізувати",
"inner": "Внутрішній",
"install": "Встановити",
"installDockerWithUrl": "Будь ласка, спочатку встановіть Docker. (https://docs.docker.com/engine/install)",
@@ -95,14 +125,15 @@
"keepStatusWhenErr": "Зберегати останній стан сервера",
"keepStatusWhenErrTip": "Тільки в разі виникнення помилки під час виконання скрипту",
"keyAuth": "Аутентифікація ключем",
"lastFailure": "Остання помилка",
"lastSuccess": "Останній успіх",
"letterCache": "Кешування букв",
"letterCacheTip": "Рекомендується відключити, але після вимкнення стане неможливим введення CJK (китайських, японських, корейських) символів.",
"license": "Ліцензія",
"location": "Місцезнаходження",
"loss": "втрата пакетів",
"madeWithLove": "Зроблено з ❤️ від {myGithub}",
"manual": "Посібник",
"max": "макс.",
"maxConcurrency": "Максимальна паралельність",
"maxRetryCount": "Кількість повторних спроб підключення до сервера",
"maxRetryCountEqual0": "Знову і знову буде намагатися повторно підключитися.",
"min": "мін.",
@@ -115,6 +146,7 @@
"net": "Мережа",
"netViewType": "Тип перегляду мережі",
"newContainer": "Новий контейнер",
"noConnectionStatsData": "Немає даних статистики з'єднань",
"noLineChart": "Не використовувати лінійні діаграми",
"noLineChartForCpu": "Не використовувати лінійні діаграми для ЦП",
"noPrivateKeyTip": "Приватного ключа немає, можливо, він був видалений або сталася помилка конфігурації.",
@@ -136,7 +168,6 @@
"plugInType": "Тип вставки",
"port": "Порт",
"preferDiskAmount": "Пріоритетно показувати ємність диска",
"preview": "Попередній перегляд",
"privateKey": "Приватний ключ",
"process": "Процес",
"prune": "Обрізати",
@@ -146,6 +177,7 @@
"pveVersionLow": "Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.",
"read": "Читати",
"reboot": "Перезавантажити",
"recentConnections": "Останні з'єднання",
"rememberPwdInMem": "Запам'ятати пароль у пам'яті",
"rememberPwdInMemTip": "Використовується для контейнерів, призупинення тощо.",
"rememberWindowSize": "Запам'ятати розмір вікна",
@@ -166,6 +198,8 @@
"serverDetailOrder": "Порядок віджетів на сторінці деталі",
"serverFuncBtns": "Кнопки функцій сервера",
"serverOrder": "Порядок сервера",
"serverTabRequired": "Вкладку сервера не можна видалити",
"servers": "серверів",
"sftpDlPrepare": "Підготовка до підключення...",
"sftpEditorTip": "Якщо порожньо, використовуйте вбудований редактор файлів програми. Якщо є значення, використовуйте редактор віддаленого сервера, наприклад, `vim` (рекомендується автоматично визначити відповідно до `EDITOR`).",
"sftpRmrDirSummary": "Використовуйте `rm -r`, щоб видалити папку в SFTP.",
@@ -180,6 +214,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": "Автоматичне переключення віртуальних клавіш",
@@ -193,10 +240,10 @@
"suspend": "Призупинити",
"suspendTip": "Функція призупинення потребує адміністративних прав та підтримки systemd.",
"switchTo": "Переключитися на {val}",
"sync": "Синхронізація",
"syncTip": "Може знадобитися перезапуск, щоб деякі зміни набрали чинності.",
"system": "Система",
"tag": "Теги",
"tapToStartDiscovery": "Натисніть кнопку пошуку, щоб виявити SSH сервери у вашій мережі",
"temperature": "Температура",
"termFontSizeTip": "Це налаштування вплине на розмір терміналу (ширину та висоту). Ви можете масштабувати на сторінці терміналу, щоб налаштувати розмір шрифту поточної сесії.",
"terminal": "Термінал",
@@ -207,6 +254,7 @@
"time": "Час",
"times": "Рази",
"total": "Всього",
"totalAttempts": "Загальна кількість",
"traffic": "Трафік",
"trySudo": "Спробуйте використовувати sudo",
"ttl": "TTL",
@@ -215,7 +263,6 @@
"update": "Оновити",
"updateIntervalEqual0": "Ви встановили 0, автоматичне оновлення не відбудеться.\nНе можна розрахувати статус ЦП.",
"updateServerStatusInterval": "Інтервал оновлення статусу сервера",
"upload": "Завантаження",
"upsideDown": "Доверху дном",
"uptime": "Час роботи",
"useCdn": "Використання CDN",
@@ -224,6 +271,7 @@
"usePodmanByDefault": "Використовувати Podman за замовчуванням",
"used": "Використано",
"view": "Переглянути",
"viewDetails": "Переглянути деталі",
"viewErr": "Переглянути помилку",
"virtKeyHelpClipboard": "Копіювати в буфер обміну, якщо вибраний термінал не порожній, в іншому випадку вставити вміст буфера обміну в термінал.",
"virtKeyHelpIME": "Увімкнути/вимкнути клавіатуру",

Some files were not shown because too many files have changed in this diff Show More