mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-23 16:45:27 +01:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd2bf08f78 | ||
|
|
98e13c39cf | ||
|
|
e70abeef04 | ||
|
|
194774d6fb | ||
|
|
640d61bab9 | ||
|
|
7f4cf22cc9 | ||
|
|
05a927753f | ||
|
|
0c7b72fb2c | ||
|
|
a869b97502 | ||
|
|
eadd343205 | ||
|
|
1bac986fe0 | ||
|
|
a94be6c2c3 | ||
|
|
fc8e9b4bb1 | ||
|
|
ec4b633889 | ||
|
|
e51804fa70 | ||
|
|
2466341999 | ||
|
|
929061213f | ||
|
|
6b52679942 | ||
|
|
efc0315c93 | ||
|
|
8e4c2a7cde | ||
|
|
4ec7f5895e | ||
|
|
ee22cdb55f |
21
.github/workflows/analysis.yml
vendored
21
.github/workflows/analysis.yml
vendored
@@ -17,17 +17,28 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
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
|
||||||
|
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pub-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
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.
|
# Consider passing '--fatal-infos' for slightly stricter analysis.
|
||||||
- name: Analyze project source
|
- name: Analyze project source
|
||||||
run: dart analyze
|
run: dart analyze
|
||||||
|
|||||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -9,6 +9,11 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
# Set by fl_build
|
||||||
|
# env:
|
||||||
|
# APP_NAME: ServerBox
|
||||||
|
# BUILD_NUMBER: ${{ github.ref_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
releaseAndroid:
|
releaseAndroid:
|
||||||
name: Release android
|
name: Release android
|
||||||
@@ -20,7 +25,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.35.1"
|
flutter-version: "3.35.3"
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
@@ -98,16 +103,12 @@ jobs:
|
|||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v4
|
||||||
# - name: Install Flutter
|
# - name: Install Flutter
|
||||||
# uses: subosito/flutter-action@v2
|
# uses: subosito/flutter-action@v2
|
||||||
# with:
|
|
||||||
# channel: 'stable'
|
|
||||||
# flutter-version: '3.32.1'
|
|
||||||
# - name: Build
|
# - name: Build
|
||||||
# run: dart run fl_build -p ios,mac
|
# run: dart run fl_build -p ios
|
||||||
# - name: Create Release
|
# - name: Create Release
|
||||||
# uses: softprops/action-gh-release@v2
|
# uses: softprops/action-gh-release@v2
|
||||||
# with:
|
# with:
|
||||||
# files: |
|
# files: |
|
||||||
# ${{ env.APP_NAME }}_universal_macos.zip
|
|
||||||
# ${{ env.APP_NAME }}_universal.ipa
|
# ${{ env.APP_NAME }}_universal.ipa
|
||||||
# env:
|
# env:
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
95
CLAUDE.md
Normal file
95
CLAUDE.md
Normal 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
|
||||||
@@ -16,8 +16,7 @@ class ForegroundService : Service() {
|
|||||||
var isRunning: Boolean = false
|
var isRunning: Boolean = false
|
||||||
}
|
}
|
||||||
private val chanId = "ForegroundServiceChannel"
|
private val chanId = "ForegroundServiceChannel"
|
||||||
private val GROUP_KEY = "ssh_sessions_group"
|
private val NOTIFICATION_ID = 1000
|
||||||
private val SUMMARY_ID = 1000
|
|
||||||
private val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
|
private val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
|
||||||
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
|
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_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
|
||||||
@@ -70,6 +69,9 @@ class ForegroundService : Service() {
|
|||||||
|
|
||||||
return when (action) {
|
return when (action) {
|
||||||
ACTION_STOP_FOREGROUND -> {
|
ACTION_STOP_FOREGROUND -> {
|
||||||
|
// Notify Flutter to stop all connections before stopping service
|
||||||
|
val stopAllIntent = Intent("tech.lolli.toolbox.STOP_ALL_CONNECTIONS")
|
||||||
|
sendBroadcast(stopAllIntent)
|
||||||
clearAll()
|
clearAll()
|
||||||
stopForegroundService()
|
stopForegroundService()
|
||||||
START_NOT_STICKY
|
START_NOT_STICKY
|
||||||
@@ -81,7 +83,7 @@ class ForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Default bring up foreground with placeholder
|
// Default bring up foreground with placeholder
|
||||||
ensureForeground(createSummaryNotification(0, emptyList()))
|
ensureForeground(createMergedNotification(0, emptyList(), emptyList()))
|
||||||
START_STICKY
|
START_STICKY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,18 +120,19 @@ class ForegroundService : Service() {
|
|||||||
private fun ensureForeground(notification: Notification) {
|
private fun ensureForeground(notification: Notification) {
|
||||||
try {
|
try {
|
||||||
if (!isFgStarted) {
|
if (!isFgStarted) {
|
||||||
startForeground(SUMMARY_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
isFgStarted = true
|
isFgStarted = true
|
||||||
} else {
|
} else {
|
||||||
val nm = getSystemService(NotificationManager::class.java)
|
val nm = getSystemService(NotificationManager::class.java)
|
||||||
nm?.notify(SUMMARY_ID, notification)
|
nm?.notify(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError("Failed to start/update foreground", e)
|
logError("Failed to start/update foreground", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 notificationIntent = Intent(this, MainActivity::class.java)
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||||
@@ -143,21 +146,56 @@ class ForegroundService : Service() {
|
|||||||
Notification.Builder(this)
|
Notification.Builder(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val inbox = Notification.InboxStyle()
|
// Use the earliest session's start time for chronometer
|
||||||
lines.forEach { inbox.addLine(it) }
|
val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis()
|
||||||
|
|
||||||
return builder
|
val title = when {
|
||||||
.setContentTitle("SSH sessions: $count active")
|
count == 0 -> "Server Box"
|
||||||
.setContentText(if (lines.isNotEmpty()) lines.first() else "Running")
|
count == 1 -> sessions.first().title
|
||||||
|
else -> "SSH sessions: $count active"
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentText = when {
|
||||||
|
count == 0 -> "Ready for connections"
|
||||||
|
count == 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)
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
.setStyle(inbox)
|
.setWhen(earliestStartTime)
|
||||||
|
.setUsesChronometer(true)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setGroup(GROUP_KEY)
|
|
||||||
.setGroupSummary(true)
|
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.addAction(android.R.drawable.ic_delete, "Stop", stopPending)
|
.addAction(android.R.drawable.ic_delete, "Stop All", stopPending)
|
||||||
.build()
|
|
||||||
|
if (style != null) {
|
||||||
|
notification.setStyle(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUpdateSessions(payload: String) {
|
private fun handleUpdateSessions(payload: String) {
|
||||||
@@ -192,71 +230,21 @@ class ForegroundService : Service() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build per-session notifications
|
// Cancel any existing individual notifications (we only show merged notification now)
|
||||||
val currentIds = mutableSetOf<Int>()
|
val toCancel = postedIds.toSet()
|
||||||
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
|
|
||||||
toCancel.forEach { nm.cancel(it) }
|
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.clear()
|
||||||
postedIds.addAll(currentIds)
|
notificationIdMap.clear()
|
||||||
|
|
||||||
// Post/update summary and ensure foreground
|
// Create merged notification content
|
||||||
val maxSummaryLines = 5
|
val summaryLines = sessions.map { "${it.title}: ${it.status}" }
|
||||||
val truncated = summaryLines.size > maxSummaryLines
|
val mergedNotification = createMergedNotification(sessions.size, summaryLines, sessions)
|
||||||
val displaySummaryLines = if (truncated) {
|
ensureForeground(mergedNotification)
|
||||||
summaryLines.take(maxSummaryLines) + "...and ${summaryLines.size - maxSummaryLines} more"
|
|
||||||
} else {
|
|
||||||
summaryLines
|
|
||||||
}
|
|
||||||
val summary = createSummaryNotification(sessions.size, displaySummaryLines)
|
|
||||||
ensureForeground(summary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearAll() {
|
private fun clearAll() {
|
||||||
val nm = getSystemService(NotificationManager::class.java)
|
val nm = getSystemService(NotificationManager::class.java)
|
||||||
nm?.cancel(SUMMARY_ID)
|
nm?.cancel(NOTIFICATION_ID)
|
||||||
postedIds.forEach { id -> nm?.cancel(id) }
|
postedIds.forEach { id -> nm?.cancel(id) }
|
||||||
postedIds.clear()
|
postedIds.clear()
|
||||||
isFgStarted = false
|
isFgStarted = false
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import android.content.Intent
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
@@ -16,6 +19,8 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
private lateinit var channel: MethodChannel
|
private lateinit var channel: MethodChannel
|
||||||
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
|
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_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) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
@@ -92,6 +97,9 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
|
|
||||||
// Handle intent if launched via notification action
|
// Handle intent if launched via notification action
|
||||||
handleActionIntent(intent)
|
handleActionIntent(intent)
|
||||||
|
|
||||||
|
// Register broadcast receiver for stop all connections
|
||||||
|
setupStopAllReceiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reqPerm() {
|
private fun reqPerm() {
|
||||||
@@ -141,4 +149,28 @@ 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)
|
||||||
|
registerReceiver(stopAllReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
stopAllReceiver?.let {
|
||||||
|
unregisterReceiver(it)
|
||||||
|
stopAllReceiver = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
flutter_server_box.wiki
Submodule
1
flutter_server_box.wiki
Submodule
Submodule flutter_server_box.wiki added at f440010313
@@ -748,7 +748,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -758,7 +758,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -884,7 +884,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -894,7 +894,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -912,7 +912,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -922,7 +922,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -956,7 +956,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
@@ -982,7 +982,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1018,7 +1018,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1031,7 +1031,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1054,7 +1054,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1066,7 +1066,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
@@ -1095,7 +1095,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1107,7 +1107,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
PRODUCT_NAME = ServerBox;
|
PRODUCT_NAME = ServerBox;
|
||||||
@@ -1133,7 +1133,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1145,7 +1145,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
PRODUCT_NAME = ServerBox;
|
PRODUCT_NAME = ServerBox;
|
||||||
|
|||||||
@@ -77,8 +77,10 @@ abstract final class MethodChans {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Register a handler for native -> Flutter callbacks.
|
/// Register a handler for native -> Flutter callbacks.
|
||||||
/// Currently handles: `disconnectSession` with argument map {id: string}
|
/// Currently handles:
|
||||||
static void registerHandler(Future<void> Function(String id) onDisconnect) {
|
/// - `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 {
|
_channel.setMethodCallHandler((call) async {
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case 'disconnectSession':
|
case 'disconnectSession':
|
||||||
@@ -88,6 +90,9 @@ abstract final class MethodChans {
|
|||||||
await onDisconnect(id);
|
await onDisconnect(id);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case 'stopAllConnections':
|
||||||
|
onStopAll?.call();
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ abstract final class SSHConfig {
|
|||||||
void addServer() {
|
void addServer() {
|
||||||
if (currentHost != null && currentHost != '*' && hostname != null) {
|
if (currentHost != null && currentHost != '*' && hostname != null) {
|
||||||
final spi = Spi(
|
final spi = Spi(
|
||||||
|
id: ShortId.generate(),
|
||||||
name: currentHost,
|
name: currentHost,
|
||||||
ip: hostname,
|
ip: hostname,
|
||||||
port: port,
|
port: port,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class SystemDetector {
|
|||||||
///
|
///
|
||||||
/// First checks if a custom system type is configured in [spi].
|
/// First checks if a custom system type is configured in [spi].
|
||||||
/// If not, attempts to detect the system by running commands:
|
/// If not, attempts to detect the system by running commands:
|
||||||
/// 1. 'ver' command to detect Windows
|
/// 1. 'uname -a' command to detect Linux/BSD/Darwin
|
||||||
/// 2. '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.
|
/// Returns [SystemType.linux] as default if detection fails.
|
||||||
static Future<SystemType> detect(SSHClient client, Spi spi) async {
|
static Future<SystemType> detect(SSHClient client, Spi spi) async {
|
||||||
@@ -22,17 +22,8 @@ class SystemDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to detect Windows systems first (more reliable detection)
|
// Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)
|
||||||
final powershellResult = await client.run('ver 2>nul').string;
|
final unixResult = await client.run('uname -a 2>/dev/null').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;
|
|
||||||
if (unixResult.contains('Linux')) {
|
if (unixResult.contains('Linux')) {
|
||||||
detectedSystemType = SystemType.linux;
|
detectedSystemType = SystemType.linux;
|
||||||
dprint('Detected Linux system type for ${spi.oldId}');
|
dprint('Detected Linux system type for ${spi.oldId}');
|
||||||
@@ -42,6 +33,15 @@ class SystemDetector {
|
|||||||
dprint('Detected BSD system type for ${spi.oldId}');
|
dprint('Detected BSD system type for ${spi.oldId}');
|
||||||
return detectedSystemType;
|
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) {
|
} catch (e) {
|
||||||
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
|
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ enum StatusCmdType implements ShellCmdType {
|
|||||||
uptime('uptime'),
|
uptime('uptime'),
|
||||||
conn('cat /proc/net/snmp'),
|
conn('cat /proc/net/snmp'),
|
||||||
disk(
|
disk(
|
||||||
'lsblk --bytes --json --output '
|
'(lsblk --bytes --json --output '
|
||||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
'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'"),
|
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||||
tempType('cat /sys/class/thermal/thermal_zone*/type'),
|
tempType('cat /sys/class/thermal/thermal_zone*/type'),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_ce_flutter/adapters.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/view/page/server/tab/tab.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/ssh/tab.dart';
|
||||||
import 'package:server_box/view/page/storage/local.dart';
|
import 'package:server_box/view/page/storage/local.dart';
|
||||||
|
|
||||||
|
part 'tab.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 103)
|
||||||
enum AppTab {
|
enum AppTab {
|
||||||
|
@HiveField(0)
|
||||||
server,
|
server,
|
||||||
|
@HiveField(1)
|
||||||
ssh,
|
ssh,
|
||||||
|
@HiveField(2)
|
||||||
file,
|
file,
|
||||||
|
@HiveField(3)
|
||||||
snippet
|
snippet
|
||||||
//settings,
|
//settings,
|
||||||
;
|
;
|
||||||
@@ -93,4 +101,35 @@ enum AppTab {
|
|||||||
static List<NavigationRailDestination> get navRailDestinations {
|
static List<NavigationRailDestination> get navRailDestinations {
|
||||||
return AppTab.values.map((e) => e.navRailDestination).toList();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
lib/data/model/app/tab.g.dart
Normal file
52
lib/data/model/app/tab.g.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/model/container/type.dart';
|
||||||
import 'package:server_box/data/res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ sealed class ContainerPs {
|
|||||||
final String? image = null;
|
final String? image = null;
|
||||||
String? get name;
|
String? get name;
|
||||||
String? get cmd;
|
String? get cmd;
|
||||||
bool get running;
|
ContainerStatus get status;
|
||||||
|
|
||||||
String? cpu;
|
String? cpu;
|
||||||
String? mem;
|
String? mem;
|
||||||
@@ -51,7 +52,7 @@ final class PodmanPs implements ContainerPs {
|
|||||||
String? get cmd => command?.firstOrNull;
|
String? get cmd => command?.firstOrNull;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get running => exited != true;
|
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void parseStats(String s) {
|
void parseStats(String s) {
|
||||||
@@ -121,10 +122,7 @@ final class DockerPs implements ContainerPs {
|
|||||||
String? get cmd => null;
|
String? get cmd => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get running {
|
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
||||||
if (state?.contains('Exited') == true) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void parseStats(String s) {
|
void parseStats(String s) {
|
||||||
|
|||||||
70
lib/data/model/container/status.dart
Normal file
70
lib/data/model/container/status.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/data/model/server/connection_stat.dart
Normal file
79
lib/data/model/server/connection_stat.dart
Normal 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;
|
||||||
|
}
|
||||||
585
lib/data/model/server/connection_stat.freezed.dart
Normal file
585
lib/data/model/server/connection_stat.freezed.dart
Normal 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
|
||||||
233
lib/data/model/server/connection_stat.g.dart
Normal file
233
lib/data/model/server/connection_stat.g.dart
Normal 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,
|
||||||
|
};
|
||||||
@@ -44,22 +44,49 @@ class Disk with EquatableMixin {
|
|||||||
static List<Disk> parse(String raw) {
|
static List<Disk> parse(String raw) {
|
||||||
final list = <Disk>[];
|
final list = <Disk>[];
|
||||||
raw = raw.trim();
|
raw = raw.trim();
|
||||||
try {
|
|
||||||
if (raw.startsWith('{')) {
|
if (raw.isEmpty) {
|
||||||
// Parse JSON output from lsblk command
|
dprint('Empty disk info data received');
|
||||||
final Map<String, dynamic> jsonData = json.decode(raw);
|
return list;
|
||||||
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
|
}
|
||||||
|
|
||||||
for (final device in blockdevices) {
|
try {
|
||||||
// Process each device
|
// Check if we have lsblk JSON output with success marker
|
||||||
_processTopLevelDevice(device, list);
|
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);
|
return _parseWithOldMethod(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we reach here, both parsing methods failed
|
||||||
|
Loggers.app.warning('Unable to parse disk info with any method');
|
||||||
|
|
||||||
} catch (e) {
|
} 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;
|
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
|
/// Process a single device without recursively processing its children
|
||||||
static Disk? _processSingleDevice(Map<String, dynamic> device) {
|
static Disk? _processSingleDevice(Map<String, dynamic> device) {
|
||||||
final fstype = device['fstype']?.toString();
|
final fstype = device['fstype']?.toString();
|
||||||
@@ -102,20 +155,7 @@ class Disk with EquatableMixin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
final fsFields = _parseFilesystemFields(device);
|
||||||
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 name = device['name']?.toString();
|
final name = device['name']?.toString();
|
||||||
final kname = device['kname']?.toString();
|
final kname = device['kname']?.toString();
|
||||||
final uuid = device['uuid']?.toString();
|
final uuid = device['uuid']?.toString();
|
||||||
@@ -124,10 +164,10 @@ class Disk with EquatableMixin {
|
|||||||
path: path,
|
path: path,
|
||||||
fsTyp: fstype,
|
fsTyp: fstype,
|
||||||
mount: mountpoint,
|
mount: mountpoint,
|
||||||
usedPercent: usedPercent,
|
usedPercent: fsFields.usedPercent,
|
||||||
used: used,
|
used: fsFields.used,
|
||||||
size: size,
|
size: fsFields.size,
|
||||||
avail: avail,
|
avail: fsFields.avail,
|
||||||
name: name,
|
name: name,
|
||||||
kname: kname,
|
kname: kname,
|
||||||
uuid: uuid,
|
uuid: uuid,
|
||||||
@@ -155,20 +195,7 @@ class Disk with EquatableMixin {
|
|||||||
|
|
||||||
// Handle common filesystem cases or parent devices with children
|
// Handle common filesystem cases or parent devices with children
|
||||||
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
|
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
final fsFields = _parseFilesystemFields(device);
|
||||||
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 name = device['name']?.toString();
|
final name = device['name']?.toString();
|
||||||
final kname = device['kname']?.toString();
|
final kname = device['kname']?.toString();
|
||||||
final uuid = device['uuid']?.toString();
|
final uuid = device['uuid']?.toString();
|
||||||
@@ -177,10 +204,10 @@ class Disk with EquatableMixin {
|
|||||||
path: path,
|
path: path,
|
||||||
fsTyp: fstype,
|
fsTyp: fstype,
|
||||||
mount: mount,
|
mount: mount,
|
||||||
usedPercent: usedPercent,
|
usedPercent: fsFields.usedPercent,
|
||||||
used: used,
|
used: fsFields.used,
|
||||||
size: size,
|
size: fsFields.size,
|
||||||
avail: avail,
|
avail: fsFields.avail,
|
||||||
name: name,
|
name: name,
|
||||||
kname: kname,
|
kname: kname,
|
||||||
uuid: uuid,
|
uuid: uuid,
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ class ContainerNotifier extends _$ContainerNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
|
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
|
||||||
this.client = client;
|
|
||||||
this.userName = userName;
|
|
||||||
this.hostId = hostId;
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
final type = Stores.container.getType(hostId);
|
final type = Stores.container.getType(hostId);
|
||||||
final initialState = ContainerState(type: type);
|
final initialState = ContainerState(type: type);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'container.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$containerNotifierHash() => r'db8f8a6b6071b7b33fbf79128dfed408a5b9fdad';
|
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'private_key.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$privateKeyNotifierHash() =>
|
String _$privateKeyNotifierHash() =>
|
||||||
r'404836a4409f64d305c1e22f4a57b52985a57b68';
|
r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7';
|
||||||
|
|
||||||
/// See also [PrivateKeyNotifier].
|
/// See also [PrivateKeyNotifier].
|
||||||
@ProviderFor(PrivateKeyNotifier)
|
@ProviderFor(PrivateKeyNotifier)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'all.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$serversNotifierHash() => r'2ae641188f772794a32e8700c008f51ba0cc1ec9';
|
String _$serversNotifierHash() => r'2b29ad3027a203c7a20bfd0142d384a503cbbcaa';
|
||||||
|
|
||||||
/// See also [ServersNotifier].
|
/// See also [ServersNotifier].
|
||||||
@ProviderFor(ServersNotifier)
|
@ProviderFor(ServersNotifier)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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/error.dart';
|
||||||
import 'package:server_box/data/model/app/scripts/script_consts.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/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.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.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/server_status_update_req.dart';
|
||||||
@@ -40,7 +41,7 @@ abstract class ServerState with _$ServerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Individual server state management
|
// Individual server state management
|
||||||
@riverpod
|
@Riverpod(keepAlive: true)
|
||||||
class ServerNotifier extends _$ServerNotifier {
|
class ServerNotifier extends _$ServerNotifier {
|
||||||
@override
|
@override
|
||||||
ServerState build(String serverId) {
|
ServerState build(String serverId) {
|
||||||
@@ -145,6 +146,15 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
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}';
|
final sessionId = 'ssh_${spi.id}';
|
||||||
TermSessionManager.add(
|
TermSessionManager.add(
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
@@ -156,6 +166,29 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
TermSessionManager.setActive(sessionId, hasTerminal: false);
|
TermSessionManager.setActive(sessionId, hasTerminal: false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
TryLimiter.inc(sid);
|
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());
|
final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
|
||||||
updateStatus(newStatus);
|
updateStatus(newStatus);
|
||||||
updateConnection(ServerConn.failed);
|
updateConnection(ServerConn.failed);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'single.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$serverNotifierHash() => r'5625b0a4762c28efdbc124809c03b84a51d213b1';
|
String _$serverNotifierHash() => r'524647748cc3810c17e5c1cd29e360f3936f5014';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'snippet.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$snippetNotifierHash() => r'caf0361f9a0346fb99cb90f032f1ceb29446dd71';
|
String _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9';
|
||||||
|
|
||||||
/// See also [SnippetNotifier].
|
/// See also [SnippetNotifier].
|
||||||
@ProviderFor(SnippetNotifier)
|
@ProviderFor(SnippetNotifier)
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
abstract class BuildData {
|
abstract class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 1231;
|
static const int build = 1253;
|
||||||
static const int script = 68;
|
static const int script = 69;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
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/container.dart';
|
||||||
import 'package:server_box/data/store/history.dart';
|
import 'package:server_box/data/store/history.dart';
|
||||||
import 'package:server_box/data/store/private_key.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/setting.dart';
|
||||||
import 'package:server_box/data/store/snippet.dart';
|
import 'package:server_box/data/store/snippet.dart';
|
||||||
|
|
||||||
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
||||||
abstract final class Stores {
|
abstract final class Stores {
|
||||||
static final setting = SettingStore.instance;
|
static SettingStore get setting => getIt<SettingStore>();
|
||||||
static final server = ServerStore.instance;
|
static ServerStore get server => getIt<ServerStore>();
|
||||||
static final container = ContainerStore.instance;
|
static ContainerStore get container => getIt<ContainerStore>();
|
||||||
static final key = PrivateKeyStore.instance;
|
static PrivateKeyStore get key => getIt<PrivateKeyStore>();
|
||||||
static final snippet = SnippetStore.instance;
|
static SnippetStore get snippet => getIt<SnippetStore>();
|
||||||
static final history = HistoryStore.instance;
|
static HistoryStore get history => getIt<HistoryStore>();
|
||||||
|
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
|
||||||
|
|
||||||
/// All stores that need backup
|
/// All stores that need backup
|
||||||
static final List<HiveStore> _allBackup = [
|
static List<HiveStore> get _allBackup => [
|
||||||
SettingStore.instance,
|
setting,
|
||||||
ServerStore.instance,
|
server,
|
||||||
ContainerStore.instance,
|
container,
|
||||||
PrivateKeyStore.instance,
|
key,
|
||||||
SnippetStore.instance,
|
snippet,
|
||||||
HistoryStore.instance,
|
history,
|
||||||
];
|
connectionStats,
|
||||||
|
];
|
||||||
|
|
||||||
static Future<void> init() async {
|
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()));
|
await Future.wait(_allBackup.map((store) => store.init()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,31 @@ abstract final class TermSessionManager {
|
|||||||
|
|
||||||
static void init() {
|
static void init() {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
MethodChans.registerHandler((id) async {
|
MethodChans.registerHandler(
|
||||||
_entries[id]?.disconnect?.call();
|
(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.
|
/// Add a session record and push update to Android.
|
||||||
static void add({
|
static void add({
|
||||||
required String id,
|
required String id,
|
||||||
|
|||||||
190
lib/data/store/connection_stats.dart
Normal file
190
lib/data/store/connection_stats.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/menu/server_func.dart';
|
||||||
import 'package:server_box/data/model/app/net_view.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/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/model/ssh/virtual_key.dart';
|
||||||
import 'package:server_box/data/res/default.dart';
|
import 'package:server_box/data/res/default.dart';
|
||||||
|
|
||||||
@@ -22,10 +23,7 @@ class SettingStore extends HiveStore {
|
|||||||
// late final launchPage = property('launchPage', Defaults.launchPageIdx);
|
// late final launchPage = property('launchPage', Defaults.launchPageIdx);
|
||||||
|
|
||||||
/// Disk view: amount / IO
|
/// Disk view: amount / IO
|
||||||
late final serverTabPreferDiskAmount = propertyDefault(
|
late final serverTabPreferDiskAmount = propertyDefault('serverTabPreferDiskAmount', false);
|
||||||
'serverTabPreferDiskAmount',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Bigger for bigger font size
|
/// Bigger for bigger font size
|
||||||
/// 1.0 means 100%
|
/// 1.0 means 100%
|
||||||
@@ -70,20 +68,14 @@ class SettingStore extends HiveStore {
|
|||||||
late final locale = propertyDefault('locale', '');
|
late final locale = propertyDefault('locale', '');
|
||||||
|
|
||||||
// SSH virtual key (ctrl | alt) auto turn off
|
// SSH virtual key (ctrl | alt) auto turn off
|
||||||
late final sshVirtualKeyAutoOff = propertyDefault(
|
late final sshVirtualKeyAutoOff = propertyDefault('sshVirtualKeyAutoOff', true);
|
||||||
'sshVirtualKeyAutoOff',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
|
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
|
||||||
|
|
||||||
// Editor theme
|
// Editor theme
|
||||||
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
|
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
|
||||||
|
|
||||||
late final editorDarkTheme = propertyDefault(
|
late final editorDarkTheme = propertyDefault('editorDarkTheme', Defaults.editorDarkTheme);
|
||||||
'editorDarkTheme',
|
|
||||||
Defaults.editorDarkTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
late final fullScreen = propertyDefault('fullScreen', false);
|
late final fullScreen = propertyDefault('fullScreen', false);
|
||||||
|
|
||||||
@@ -113,29 +105,20 @@ class SettingStore extends HiveStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Only valid on iOS
|
// Only valid on iOS
|
||||||
late final autoUpdateHomeWidget = propertyDefault(
|
late final autoUpdateHomeWidget = propertyDefault('autoUpdateHomeWidget', isIOS);
|
||||||
'autoUpdateHomeWidget',
|
|
||||||
isIOS,
|
|
||||||
);
|
|
||||||
|
|
||||||
late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true);
|
late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true);
|
||||||
|
|
||||||
/// Display server tab function buttons on the bottom of each server card if [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
|
/// Otherwise, display them on the top of server detail page
|
||||||
late final moveServerFuncs = propertyDefault(
|
late final moveServerFuncs = propertyDefault('moveOutServerTabFuncBtns', false);
|
||||||
'moveOutServerTabFuncBtns',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Whether use `rm -r` to delete directory on SFTP
|
/// Whether use `rm -r` to delete directory on SFTP
|
||||||
late final sftpRmrDir = propertyDefault('sftpRmrDir', false);
|
late final sftpRmrDir = propertyDefault('sftpRmrDir', false);
|
||||||
|
|
||||||
/// Whether use system's primary color as the app's primary color
|
/// Whether use system's primary color as the app's primary color
|
||||||
late final useSystemPrimaryColor = propertyDefault(
|
late final useSystemPrimaryColor = propertyDefault('useSystemPrimaryColor', false);
|
||||||
'useSystemPrimaryColor',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Only valid on iOS / Android / Windows
|
/// Only valid on iOS / Android / Windows
|
||||||
late final useBioAuth = propertyDefault('useBioAuth', false);
|
late final useBioAuth = propertyDefault('useBioAuth', false);
|
||||||
@@ -151,10 +134,7 @@ class SettingStore extends HiveStore {
|
|||||||
late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true);
|
late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true);
|
||||||
|
|
||||||
/// Show folders first in SFTP file browser
|
/// Show folders first in SFTP file browser
|
||||||
late final sftpShowFoldersFirst = propertyDefault(
|
late final sftpShowFoldersFirst = propertyDefault('sftpShowFoldersFirst', true);
|
||||||
'sftpShowFoldersFirst',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Show tip of suspend
|
/// Show tip of suspend
|
||||||
late final showSuspendTip = propertyDefault('showSuspendTip', true);
|
late final showSuspendTip = propertyDefault('showSuspendTip', true);
|
||||||
@@ -162,10 +142,7 @@ class SettingStore extends HiveStore {
|
|||||||
/// Whether collapse UI items by default
|
/// Whether collapse UI items by default
|
||||||
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
||||||
|
|
||||||
late final serverFuncBtns = listProperty(
|
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
|
||||||
'serverBtns',
|
|
||||||
defaultValue: ServerFuncBtn.defaultIdxs,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Docker is more popular than podman, set to `false` to use docker
|
/// Docker is more popular than podman, set to `false` to use docker
|
||||||
late final usePodman = propertyDefault('usePodman', false);
|
late final usePodman = propertyDefault('usePodman', false);
|
||||||
@@ -180,16 +157,10 @@ class SettingStore extends HiveStore {
|
|||||||
late final containerParseStat = propertyDefault('containerParseStat', true);
|
late final containerParseStat = propertyDefault('containerParseStat', true);
|
||||||
|
|
||||||
/// Auto refresh container status
|
/// Auto refresh container status
|
||||||
late final containerAutoRefresh = propertyDefault(
|
late final containerAutoRefresh = propertyDefault('containerAutoRefresh', true);
|
||||||
'containerAutoRefresh',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Use double column servers page on Desktop
|
/// Use double column servers page on Desktop
|
||||||
late final doubleColumnServersPage = propertyDefault(
|
late final doubleColumnServersPage = propertyDefault('doubleColumnServersPage', true);
|
||||||
'doubleColumnServersPage',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Ignore local network device (eg: br-xxx, ovs-system...)
|
/// Ignore local network device (eg: br-xxx, ovs-system...)
|
||||||
/// when building traffic view on server tab
|
/// when building traffic view on server tab
|
||||||
@@ -244,8 +215,7 @@ class SettingStore extends HiveStore {
|
|||||||
/// Record the position and size of the window.
|
/// Record the position and size of the window.
|
||||||
late final windowState = property<WindowState>(
|
late final windowState = property<WindowState>(
|
||||||
'windowState',
|
'windowState',
|
||||||
fromObj: (raw) =>
|
fromObj: (raw) => WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
|
||||||
WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
|
|
||||||
toObj: (state) => state == null ? null : jsonEncode(state.toJson()),
|
toObj: (state) => state == null ? null : jsonEncode(state.toJson()),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -258,10 +228,7 @@ class SettingStore extends HiveStore {
|
|||||||
late final sftpEditor = propertyDefault('sftpEditor', '');
|
late final sftpEditor = propertyDefault('sftpEditor', '');
|
||||||
|
|
||||||
/// Preferred terminal emulator command on desktop
|
/// Preferred terminal emulator command on desktop
|
||||||
late final desktopTerminal = propertyDefault(
|
late final desktopTerminal = propertyDefault('desktopTerminal', 'x-terminal-emulator');
|
||||||
'desktopTerminal',
|
|
||||||
'x-terminal-emulator',
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Run foreground service on Android, if the SSH terminal is running
|
/// Run foreground service on Android, if the SSH terminal is running
|
||||||
late final fgService = propertyDefault('fgService', false);
|
late final fgService = propertyDefault('fgService', false);
|
||||||
@@ -280,4 +247,14 @@ class SettingStore extends HiveStore {
|
|||||||
|
|
||||||
/// Whether to read SSH config from ~/.ssh/config on first time
|
/// Whether to read SSH config from ~/.ssh/config on first time
|
||||||
late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);
|
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() ?? [];
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1621,6 +1621,126 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
||||||
String get writeScriptTip;
|
String get writeScriptTip;
|
||||||
|
|
||||||
|
/// 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 @noConnectionStatsData.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No connection statistics data'**
|
||||||
|
String get noConnectionStatsData;
|
||||||
|
|
||||||
|
/// No description provided for @totalAttempts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Total'**
|
||||||
|
String get totalAttempts;
|
||||||
|
|
||||||
|
/// No description provided for @lastSuccess.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Last Success'**
|
||||||
|
String get lastSuccess;
|
||||||
|
|
||||||
|
/// No description provided for @lastFailure.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Last Failure'**
|
||||||
|
String get lastFailure;
|
||||||
|
|
||||||
|
/// No description provided for @recentConnections.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Recent Connections'**
|
||||||
|
String get recentConnections;
|
||||||
|
|
||||||
|
/// No description provided for @viewDetails.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'View Details'**
|
||||||
|
String get viewDetails;
|
||||||
|
|
||||||
|
/// No description provided for @connectionDetails.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connection Details'**
|
||||||
|
String get connectionDetails;
|
||||||
|
|
||||||
|
/// No description provided for @clearThisServerStats.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear This Server Statistics'**
|
||||||
|
String get clearThisServerStats;
|
||||||
|
|
||||||
|
/// No description provided for @clearAllStatsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear All Statistics'**
|
||||||
|
String get clearAllStatsTitle;
|
||||||
|
|
||||||
|
/// 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 @clearServerStatsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear {serverName} Statistics'**
|
||||||
|
String clearServerStatsTitle(String serverName);
|
||||||
|
|
||||||
|
/// 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 @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 @reset.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reset'**
|
||||||
|
String get reset;
|
||||||
|
|
||||||
|
/// No description provided for @availableTabs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Available Tabs'**
|
||||||
|
String get availableTabs;
|
||||||
|
|
||||||
|
/// 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 @serverTabRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Server tab cannot be removed'**
|
||||||
|
String get serverTabRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -851,4 +851,71 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => 'Verbindungsstatistiken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc =>
|
||||||
|
'Server-Verbindungserfolgsrate und Verlauf anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => 'Keine Verbindungsstatistikdaten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Gesamt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Letzter Erfolg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Letzter Fehler';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Kürzliche Verbindungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Details anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Verbindungsdetails';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Statistiken dieses Servers löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Alle Statistiken löschen';
|
||||||
|
|
||||||
|
@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 clearServerStatsTitle(String serverName) {
|
||||||
|
return '$serverName 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 get homeTabs => 'Home-Tabs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Zurücksetzen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Verfügbare Tabs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server-Tab kann nicht entfernt werden';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -843,4 +843,71 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => 'Connection Statistics';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc =>
|
||||||
|
'View server connection success rate and history';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => 'No connection statistics data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Total';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Last Success';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Last Failure';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Recent Connections';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'View Details';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Connection Details';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Clear This Server Statistics';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Clear All Statistics';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent =>
|
||||||
|
'Are you sure you want to clear all server connection statistics? This action cannot be undone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Clear $serverName 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 get homeTabs => 'Home Tabs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Customize which tabs appear on the home page and their order';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Reset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Available Tabs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'At least one tab must be selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -852,4 +852,72 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
||||||
|
|
||||||
|
@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 noConnectionStatsData =>
|
||||||
|
'No hay datos de estadísticas de conexión';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Total';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Último éxito';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Último fallo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Conexiones recientes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Ver detalles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Detalles de conexión';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Limpiar estadísticas de este servidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Limpiar todas las estadísticas';
|
||||||
|
|
||||||
|
@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 clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Limpiar estadísticas de $serverName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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 reset => 'Restablecer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Pestañas disponibles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -855,4 +855,72 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
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.';
|
||||||
|
|
||||||
|
@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 noConnectionStatsData =>
|
||||||
|
'Aucune donnée de statistiques de connexion';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Total';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Dernier succès';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Dernier échec';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Connexions récentes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Voir les détails';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Détails de connexion';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Effacer les statistiques de ce serveur';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Effacer toutes les statistiques';
|
||||||
|
|
||||||
|
@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 clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Effacer les statistiques de $serverName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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 reset => 'Réinitialiser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Onglets disponibles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Au moins un onglet doit être sélectionné';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -843,4 +843,71 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => 'Statistik Koneksi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc =>
|
||||||
|
'Lihat tingkat keberhasilan koneksi server dan riwayat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => 'Tidak ada data statistik koneksi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Total';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Sukses Terakhir';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Gagal Terakhir';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Koneksi Terkini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Lihat Detail';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Detail Koneksi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Hapus Statistik Server Ini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Hapus Semua Statistik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent =>
|
||||||
|
'Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Hapus Statistik $serverName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsContent(String serverName) {
|
||||||
|
return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"$serverName\"? Tindakan ini tidak dapat dibatalkan.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabs => 'Tab Beranda';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Sesuaikan tab mana yang muncul di halaman beranda dan urutannya';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Reset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Tab Tersedia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Setidaknya satu tab harus dipilih';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -818,4 +818,68 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => '接続統計';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc => 'サーバー接続成功率と履歴を表示';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => '接続統計データがありません';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => '総計';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => '最後の成功';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => '最後の失敗';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => '最近の接続';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => '詳細を表示';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => '接続の詳細';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'このサーバーの統計をクリア';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'すべての統計をクリア';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent => 'すべてのサーバー接続統計を削除してもよろしいですか?この操作は元に戻せません。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return '$serverNameの統計をクリア';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsContent(String serverName) {
|
||||||
|
return 'サーバー\"$serverName\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabs => 'ホームタブ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc => 'ホームページに表示するタブとその順序をカスタマイズします';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'リセット';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => '利用可能なタブ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'サーバータブは削除できません';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -849,4 +849,72 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => 'Verbindingsstatistieken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc =>
|
||||||
|
'Bekijk server verbindingssucces ratio en geschiedenis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => 'Geen verbindingsstatistiekgegevens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Totaal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Laatst succesvol';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Laatst gefaald';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Recente verbindingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Details bekijken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Verbindingsdetails';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Statistieken van deze server wissen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Alle statistieken wissen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent =>
|
||||||
|
'Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Statistieken van $serverName 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 get homeTabs => 'Home-tabbladen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Resetten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Beschikbare tabbladen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab =>
|
||||||
|
'Er moet minimaal één tabblad worden geselecteerd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -846,4 +846,71 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
||||||
|
|
||||||
|
@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 noConnectionStatsData => 'Não há dados de estatísticas de conexão';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Total';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Último sucesso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Última falha';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Conexões recentes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Ver detalhes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Detalhes da conexão';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Limpar estatísticas deste servidor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Limpar todas as estatísticas';
|
||||||
|
|
||||||
|
@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 clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Limpar estatísticas de $serverName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 get homeTabs => 'Abas iniciais';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Personalize quais abas aparecem na página inicial e sua ordem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Redefinir';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Abas disponíveis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -848,4 +848,71 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => 'Статистика соединений';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc =>
|
||||||
|
'Просмотр коэффициента успешности подключения к серверу и истории';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => 'Нет данных статистики соединений';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Общее';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Последний успех';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Последний сбой';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Недавние соединения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Просмотр деталей';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Детали соединения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Очистить статистику этого сервера';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Очистить всю статистику';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent =>
|
||||||
|
'Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Очистить статистику $serverName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsContent(String serverName) {
|
||||||
|
return 'Вы уверены, что хотите очистить статистику соединений для сервера \"$serverName\"? Это действие не может быть отменено.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabs => 'Вкладки дома';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Настройте, какие вкладки появляются на главной странице и их порядок';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Сброс';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Доступные вкладки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -843,4 +843,71 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
||||||
|
|
||||||
|
@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 noConnectionStatsData => 'Bağlantı istatistik verisi yok';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Toplam';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Son Başarı';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Son Başarısızlık';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Son Bağlantılar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Detayları Görüntüle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Bağlantı Detayları';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Bu Sunucu İstatistiklerini Temizle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Tüm İstatistikleri Temizle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent =>
|
||||||
|
'Tüm sunucu 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 clearServerStatsContent(String serverName) {
|
||||||
|
return '\"$serverName\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 reset => 'Sıfırla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Mevcut Sekmeler';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'En az bir sekme seçilmelidir';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -849,4 +849,71 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => 'Статистика з\'єднань';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc =>
|
||||||
|
'Переглянути коефіцієнт успішності підключення до сервера та історію';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => 'Немає даних статистики з\'єднань';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => 'Загальна кількість';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => 'Останній успіх';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => 'Остання помилка';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => 'Останні з\'єднання';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => 'Переглянути деталі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => 'Деталі з\'єднання';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => 'Очистити статистику цього сервера';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => 'Очистити всю статистику';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent =>
|
||||||
|
'Ви впевнені, що хочете очистити всю статистику з\'єднань сервера? Цю дію не можна скасувати.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return 'Очистити статистику $serverName';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsContent(String serverName) {
|
||||||
|
return 'Ви впевнені, що хочете очистити статистику з\'єднань для сервера \"$serverName\"? Цю дію не можна скасувати.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabs => 'Домашні вкладки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc =>
|
||||||
|
'Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => 'Скинути';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => 'Доступні вкладки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => 'Server tab cannot be removed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -803,6 +803,70 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => '连接统计';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc => '查看服务器连接成功率和历史记录';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => '暂无连接统计数据';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => '总次数';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => '最后成功';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => '最后失败';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => '最近连接记录';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => '查看详情';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => '连接详情';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => '清空此服务器统计';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => '清空所有统计';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent => '确定要清空所有服务器的连接统计数据吗?此操作无法撤销。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return '清空 $serverName 统计';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsContent(String serverName) {
|
||||||
|
return '确定要清空服务器 \"$serverName\" 的连接统计数据吗?此操作无法撤销。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabs => '主页标签';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc => '自定义主页上显示的标签及其顺序';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => '重置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => '可用标签';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => '至少需要选择一个标签';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => '服务器标签不能被移除';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||||
@@ -1604,4 +1668,68 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStats => '連線統計';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionStatsDesc => '檢視伺服器連線成功率和歷史記錄';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noConnectionStatsData => '暫無連線統計資料';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get totalAttempts => '總次數';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSuccess => '最後成功';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastFailure => '最後失敗';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recentConnections => '最近連線記錄';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewDetails => '檢視詳情';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionDetails => '連線詳情';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearThisServerStats => '清空此伺服器統計';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsTitle => '清空所有統計';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearAllStatsContent => '確定要清空所有伺服器的連線統計資料嗎?此操作無法撤銷。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsTitle(String serverName) {
|
||||||
|
return '清空 $serverName 統計';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String clearServerStatsContent(String serverName) {
|
||||||
|
return '確定要清空伺服器 \"$serverName\" 的連線統計資料嗎?此操作無法撤銷。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabs => '主頁標籤';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeTabsCustomizeDesc => '自訂主頁上顯示的標籤及其順序';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reset => '重置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableTabs => '可用標籤';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get atLeastOneTab => '至少需要選擇一個標籤';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get serverTabRequired => '服務器標籤不能被移除';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,18 @@
|
|||||||
// Check in to version control
|
// Check in to version control
|
||||||
|
|
||||||
import 'package:hive_ce/hive.dart';
|
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';
|
import 'package:server_box/hive/hive_adapters.dart';
|
||||||
|
|
||||||
extension HiveRegistrar on HiveInterface {
|
extension HiveRegistrar on HiveInterface {
|
||||||
void registerAdapters() {
|
void registerAdapters() {
|
||||||
|
registerAdapter(AppTabAdapter());
|
||||||
|
registerAdapter(ConnectionResultAdapter());
|
||||||
|
registerAdapter(ConnectionStatAdapter());
|
||||||
registerAdapter(NetViewTypeAdapter());
|
registerAdapter(NetViewTypeAdapter());
|
||||||
registerAdapter(PrivateKeyInfoAdapter());
|
registerAdapter(PrivateKeyInfoAdapter());
|
||||||
|
registerAdapter(ServerConnectionStatsAdapter());
|
||||||
registerAdapter(ServerCustomAdapter());
|
registerAdapter(ServerCustomAdapter());
|
||||||
registerAdapter(ServerFuncBtnAdapter());
|
registerAdapter(ServerFuncBtnAdapter());
|
||||||
registerAdapter(SnippetAdapter());
|
registerAdapter(SnippetAdapter());
|
||||||
@@ -21,8 +27,12 @@ extension HiveRegistrar on HiveInterface {
|
|||||||
|
|
||||||
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||||
void registerAdapters() {
|
void registerAdapters() {
|
||||||
|
registerAdapter(AppTabAdapter());
|
||||||
|
registerAdapter(ConnectionResultAdapter());
|
||||||
|
registerAdapter(ConnectionStatAdapter());
|
||||||
registerAdapter(NetViewTypeAdapter());
|
registerAdapter(NetViewTypeAdapter());
|
||||||
registerAdapter(PrivateKeyInfoAdapter());
|
registerAdapter(PrivateKeyInfoAdapter());
|
||||||
|
registerAdapter(ServerConnectionStatsAdapter());
|
||||||
registerAdapter(ServerCustomAdapter());
|
registerAdapter(ServerCustomAdapter());
|
||||||
registerAdapter(ServerFuncBtnAdapter());
|
registerAdapter(ServerFuncBtnAdapter());
|
||||||
registerAdapter(SnippetAdapter());
|
registerAdapter(SnippetAdapter());
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
IntroPage.title(text: l10n.backupPassword, big: true),
|
IntroPage.title(text: l10n.backupPassword, big: true),
|
||||||
SizedBox(height: padTop * 0.5),
|
SizedBox(height: padTop * 0.5),
|
||||||
Text(
|
Text(
|
||||||
'${l10n.backupTip}\n\n${l10n.backupPasswordTip}',
|
l10n.backupTip,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -148,10 +148,7 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: Btnx.cancelOk,
|
||||||
TextButton(onPressed: () => ctx.pop(false), child: Text(libL10n.cancel)),
|
|
||||||
TextButton(onPressed: () => ctx.pop(true), child: Text(libL10n.ok)),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
final pwd = controller.text.trim();
|
final pwd = controller.text.trim();
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
||||||
"write": "Schreiben",
|
"write": "Schreiben",
|
||||||
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
||||||
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
|
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
|
||||||
}
|
"connectionStats": "Verbindungsstatistiken",
|
||||||
|
"connectionStatsDesc": "Server-Verbindungserfolgsrate und Verlauf anzeigen",
|
||||||
|
"noConnectionStatsData": "Keine Verbindungsstatistikdaten",
|
||||||
|
"totalAttempts": "Gesamt",
|
||||||
|
"lastSuccess": "Letzter Erfolg",
|
||||||
|
"lastFailure": "Letzter Fehler",
|
||||||
|
"recentConnections": "Kürzliche Verbindungen",
|
||||||
|
"viewDetails": "Details anzeigen",
|
||||||
|
"connectionDetails": "Verbindungsdetails",
|
||||||
|
"clearThisServerStats": "Statistiken dieses Servers löschen",
|
||||||
|
"clearAllStatsTitle": "Alle Statistiken löschen",
|
||||||
|
"clearAllStatsContent": "Sind Sie sicher, dass Sie alle Server-Verbindungsstatistiken löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"clearServerStatsTitle": "{serverName} Statistiken löschen",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Home-Tabs",
|
||||||
|
"homeTabsCustomizeDesc": "Passen Sie an, welche Tabs auf der Startseite angezeigt werden und ihre Reihenfolge",
|
||||||
|
"reset": "Zurücksetzen",
|
||||||
|
"availableTabs": "Verfügbare Tabs",
|
||||||
|
"atLeastOneTab": "Mindestens ein Tab muss ausgewählt sein",
|
||||||
|
"serverTabRequired": "Server-Tab kann nicht entfernt werden"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
||||||
"write": "Write",
|
"write": "Write",
|
||||||
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
||||||
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content."
|
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.",
|
||||||
|
"connectionStats": "Connection Statistics",
|
||||||
|
"connectionStatsDesc": "View server connection success rate and history",
|
||||||
|
"noConnectionStatsData": "No connection statistics data",
|
||||||
|
"totalAttempts": "Total",
|
||||||
|
"lastSuccess": "Last Success",
|
||||||
|
"lastFailure": "Last Failure",
|
||||||
|
"recentConnections": "Recent Connections",
|
||||||
|
"viewDetails": "View Details",
|
||||||
|
"connectionDetails": "Connection Details",
|
||||||
|
"clearThisServerStats": "Clear This Server Statistics",
|
||||||
|
"clearAllStatsTitle": "Clear All Statistics",
|
||||||
|
"clearAllStatsContent": "Are you sure you want to clear all server connection statistics? This action cannot be undone.",
|
||||||
|
"clearServerStatsTitle": "Clear {serverName} Statistics",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Home Tabs",
|
||||||
|
"homeTabsCustomizeDesc": "Customize which tabs appear on the home page and their order",
|
||||||
|
"reset": "Reset",
|
||||||
|
"availableTabs": "Available Tabs",
|
||||||
|
"atLeastOneTab": "At least one tab must be selected",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
}
|
}
|
||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
||||||
"write": "Escribir",
|
"write": "Escribir",
|
||||||
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
||||||
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script."
|
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
|
||||||
}
|
"connectionStats": "Estadísticas de conexión",
|
||||||
|
"connectionStatsDesc": "Ver la tasa de éxito de conexión del servidor e historial",
|
||||||
|
"noConnectionStatsData": "No hay datos de estadísticas de conexión",
|
||||||
|
"totalAttempts": "Total",
|
||||||
|
"lastSuccess": "Último éxito",
|
||||||
|
"lastFailure": "Último fallo",
|
||||||
|
"recentConnections": "Conexiones recientes",
|
||||||
|
"viewDetails": "Ver detalles",
|
||||||
|
"connectionDetails": "Detalles de conexión",
|
||||||
|
"clearThisServerStats": "Limpiar estadísticas de este servidor",
|
||||||
|
"clearAllStatsTitle": "Limpiar todas las estadísticas",
|
||||||
|
"clearAllStatsContent": "¿Estás seguro de que quieres limpiar todas las estadísticas de conexión del servidor? Esta acción no se puede deshacer.",
|
||||||
|
"clearServerStatsTitle": "Limpiar estadísticas de {serverName}",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"{serverName}\"? Esta acción no se puede deshacer.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Pestañas de inicio",
|
||||||
|
"homeTabsCustomizeDesc": "Personaliza qué pestañas aparecen en la página de inicio y su orden",
|
||||||
|
"reset": "Restablecer",
|
||||||
|
"availableTabs": "Pestañas disponibles",
|
||||||
|
"atLeastOneTab": "Al menos una pestaña debe estar seleccionada",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
"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",
|
"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.",
|
"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.",
|
||||||
}
|
"connectionStats": "Statistiques de connexion",
|
||||||
|
"connectionStatsDesc": "Voir le taux de réussite de connexion du serveur et l'historique",
|
||||||
|
"noConnectionStatsData": "Aucune donnée de statistiques de connexion",
|
||||||
|
"totalAttempts": "Total",
|
||||||
|
"lastSuccess": "Dernier succès",
|
||||||
|
"lastFailure": "Dernier échec",
|
||||||
|
"recentConnections": "Connexions récentes",
|
||||||
|
"viewDetails": "Voir les détails",
|
||||||
|
"connectionDetails": "Détails de connexion",
|
||||||
|
"clearThisServerStats": "Effacer les statistiques de ce serveur",
|
||||||
|
"clearAllStatsTitle": "Effacer toutes les statistiques",
|
||||||
|
"clearAllStatsContent": "Êtes-vous sûr de vouloir effacer toutes les statistiques de connexion des serveurs ? Cette action ne peut pas être annulée.",
|
||||||
|
"clearServerStatsTitle": "Effacer les statistiques de {serverName}",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"{serverName}\" ? Cette action ne peut pas être annulée.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Onglets d'accueil",
|
||||||
|
"homeTabsCustomizeDesc": "Personnalisez les onglets qui apparaissent sur la page d'accueil et leur ordre",
|
||||||
|
"reset": "Réinitialiser",
|
||||||
|
"availableTabs": "Onglets disponibles",
|
||||||
|
"atLeastOneTab": "Au moins un onglet doit être sélectionné",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
||||||
"write": "Tulis",
|
"write": "Tulis",
|
||||||
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
||||||
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
|
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
|
||||||
}
|
"connectionStats": "Statistik Koneksi",
|
||||||
|
"connectionStatsDesc": "Lihat tingkat keberhasilan koneksi server dan riwayat",
|
||||||
|
"noConnectionStatsData": "Tidak ada data statistik koneksi",
|
||||||
|
"totalAttempts": "Total",
|
||||||
|
"lastSuccess": "Sukses Terakhir",
|
||||||
|
"lastFailure": "Gagal Terakhir",
|
||||||
|
"recentConnections": "Koneksi Terkini",
|
||||||
|
"viewDetails": "Lihat Detail",
|
||||||
|
"connectionDetails": "Detail Koneksi",
|
||||||
|
"clearThisServerStats": "Hapus Statistik Server Ini",
|
||||||
|
"clearAllStatsTitle": "Hapus Semua Statistik",
|
||||||
|
"clearAllStatsContent": "Apakah Anda yakin ingin menghapus semua statistik koneksi server? Tindakan ini tidak dapat dibatalkan.",
|
||||||
|
"clearServerStatsTitle": "Hapus Statistik {serverName}",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"{serverName}\"? Tindakan ini tidak dapat dibatalkan.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Tab Beranda",
|
||||||
|
"homeTabsCustomizeDesc": "Sesuaikan tab mana yang muncul di halaman beranda dan urutannya",
|
||||||
|
"reset": "Reset",
|
||||||
|
"availableTabs": "Tab Tersedia",
|
||||||
|
"atLeastOneTab": "Setidaknya satu tab harus dipilih",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
||||||
"write": "書き込み",
|
"write": "書き込み",
|
||||||
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
||||||
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
|
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
|
||||||
}
|
"connectionStats": "接続統計",
|
||||||
|
"connectionStatsDesc": "サーバー接続成功率と履歴を表示",
|
||||||
|
"noConnectionStatsData": "接続統計データがありません",
|
||||||
|
"totalAttempts": "総計",
|
||||||
|
"lastSuccess": "最後の成功",
|
||||||
|
"lastFailure": "最後の失敗",
|
||||||
|
"recentConnections": "最近の接続",
|
||||||
|
"viewDetails": "詳細を表示",
|
||||||
|
"connectionDetails": "接続の詳細",
|
||||||
|
"clearThisServerStats": "このサーバーの統計をクリア",
|
||||||
|
"clearAllStatsTitle": "すべての統計をクリア",
|
||||||
|
"clearAllStatsContent": "すべてのサーバー接続統計を削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
|
"clearServerStatsTitle": "{serverName}の統計をクリア",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "サーバー\"{serverName}\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "ホームタブ",
|
||||||
|
"homeTabsCustomizeDesc": "ホームページに表示するタブとその順序をカスタマイズします",
|
||||||
|
"reset": "リセット",
|
||||||
|
"availableTabs": "利用可能なタブ",
|
||||||
|
"atLeastOneTab": "少なくとも1つのタブを選択する必要があります",
|
||||||
|
"serverTabRequired": "サーバータブは削除できません"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
||||||
"write": "Schrijven",
|
"write": "Schrijven",
|
||||||
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
||||||
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
|
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
|
||||||
}
|
"connectionStats": "Verbindingsstatistieken",
|
||||||
|
"connectionStatsDesc": "Bekijk server verbindingssucces ratio en geschiedenis",
|
||||||
|
"noConnectionStatsData": "Geen verbindingsstatistiekgegevens",
|
||||||
|
"totalAttempts": "Totaal",
|
||||||
|
"lastSuccess": "Laatst succesvol",
|
||||||
|
"lastFailure": "Laatst gefaald",
|
||||||
|
"recentConnections": "Recente verbindingen",
|
||||||
|
"viewDetails": "Details bekijken",
|
||||||
|
"connectionDetails": "Verbindingsdetails",
|
||||||
|
"clearThisServerStats": "Statistieken van deze server wissen",
|
||||||
|
"clearAllStatsTitle": "Alle statistieken wissen",
|
||||||
|
"clearAllStatsContent": "Weet u zeker dat u alle serververbindingsstatistieken wilt wissen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||||
|
"clearServerStatsTitle": "Statistieken van {serverName} wissen",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "Weet u zeker dat u de verbindingsstatistieken voor server \"{serverName}\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Home-tabbladen",
|
||||||
|
"homeTabsCustomizeDesc": "Pas aan welke tabbladen op de startpagina worden weergegeven en hun volgorde",
|
||||||
|
"reset": "Resetten",
|
||||||
|
"availableTabs": "Beschikbare tabbladen",
|
||||||
|
"atLeastOneTab": "Er moet minimaal één tabblad worden geselecteerd",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
||||||
"write": "Escrita",
|
"write": "Escrita",
|
||||||
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
||||||
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script."
|
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
|
||||||
}
|
"connectionStats": "Estatísticas de conexão",
|
||||||
|
"connectionStatsDesc": "Ver taxa de sucesso de conexão do servidor e histórico",
|
||||||
|
"noConnectionStatsData": "Não há dados de estatísticas de conexão",
|
||||||
|
"totalAttempts": "Total",
|
||||||
|
"lastSuccess": "Último sucesso",
|
||||||
|
"lastFailure": "Última falha",
|
||||||
|
"recentConnections": "Conexões recentes",
|
||||||
|
"viewDetails": "Ver detalhes",
|
||||||
|
"connectionDetails": "Detalhes da conexão",
|
||||||
|
"clearThisServerStats": "Limpar estatísticas deste servidor",
|
||||||
|
"clearAllStatsTitle": "Limpar todas as estatísticas",
|
||||||
|
"clearAllStatsContent": "Tem certeza de que deseja limpar todas as estatísticas de conexão do servidor? Esta ação não pode ser desfeita.",
|
||||||
|
"clearServerStatsTitle": "Limpar estatísticas de {serverName}",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Abas iniciais",
|
||||||
|
"homeTabsCustomizeDesc": "Personalize quais abas aparecem na página inicial e sua ordem",
|
||||||
|
"reset": "Redefinir",
|
||||||
|
"availableTabs": "Abas disponíveis",
|
||||||
|
"atLeastOneTab": "Pelo menos uma aba deve ser selecionada",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
||||||
"write": "Запись",
|
"write": "Запись",
|
||||||
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
||||||
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
|
||||||
}
|
"connectionStats": "Статистика соединений",
|
||||||
|
"connectionStatsDesc": "Просмотр коэффициента успешности подключения к серверу и истории",
|
||||||
|
"noConnectionStatsData": "Нет данных статистики соединений",
|
||||||
|
"totalAttempts": "Общее",
|
||||||
|
"lastSuccess": "Последний успех",
|
||||||
|
"lastFailure": "Последний сбой",
|
||||||
|
"recentConnections": "Недавние соединения",
|
||||||
|
"viewDetails": "Просмотр деталей",
|
||||||
|
"connectionDetails": "Детали соединения",
|
||||||
|
"clearThisServerStats": "Очистить статистику этого сервера",
|
||||||
|
"clearAllStatsTitle": "Очистить всю статистику",
|
||||||
|
"clearAllStatsContent": "Вы уверены, что хотите очистить всю статистику соединений сервера? Это действие не может быть отменено.",
|
||||||
|
"clearServerStatsTitle": "Очистить статистику {serverName}",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "Вы уверены, что хотите очистить статистику соединений для сервера \"{serverName}\"? Это действие не может быть отменено.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Вкладки дома",
|
||||||
|
"homeTabsCustomizeDesc": "Настройте, какие вкладки появляются на главной странице и их порядок",
|
||||||
|
"reset": "Сброс",
|
||||||
|
"availableTabs": "Доступные вкладки",
|
||||||
|
"atLeastOneTab": "Должна быть выбрана хотя бы одна вкладка",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
||||||
"write": "Yaz",
|
"write": "Yaz",
|
||||||
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
||||||
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
|
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
|
||||||
}
|
"connectionStats": "Bağlantı İstatistikleri",
|
||||||
|
"connectionStatsDesc": "Sunucu bağlantı başarı oranını ve geçmişi görüntüle",
|
||||||
|
"noConnectionStatsData": "Bağlantı istatistik verisi yok",
|
||||||
|
"totalAttempts": "Toplam",
|
||||||
|
"lastSuccess": "Son Başarı",
|
||||||
|
"lastFailure": "Son Başarısızlık",
|
||||||
|
"recentConnections": "Son Bağlantılar",
|
||||||
|
"viewDetails": "Detayları Görüntüle",
|
||||||
|
"connectionDetails": "Bağlantı Detayları",
|
||||||
|
"clearThisServerStats": "Bu Sunucu İstatistiklerini Temizle",
|
||||||
|
"clearAllStatsTitle": "Tüm İstatistikleri Temizle",
|
||||||
|
"clearAllStatsContent": "Tüm sunucu bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||||
|
"clearServerStatsTitle": "{serverName} İstatistiklerini Temizle",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "\"{serverName}\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Ana Sayfa Sekmeleri",
|
||||||
|
"homeTabsCustomizeDesc": "Ana sayfada görünecek sekmeleri ve sıralarını özelleştirin",
|
||||||
|
"reset": "Sıfırla",
|
||||||
|
"availableTabs": "Mevcut Sekmeler",
|
||||||
|
"atLeastOneTab": "En az bir sekme seçilmelidir",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
||||||
"write": "Записати",
|
"write": "Записати",
|
||||||
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
||||||
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
|
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
|
||||||
}
|
"connectionStats": "Статистика з'єднань",
|
||||||
|
"connectionStatsDesc": "Переглянути коефіцієнт успішності підключення до сервера та історію",
|
||||||
|
"noConnectionStatsData": "Немає даних статистики з'єднань",
|
||||||
|
"totalAttempts": "Загальна кількість",
|
||||||
|
"lastSuccess": "Останній успіх",
|
||||||
|
"lastFailure": "Остання помилка",
|
||||||
|
"recentConnections": "Останні з'єднання",
|
||||||
|
"viewDetails": "Переглянути деталі",
|
||||||
|
"connectionDetails": "Деталі з'єднання",
|
||||||
|
"clearThisServerStats": "Очистити статистику цього сервера",
|
||||||
|
"clearAllStatsTitle": "Очистити всю статистику",
|
||||||
|
"clearAllStatsContent": "Ви впевнені, що хочете очистити всю статистику з'єднань сервера? Цю дію не можна скасувати.",
|
||||||
|
"clearServerStatsTitle": "Очистити статистику {serverName}",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "Ви впевнені, що хочете очистити статистику з'єднань для сервера \"{serverName}\"? Цю дію не можна скасувати.",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "Домашні вкладки",
|
||||||
|
"homeTabsCustomizeDesc": "Налаштуйте, які вкладки відображаються на головній сторінці та їх порядок",
|
||||||
|
"reset": "Скинути",
|
||||||
|
"availableTabs": "Доступні вкладки",
|
||||||
|
"atLeastOneTab": "Потрібно вибрати принаймні одну вкладку",
|
||||||
|
"serverTabRequired": "Server tab cannot be removed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
||||||
"write": "写",
|
"write": "写",
|
||||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。",
|
||||||
|
"connectionStats": "连接统计",
|
||||||
|
"connectionStatsDesc": "查看服务器连接成功率和历史记录",
|
||||||
|
"noConnectionStatsData": "暂无连接统计数据",
|
||||||
|
"totalAttempts": "总次数",
|
||||||
|
"lastSuccess": "最后成功",
|
||||||
|
"lastFailure": "最后失败",
|
||||||
|
"recentConnections": "最近连接记录",
|
||||||
|
"viewDetails": "查看详情",
|
||||||
|
"connectionDetails": "连接详情",
|
||||||
|
"clearThisServerStats": "清空此服务器统计",
|
||||||
|
"clearAllStatsTitle": "清空所有统计",
|
||||||
|
"clearAllStatsContent": "确定要清空所有服务器的连接统计数据吗?此操作无法撤销。",
|
||||||
|
"clearServerStatsTitle": "清空 {serverName} 统计",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "确定要清空服务器 \"{serverName}\" 的连接统计数据吗?此操作无法撤销。",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "主页标签",
|
||||||
|
"homeTabsCustomizeDesc": "自定义主页上显示的标签及其顺序",
|
||||||
|
"reset": "重置",
|
||||||
|
"availableTabs": "可用标签",
|
||||||
|
"atLeastOneTab": "至少需要选择一个标签",
|
||||||
|
"serverTabRequired": "服务器标签不能被移除"
|
||||||
}
|
}
|
||||||
@@ -249,5 +249,39 @@
|
|||||||
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
||||||
"write": "寫入",
|
"write": "寫入",
|
||||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
|
||||||
}
|
"connectionStats": "連線統計",
|
||||||
|
"connectionStatsDesc": "檢視伺服器連線成功率和歷史記錄",
|
||||||
|
"noConnectionStatsData": "暫無連線統計資料",
|
||||||
|
"totalAttempts": "總次數",
|
||||||
|
"lastSuccess": "最後成功",
|
||||||
|
"lastFailure": "最後失敗",
|
||||||
|
"recentConnections": "最近連線記錄",
|
||||||
|
"viewDetails": "檢視詳情",
|
||||||
|
"connectionDetails": "連線詳情",
|
||||||
|
"clearThisServerStats": "清空此伺服器統計",
|
||||||
|
"clearAllStatsTitle": "清空所有統計",
|
||||||
|
"clearAllStatsContent": "確定要清空所有伺服器的連線統計資料嗎?此操作無法撤銷。",
|
||||||
|
"clearServerStatsTitle": "清空 {serverName} 統計",
|
||||||
|
"@clearServerStatsTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clearServerStatsContent": "確定要清空伺服器 \"{serverName}\" 的連線統計資料嗎?此操作無法撤銷。",
|
||||||
|
"@clearServerStatsContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"serverName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeTabs": "主頁標籤",
|
||||||
|
"homeTabsCustomizeDesc": "自訂主頁上顯示的標籤及其順序",
|
||||||
|
"reset": "重置",
|
||||||
|
"availableTabs": "可用標籤",
|
||||||
|
"atLeastOneTab": "至少需要選擇一個標籤",
|
||||||
|
"serverTabRequired": "服務器標籤不能被移除"
|
||||||
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
|||||||
Widget _buildPs(ContainerState containerState) {
|
Widget _buildPs(ContainerState containerState) {
|
||||||
final items = containerState.items;
|
final items = containerState.items;
|
||||||
if (items == null) return UIs.placeholder;
|
if (items == null) return UIs.placeholder;
|
||||||
final running = items.where((e) => e.running).length;
|
final running = items.where((e) => e.status.isRunning).length;
|
||||||
final stopped = items.length - running;
|
final stopped = items.length - running;
|
||||||
final subtitle = stopped > 0
|
final subtitle = stopped > 0
|
||||||
? l10n.dockerStatusRunningAndStoppedFmt(running, stopped)
|
? l10n.dockerStatusRunningAndStoppedFmt(running, stopped)
|
||||||
@@ -219,8 +219,8 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${item.image ?? l10n.unknown} - ${switch (item) {
|
'${item.image ?? l10n.unknown} - ${switch (item) {
|
||||||
final PodmanPs ps => ps.running ? l10n.running : l10n.stopped,
|
final PodmanPs ps => ps.status.displayName,
|
||||||
final DockerPs ps => ps.state,
|
final DockerPs ps => ps.state ?? ps.status.displayName,
|
||||||
}}',
|
}}',
|
||||||
style: UIs.text13Grey,
|
style: UIs.text13Grey,
|
||||||
),
|
),
|
||||||
@@ -277,7 +277,7 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
|
|||||||
|
|
||||||
Widget _buildMoreBtn(ContainerPs dItem) {
|
Widget _buildMoreBtn(ContainerPs dItem) {
|
||||||
return PopupMenu(
|
return PopupMenu(
|
||||||
items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
items: ContainerMenu.items(dItem.status.isRunning).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||||
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
|
|
||||||
late final _notifier = ref.read(serversNotifierProvider.notifier);
|
late final _notifier = ref.read(serversNotifierProvider.notifier);
|
||||||
late final _provider = ref.read(serversNotifierProvider);
|
late final _provider = ref.read(serversNotifierProvider);
|
||||||
|
late List<AppTab> _tabs = Stores.setting.homeTabs.fetch();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -51,13 +52,30 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
SystemUIs.switchStatusBar(hide: false);
|
SystemUIs.switchStatusBar(hide: false);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
// avoid index out of range
|
// avoid index out of range
|
||||||
if (_selectIndex.value >= AppTab.values.length || _selectIndex.value < 0) {
|
if (_selectIndex.value >= _tabs.length || _selectIndex.value < 0) {
|
||||||
_selectIndex.value = 0;
|
_selectIndex.value = 0;
|
||||||
}
|
}
|
||||||
_pageController = PageController(initialPage: _selectIndex.value);
|
_pageController = PageController(initialPage: _selectIndex.value);
|
||||||
if (Stores.setting.generalWakeLock.fetch()) {
|
if (Stores.setting.generalWakeLock.fetch()) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen to homeTabs changes
|
||||||
|
Stores.setting.homeTabs.listenable().addListener(() {
|
||||||
|
final newTabs = Stores.setting.homeTabs.fetch();
|
||||||
|
if (mounted && newTabs != _tabs) {
|
||||||
|
setState(() {
|
||||||
|
_tabs = newTabs;
|
||||||
|
// Ensure current page index is valid
|
||||||
|
if (_selectIndex.value >= _tabs.length) {
|
||||||
|
_selectIndex.value = _tabs.length - 1;
|
||||||
|
}
|
||||||
|
if (_selectIndex.value < 0 && _tabs.isNotEmpty) {
|
||||||
|
_selectIndex.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -119,9 +137,9 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemCount: AppTab.values.length,
|
itemCount: _tabs.length,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (_, index) => AppTab.values[index].page,
|
itemBuilder: (_, index) => _tabs[index].page,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
if (!_switchingPage) {
|
if (!_switchingPage) {
|
||||||
@@ -146,7 +164,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
animationDuration: const Duration(milliseconds: 250),
|
animationDuration: const Duration(milliseconds: 250),
|
||||||
onDestinationSelected: _onDestinationSelected,
|
onDestinationSelected: _onDestinationSelected,
|
||||||
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
|
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||||
destinations: AppTab.navDestinations,
|
destinations: _tabs.map((tab) => tab.navDestination).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,7 +183,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
trailing: extended ? const SizedBox(height: 20) : null,
|
trailing: extended ? const SizedBox(height: 20) : null,
|
||||||
labelType: extended ? NavigationRailLabelType.none : NavigationRailLabelType.all,
|
labelType: extended ? NavigationRailLabelType.none : NavigationRailLabelType.all,
|
||||||
selectedIndex: idx,
|
selectedIndex: idx,
|
||||||
destinations: AppTab.navRailDestinations,
|
destinations: _tabs.map((tab) => tab.navRailDestination).toList(),
|
||||||
onDestinationSelected: _onDestinationSelected,
|
onDestinationSelected: _onDestinationSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -236,6 +254,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
|
|
||||||
void _onDestinationSelected(int index) {
|
void _onDestinationSelected(int index) {
|
||||||
if (_selectIndex.value == index) return;
|
if (_selectIndex.value == index) return;
|
||||||
|
if (index < 0 || index >= _tabs.length) return;
|
||||||
_selectIndex.value = index;
|
_selectIndex.value = index;
|
||||||
_switchingPage = true;
|
_switchingPage = true;
|
||||||
_pageController.animateToPage(
|
_pageController.animateToPage(
|
||||||
|
|||||||
360
lib/view/page/server/connection_stats.dart
Normal file
360
lib/view/page/server/connection_stats.dart
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
|
class ConnectionStatsPage extends StatefulWidget {
|
||||||
|
const ConnectionStatsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ConnectionStatsPage> createState() => _ConnectionStatsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
||||||
|
List<ServerConnectionStats> _serverStats = [];
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadStats() {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final stats = Stores.connectionStats.getAllServerStats();
|
||||||
|
setState(() {
|
||||||
|
_serverStats = stats;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: CustomAppBar(
|
||||||
|
title: Text(l10n.connectionStats),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _loadStats,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: libL10n.refresh,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _showClearAllDialog,
|
||||||
|
icon: const Icon(Icons.clear_all, color: Colors.red),
|
||||||
|
tooltip: libL10n.clear,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _buildBody,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _buildBody {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(child: SizedLoading.large);
|
||||||
|
}
|
||||||
|
if (_serverStats.isEmpty) {
|
||||||
|
return Center(child: Text(l10n.noConnectionStatsData));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: _serverStats.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final stats = _serverStats[index];
|
||||||
|
return _buildServerStatsCard(stats);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildServerStatsCard(ServerConnectionStats stats) {
|
||||||
|
final successRate = stats.totalAttempts == 0
|
||||||
|
? 'N/A'
|
||||||
|
: '${(stats.successRate * 100).toStringAsFixed(1)}%';
|
||||||
|
final lastSuccessTime = stats.lastSuccessTime;
|
||||||
|
final lastFailureTime = stats.lastFailureTime;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
stats.serverName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${libL10n.success}: $successRate',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: stats.successRate >= 0.8
|
||||||
|
? Colors.green
|
||||||
|
: stats.successRate >= 0.5
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildStatItem(
|
||||||
|
l10n.totalAttempts,
|
||||||
|
stats.totalAttempts.toString(),
|
||||||
|
Icons.all_inclusive,
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
libL10n.success,
|
||||||
|
stats.successCount.toString(),
|
||||||
|
Icons.check_circle,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
libL10n.fail,
|
||||||
|
stats.failureCount.toString(),
|
||||||
|
Icons.error,
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (lastSuccessTime != null || lastFailureTime != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (lastSuccessTime != null)
|
||||||
|
_buildTimeItem(
|
||||||
|
l10n.lastSuccess,
|
||||||
|
lastSuccessTime,
|
||||||
|
Icons.check_circle,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
if (lastFailureTime != null)
|
||||||
|
_buildTimeItem(
|
||||||
|
l10n.lastFailure,
|
||||||
|
lastFailureTime,
|
||||||
|
Icons.error,
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.recentConnections,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _showServerDetailsDialog(stats),
|
||||||
|
child: Text(l10n.viewDetails),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...stats.recentConnections.take(3).map(_buildConnectionItem),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatItem(
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
IconData icon, [
|
||||||
|
Color? color,
|
||||||
|
]) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 24, color: color ?? Colors.grey),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeItem(
|
||||||
|
String label,
|
||||||
|
DateTime time,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
final timeStr = time.simple();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: color),
|
||||||
|
UIs.width7,
|
||||||
|
Text(
|
||||||
|
'$label: ',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildConnectionItem(ConnectionStat stat) {
|
||||||
|
final timeStr = stat.timestamp.simple();
|
||||||
|
final isSuccess = stat.result.isSuccess;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isSuccess ? Icons.check_circle : Icons.error,
|
||||||
|
size: 16,
|
||||||
|
color: isSuccess ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
UIs.width7,
|
||||||
|
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||||
|
UIs.width7,
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
isSuccess
|
||||||
|
? '${libL10n.success} (${stat.durationMs}ms)'
|
||||||
|
: stat.result.displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isSuccess ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on _ConnectionStatsPageState {
|
||||||
|
void _showServerDetailsDialog(ServerConnectionStats stats) {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: '${stats.serverName} - ${l10n.connectionDetails}',
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
height: MediaQuery.sizeOf(context).height * 0.7,
|
||||||
|
child: ListView.separated(
|
||||||
|
itemCount: stats.recentConnections.length,
|
||||||
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final stat = stats.recentConnections[index];
|
||||||
|
final timeStr = stat.timestamp.simple();
|
||||||
|
final isSuccess = stat.result.isSuccess;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: Icon(
|
||||||
|
isSuccess ? Icons.check_circle : Icons.error,
|
||||||
|
color: isSuccess ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
title: Text(timeStr),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isSuccess
|
||||||
|
? '${libL10n.success} (${stat.durationMs}ms)'
|
||||||
|
: '${libL10n.fail}: ${stat.result.displayName}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSuccess ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isSuccess && stat.errorMessage.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
stat.errorMessage,
|
||||||
|
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: context.pop, child: Text(libL10n.close)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_showClearServerStatsDialog(stats);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
l10n.clearThisServerStats,
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showClearAllDialog() {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: l10n.clearAllStatsTitle,
|
||||||
|
child: Text(l10n.clearAllStatsContent),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||||
|
CountDownBtn(
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
Stores.connectionStats.clearAll();
|
||||||
|
_loadStats();
|
||||||
|
},
|
||||||
|
text: libL10n.ok,
|
||||||
|
afterColor: Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showClearServerStatsDialog(ServerConnectionStats stats) {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: l10n.clearServerStatsTitle(stats.serverName),
|
||||||
|
child: Text(l10n.clearServerStatsContent(stats.serverName)),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||||
|
CountDownBtn(
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
Stores.connectionStats.clearServerStats(stats.serverId);
|
||||||
|
_loadStats();
|
||||||
|
},
|
||||||
|
text: libL10n.ok,
|
||||||
|
afterColor: Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import 'package:server_box/data/model/server/system.dart';
|
|||||||
import 'package:server_box/data/provider/server/single.dart';
|
import 'package:server_box/data/provider/server/single.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/pve.dart';
|
import 'package:server_box/view/page/pve.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|
||||||
part 'misc.dart';
|
part 'misc.dart';
|
||||||
|
|||||||
@@ -1,964 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:choice/choice.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
|
||||||
import 'package:server_box/core/route.dart';
|
|
||||||
import 'package:server_box/core/utils/server_dedup.dart';
|
|
||||||
import 'package:server_box/core/utils/ssh_config.dart';
|
|
||||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
|
||||||
import 'package:server_box/data/model/server/custom.dart';
|
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
||||||
import 'package:server_box/data/model/server/system.dart';
|
|
||||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
|
||||||
import 'package:server_box/data/provider/private_key.dart';
|
|
||||||
import 'package:server_box/data/provider/server/all.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
import 'package:server_box/data/store/server.dart';
|
|
||||||
import 'package:server_box/view/page/private_key/edit.dart';
|
|
||||||
|
|
||||||
class ServerEditPage extends ConsumerStatefulWidget {
|
|
||||||
final SpiRequiredArgs? args;
|
|
||||||
|
|
||||||
const ServerEditPage({super.key, this.args});
|
|
||||||
|
|
||||||
static const route = AppRoute<bool, SpiRequiredArgs>(page: ServerEditPage.new, path: '/servers/edit');
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayoutMixin {
|
|
||||||
late final spi = widget.args?.spi;
|
|
||||||
final _nameController = TextEditingController();
|
|
||||||
final _ipController = TextEditingController();
|
|
||||||
final _altUrlController = TextEditingController();
|
|
||||||
final _portController = TextEditingController();
|
|
||||||
final _usernameController = TextEditingController();
|
|
||||||
final _passwordController = TextEditingController();
|
|
||||||
final _pveAddrCtrl = TextEditingController();
|
|
||||||
final _preferTempDevCtrl = TextEditingController();
|
|
||||||
final _logoUrlCtrl = TextEditingController();
|
|
||||||
final _wolMacCtrl = TextEditingController();
|
|
||||||
final _wolIpCtrl = TextEditingController();
|
|
||||||
final _wolPwdCtrl = TextEditingController();
|
|
||||||
final _netDevCtrl = TextEditingController();
|
|
||||||
final _scriptDirCtrl = TextEditingController();
|
|
||||||
|
|
||||||
final _nameFocus = FocusNode();
|
|
||||||
final _ipFocus = FocusNode();
|
|
||||||
final _alterUrlFocus = FocusNode();
|
|
||||||
final _portFocus = FocusNode();
|
|
||||||
final _usernameFocus = FocusNode();
|
|
||||||
|
|
||||||
late FocusScopeNode _focusScope;
|
|
||||||
|
|
||||||
/// -1: non selected, null: password, others: index of private key
|
|
||||||
final _keyIdx = ValueNotifier<int?>(null);
|
|
||||||
final _autoConnect = ValueNotifier(true);
|
|
||||||
final _jumpServer = nvn<String?>();
|
|
||||||
final _pveIgnoreCert = ValueNotifier(false);
|
|
||||||
final _env = <String, String>{}.vn;
|
|
||||||
final _customCmds = <String, String>{}.vn;
|
|
||||||
final _tags = <String>{}.vn;
|
|
||||||
final _systemType = ValueNotifier<SystemType?>(null);
|
|
||||||
final _disabledCmdTypes = <String>{}.vn;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_nameController.dispose();
|
|
||||||
_ipController.dispose();
|
|
||||||
_altUrlController.dispose();
|
|
||||||
_portController.dispose();
|
|
||||||
_usernameController.dispose();
|
|
||||||
_passwordController.dispose();
|
|
||||||
_preferTempDevCtrl.dispose();
|
|
||||||
_logoUrlCtrl.dispose();
|
|
||||||
_wolMacCtrl.dispose();
|
|
||||||
_wolIpCtrl.dispose();
|
|
||||||
_wolPwdCtrl.dispose();
|
|
||||||
_netDevCtrl.dispose();
|
|
||||||
_scriptDirCtrl.dispose();
|
|
||||||
|
|
||||||
_nameFocus.dispose();
|
|
||||||
_ipFocus.dispose();
|
|
||||||
_alterUrlFocus.dispose();
|
|
||||||
_portFocus.dispose();
|
|
||||||
_usernameFocus.dispose();
|
|
||||||
_pveAddrCtrl.dispose();
|
|
||||||
|
|
||||||
_keyIdx.dispose();
|
|
||||||
_autoConnect.dispose();
|
|
||||||
_jumpServer.dispose();
|
|
||||||
_pveIgnoreCert.dispose();
|
|
||||||
_env.dispose();
|
|
||||||
_customCmds.dispose();
|
|
||||||
_tags.dispose();
|
|
||||||
_systemType.dispose();
|
|
||||||
_disabledCmdTypes.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_focusScope = FocusScope.of(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final actions = <Widget>[];
|
|
||||||
if (spi != null) actions.add(_buildDelBtn());
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => _focusScope.unfocus(),
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
|
|
||||||
body: _buildForm(),
|
|
||||||
floatingActionButton: _buildFAB(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildForm() {
|
|
||||||
final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan(), if (isDesktop) _buildSSHImport()];
|
|
||||||
final children = [
|
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: topItems.joinWith(UIs.width13).toList()),
|
|
||||||
Input(
|
|
||||||
autoFocus: true,
|
|
||||||
controller: _nameController,
|
|
||||||
type: TextInputType.text,
|
|
||||||
node: _nameFocus,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
|
|
||||||
hint: libL10n.example,
|
|
||||||
label: libL10n.name,
|
|
||||||
icon: BoxIcons.bx_rename,
|
|
||||||
obscureText: false,
|
|
||||||
autoCorrect: true,
|
|
||||||
suggestion: true,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _ipController,
|
|
||||||
type: TextInputType.url,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
|
||||||
node: _ipFocus,
|
|
||||||
label: l10n.host,
|
|
||||||
icon: BoxIcons.bx_server,
|
|
||||||
hint: 'example.com',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _portController,
|
|
||||||
type: TextInputType.number,
|
|
||||||
node: _portFocus,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
|
|
||||||
label: l10n.port,
|
|
||||||
icon: Bootstrap.number_123,
|
|
||||||
hint: '22',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _usernameController,
|
|
||||||
type: TextInputType.text,
|
|
||||||
node: _usernameFocus,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),
|
|
||||||
label: libL10n.user,
|
|
||||||
icon: Icons.account_box,
|
|
||||||
hint: 'root',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
TagTile(tags: _tags, allTags: ref.watch(serversNotifierProvider).tags).cardx,
|
|
||||||
ListTile(
|
|
||||||
title: Text(l10n.autoConnect),
|
|
||||||
trailing: _autoConnect.listenVal(
|
|
||||||
(val) => Switch(
|
|
||||||
value: val,
|
|
||||||
onChanged: (val) {
|
|
||||||
_autoConnect.value = val;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildAuth(),
|
|
||||||
_buildSystemType(),
|
|
||||||
_buildJumpServer(),
|
|
||||||
_buildMore(),
|
|
||||||
];
|
|
||||||
return AutoMultiList(children: children);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAuth() {
|
|
||||||
final switch_ = ListTile(
|
|
||||||
title: Text(l10n.keyAuth),
|
|
||||||
trailing: _keyIdx.listenVal(
|
|
||||||
(v) => Switch(
|
|
||||||
value: v != null,
|
|
||||||
onChanged: (val) {
|
|
||||||
if (val) {
|
|
||||||
_keyIdx.value = -1;
|
|
||||||
} else {
|
|
||||||
_keyIdx.value = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Put [switch_] out of [ValueBuilder] to avoid rebuild
|
|
||||||
return _keyIdx.listenVal((v) {
|
|
||||||
final children = <Widget>[switch_];
|
|
||||||
if (v != null) {
|
|
||||||
children.add(_buildKeyAuth());
|
|
||||||
} else {
|
|
||||||
children.add(
|
|
||||||
Input(
|
|
||||||
controller: _passwordController,
|
|
||||||
obscureText: true,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: libL10n.pwd,
|
|
||||||
icon: Icons.password,
|
|
||||||
suggestion: false,
|
|
||||||
onSubmitted: (_) => _onSave(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Column(children: children);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildKeyAuth() {
|
|
||||||
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
|
||||||
final pkis = privateKeyState.keys;
|
|
||||||
|
|
||||||
final tiles = List<Widget>.generate(pkis.length, (index) {
|
|
||||||
final e = pkis[index];
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.only(left: 10, right: 15),
|
|
||||||
leading: Radio<int>(value: index),
|
|
||||||
title: Text(e.id, textAlign: TextAlign.start),
|
|
||||||
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
|
|
||||||
trailing: Btn.icon(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)),
|
|
||||||
),
|
|
||||||
onTap: () => _keyIdx.value = index,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
title: Text(libL10n.add),
|
|
||||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
|
||||||
trailing: const Icon(Icons.add),
|
|
||||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return RadioGroup<int>(
|
|
||||||
onChanged: (val) => _keyIdx.value = val,
|
|
||||||
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEnvs() {
|
|
||||||
return _env.listenVal((val) {
|
|
||||||
final subtitle = val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(HeroIcons.variable),
|
|
||||||
subtitle: subtitle,
|
|
||||||
title: Text(l10n.envVars),
|
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
||||||
onTap: () async {
|
|
||||||
final res = await KvEditor.route.go(context, KvEditorArgs(data: spi?.envs ?? {}));
|
|
||||||
if (res == null) return;
|
|
||||||
_env.value = res;
|
|
||||||
},
|
|
||||||
).cardx;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMore() {
|
|
||||||
return ExpandTile(
|
|
||||||
title: Text(l10n.more),
|
|
||||||
children: [
|
|
||||||
Input(
|
|
||||||
controller: _logoUrlCtrl,
|
|
||||||
type: TextInputType.url,
|
|
||||||
icon: Icons.image,
|
|
||||||
label: 'Logo URL',
|
|
||||||
hint: 'https://example.com/logo.png',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
_buildAltUrl(),
|
|
||||||
_buildScriptDir(),
|
|
||||||
_buildEnvs(),
|
|
||||||
_buildPVEs(),
|
|
||||||
_buildCustomCmds(),
|
|
||||||
_buildDisabledCmdTypes(),
|
|
||||||
_buildCustomDev(),
|
|
||||||
_buildWOLs(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildScriptDir() {
|
|
||||||
return Input(
|
|
||||||
controller: _scriptDirCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: '${l10n.remotePath} (Shell ${l10n.install})',
|
|
||||||
icon: Icons.folder,
|
|
||||||
hint: '~/.config/server_box',
|
|
||||||
suggestion: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCustomDev() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CenterGreyTitle(l10n.specifyDev),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MingCute.question_line),
|
|
||||||
title: TipText(libL10n.note, l10n.specifyDevTip),
|
|
||||||
).cardx,
|
|
||||||
Input(
|
|
||||||
controller: _preferTempDevCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: l10n.temperature,
|
|
||||||
icon: MingCute.low_temperature_line,
|
|
||||||
hint: 'nvme-pci-0400',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _netDevCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: l10n.net,
|
|
||||||
icon: ZondIcons.network,
|
|
||||||
hint: 'eth0',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSystemType() {
|
|
||||||
return _systemType.listenVal((val) {
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(MingCute.laptop_2_line),
|
|
||||||
title: Text(l10n.system),
|
|
||||||
trailing: PopupMenu<SystemType?>(
|
|
||||||
initialValue: val,
|
|
||||||
items: [
|
|
||||||
PopupMenuItem(value: null, child: Text(libL10n.auto)),
|
|
||||||
PopupMenuItem(value: SystemType.linux, child: Text('Linux')),
|
|
||||||
PopupMenuItem(value: SystemType.bsd, child: Text('BSD')),
|
|
||||||
PopupMenuItem(value: SystemType.windows, child: Text('Windows')),
|
|
||||||
],
|
|
||||||
onSelected: (value) => _systemType.value = value,
|
|
||||||
child: Text(val?.name ?? libL10n.auto, style: TextStyle(color: val == null ? Colors.grey : null)),
|
|
||||||
),
|
|
||||||
).cardx;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAltUrl() {
|
|
||||||
return Input(
|
|
||||||
controller: _altUrlController,
|
|
||||||
type: TextInputType.url,
|
|
||||||
node: _alterUrlFocus,
|
|
||||||
label: l10n.fallbackSshDest,
|
|
||||||
icon: MingCute.link_line,
|
|
||||||
hint: 'user@ip:port',
|
|
||||||
suggestion: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPVEs() {
|
|
||||||
const addr = 'https://127.0.0.1:8006';
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const CenterGreyTitle('PVE'),
|
|
||||||
Input(
|
|
||||||
controller: _pveAddrCtrl,
|
|
||||||
type: TextInputType.url,
|
|
||||||
icon: MingCute.web_line,
|
|
||||||
label: 'URL',
|
|
||||||
hint: addr,
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MingCute.certificate_line),
|
|
||||||
title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),
|
|
||||||
trailing: _pveIgnoreCert.listenVal(
|
|
||||||
(v) => Switch(
|
|
||||||
value: v,
|
|
||||||
onChanged: (val) {
|
|
||||||
_pveIgnoreCert.value = val;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).cardx,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCustomCmds() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CenterGreyTitle(l10n.customCmd),
|
|
||||||
_customCmds.listenVal((vals) {
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(BoxIcons.bxs_file_json),
|
|
||||||
title: const Text('JSON'),
|
|
||||||
subtitle: vals.isEmpty ? null : Text(vals.keys.join(','), style: UIs.textGrey),
|
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
||||||
onTap: _onTapCustomItem,
|
|
||||||
);
|
|
||||||
}).cardx,
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MingCute.doc_line),
|
|
||||||
title: Text(libL10n.doc),
|
|
||||||
trailing: const Icon(Icons.open_in_new, size: 17),
|
|
||||||
onTap: l10n.customCmdDocUrl.launchUrl,
|
|
||||||
).cardx,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDisabledCmdTypes() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
|
|
||||||
_disabledCmdTypes.listenVal((disabled) {
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.disabled_by_default),
|
|
||||||
title: Text('${libL10n.disabled} ${l10n.cmd}'),
|
|
||||||
subtitle: disabled.isEmpty ? null : Text(disabled.join(', '), style: UIs.textGrey),
|
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
||||||
onTap: _onTapDisabledCmdTypes,
|
|
||||||
);
|
|
||||||
}).cardx,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildWOLs() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const CenterGreyTitle('Wake On LAN (beta)'),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(BoxIcons.bxs_help_circle),
|
|
||||||
title: TipText(libL10n.about, l10n.wolTip),
|
|
||||||
).cardx,
|
|
||||||
Input(
|
|
||||||
controller: _wolMacCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: 'MAC ${l10n.addr}',
|
|
||||||
icon: Icons.computer,
|
|
||||||
hint: '00:11:22:33:44:55',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _wolIpCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: 'IP ${l10n.addr}',
|
|
||||||
icon: ZondIcons.network,
|
|
||||||
hint: '192.168.1.x',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _wolPwdCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
obscureText: true,
|
|
||||||
label: libL10n.pwd,
|
|
||||||
icon: Icons.password,
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFAB() {
|
|
||||||
return FloatingActionButton(onPressed: _onSave, child: const Icon(Icons.save));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildJumpServer() {
|
|
||||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
|
||||||
final srvs = ref
|
|
||||||
.watch(serversNotifierProvider)
|
|
||||||
.servers
|
|
||||||
.values
|
|
||||||
.where((e) => e.jumpId == null)
|
|
||||||
.where((e) => e.id != spi?.id)
|
|
||||||
.toList();
|
|
||||||
final choice = _jumpServer.listenVal((val) {
|
|
||||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
|
||||||
return Choice<Spi>(
|
|
||||||
multiple: false,
|
|
||||||
clearable: true,
|
|
||||||
value: srv != null ? [srv] : [],
|
|
||||||
builder: (state, _) => Wrap(
|
|
||||||
children: List<Widget>.generate(srvs.length, (index) {
|
|
||||||
final item = srvs[index];
|
|
||||||
return ChoiceChipX<Spi>(
|
|
||||||
label: item.name,
|
|
||||||
state: state,
|
|
||||||
value: item,
|
|
||||||
onSelected: (srv, on) {
|
|
||||||
if (on) {
|
|
||||||
_jumpServer.value = srv.id;
|
|
||||||
} else {
|
|
||||||
_jumpServer.value = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return ExpandTile(
|
|
||||||
leading: const Icon(Icons.map),
|
|
||||||
initiallyExpanded: _jumpServer.value != null,
|
|
||||||
childrenPadding: padding,
|
|
||||||
title: Text(l10n.jumpServer),
|
|
||||||
children: [choice],
|
|
||||||
).cardx;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildWriteScriptTip() {
|
|
||||||
return Btn.tile(
|
|
||||||
text: libL10n.attention,
|
|
||||||
icon: const Icon(Icons.tips_and_updates, color: Colors.grey),
|
|
||||||
onTap: () {
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.attention,
|
|
||||||
child: SimpleMarkdown(data: l10n.writeScriptTip),
|
|
||||||
actions: Btnx.oks,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
textStyle: UIs.textGrey,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQrScan() {
|
|
||||||
return Btn.tile(
|
|
||||||
text: libL10n.import,
|
|
||||||
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
|
||||||
onTap: () async {
|
|
||||||
final ret = await BarcodeScannerPage.route.go(context, args: const BarcodeScannerPageArgs());
|
|
||||||
final code = ret?.text;
|
|
||||||
if (code == null) return;
|
|
||||||
try {
|
|
||||||
final spi = Spi.fromJson(json.decode(code));
|
|
||||||
_initWithSpi(spi);
|
|
||||||
} catch (e, s) {
|
|
||||||
context.showErrDialog(e, s);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textStyle: UIs.textGrey,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSSHImport() {
|
|
||||||
return Btn.tile(
|
|
||||||
text: l10n.sshConfigImport,
|
|
||||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
|
||||||
onTap: _onTapSSHImport,
|
|
||||||
textStyle: UIs.textGrey,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDelBtn() {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.attention,
|
|
||||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.server}(${spi!.name})')),
|
|
||||||
actions: Btn.ok(
|
|
||||||
onTap: () async {
|
|
||||||
context.pop();
|
|
||||||
ref.read(serversNotifierProvider.notifier).delServer(spi!.id);
|
|
||||||
context.pop(true);
|
|
||||||
},
|
|
||||||
red: true,
|
|
||||||
).toList,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void afterFirstLayout(BuildContext context) {
|
|
||||||
if (spi != null) {
|
|
||||||
_initWithSpi(spi!);
|
|
||||||
} else {
|
|
||||||
// Only for new servers, check SSH config import on first time
|
|
||||||
_checkSSHConfigImport();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _Actions on _ServerEditPageState {
|
|
||||||
void _onTapSSHImport() async {
|
|
||||||
try {
|
|
||||||
final servers = await SSHConfig.parseConfig();
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
context.showSnackBar(l10n.sshConfigNoServers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dprint('Parsed ${servers.length} servers from SSH config');
|
|
||||||
await _processSSHServers(servers);
|
|
||||||
dprint('Finished processing SSH config servers');
|
|
||||||
} catch (e, s) {
|
|
||||||
_handleImportSSHCfgPermissionIssue(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
|
||||||
dprint('Error importing SSH config: $e');
|
|
||||||
// Check if it's a permission error and offer file picker as fallback
|
|
||||||
if (e is PathAccessException || e.toString().contains('Operation not permitted')) {
|
|
||||||
final useFilePicker = await context.showRoundDialog<bool>(
|
|
||||||
title: l10n.sshConfigImport,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.sshConfigPermissionDenied),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(l10n.sshConfigManualSelect),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: Btnx.cancelOk,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (useFilePicker == true) {
|
|
||||||
await _onTapSSHImportWithFilePicker();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context.showErrDialog(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _processSSHServers(List<Spi> servers) async {
|
|
||||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
|
||||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
|
||||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
|
||||||
|
|
||||||
if (!summary.hasItemsToImport) {
|
|
||||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final shouldImport = await context.showRoundDialog<bool>(
|
|
||||||
title: l10n.sshConfigImport,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
|
||||||
if (summary.hasDuplicates)
|
|
||||||
Text(l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'), style: UIs.textGrey),
|
|
||||||
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
...resolved.map((s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: Btnx.cancelOk,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldImport == true) {
|
|
||||||
for (final server in resolved) {
|
|
||||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
|
||||||
}
|
|
||||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onTapSSHImportWithFilePicker() async {
|
|
||||||
try {
|
|
||||||
final result = await FilePicker.platform.pickFiles(
|
|
||||||
type: FileType.any,
|
|
||||||
allowMultiple: false,
|
|
||||||
dialogTitle: 'SSH ${libL10n.select}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result?.files.single.path case final path?) {
|
|
||||||
final servers = await SSHConfig.parseConfig(path);
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
context.showSnackBar(l10n.sshConfigNoServers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _processSSHServers(servers);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
context.showErrDialog(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTapCustomItem() async {
|
|
||||||
final res = await KvEditor.route.go(context, KvEditorArgs(data: _customCmds.value));
|
|
||||||
if (res == null) return;
|
|
||||||
_customCmds.value = res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTapDisabledCmdTypes() async {
|
|
||||||
final allCmdTypes = ShellCmdType.all;
|
|
||||||
|
|
||||||
// [TimeSeq] depends on the `time` cmd type, so it should be removed from the list
|
|
||||||
allCmdTypes.remove(StatusCmdType.time);
|
|
||||||
|
|
||||||
await _showCmdTypesDialog(allCmdTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSave() async {
|
|
||||||
if (_ipController.text.isEmpty) {
|
|
||||||
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
|
||||||
final ok = await context.showRoundDialog<bool>(
|
|
||||||
title: libL10n.attention,
|
|
||||||
child: Text(libL10n.askContinue(l10n.useNoPwd)),
|
|
||||||
actions: Btnx.cancelRedOk,
|
|
||||||
);
|
|
||||||
if (ok != true) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If [_pubKeyIndex] is -1, it means that the user has not selected
|
|
||||||
if (_keyIdx.value == -1) {
|
|
||||||
context.showSnackBar(libL10n.empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_usernameController.text.isEmpty) {
|
|
||||||
_usernameController.text = 'root';
|
|
||||||
}
|
|
||||||
if (_portController.text.isEmpty) {
|
|
||||||
_portController.text = '22';
|
|
||||||
}
|
|
||||||
final customCmds = _customCmds.value;
|
|
||||||
final custom = ServerCustom(
|
|
||||||
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
|
||||||
pveIgnoreCert: _pveIgnoreCert.value,
|
|
||||||
cmds: customCmds.isEmpty ? null : customCmds,
|
|
||||||
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
|
||||||
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
|
|
||||||
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
|
|
||||||
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
|
|
||||||
);
|
|
||||||
|
|
||||||
final wolEmpty = _wolMacCtrl.text.isEmpty && _wolIpCtrl.text.isEmpty && _wolPwdCtrl.text.isEmpty;
|
|
||||||
final wol = wolEmpty
|
|
||||||
? null
|
|
||||||
: WakeOnLanCfg(mac: _wolMacCtrl.text, ip: _wolIpCtrl.text, pwd: _wolPwdCtrl.text.selfNotEmptyOrNull);
|
|
||||||
if (wol != null) {
|
|
||||||
final wolValidation = wol.validate();
|
|
||||||
if (!wolValidation.$2) {
|
|
||||||
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final spi = Spi(
|
|
||||||
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
|
|
||||||
ip: _ipController.text,
|
|
||||||
port: int.parse(_portController.text),
|
|
||||||
user: _usernameController.text,
|
|
||||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
|
||||||
keyId: _keyIdx.value != null
|
|
||||||
? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id
|
|
||||||
: null,
|
|
||||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
|
||||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
|
||||||
autoConnect: _autoConnect.value,
|
|
||||||
jumpId: _jumpServer.value,
|
|
||||||
custom: custom,
|
|
||||||
wolCfg: wol,
|
|
||||||
envs: _env.value.isEmpty ? null : _env.value,
|
|
||||||
id: widget.args?.spi.id ?? ShortId.generate(),
|
|
||||||
customSystemType: _systemType.value,
|
|
||||||
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.spi == null) {
|
|
||||||
final existsIds = ServerStore.instance.box.keys;
|
|
||||||
if (existsIds.contains(spi.id)) {
|
|
||||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ref.read(serversNotifierProvider.notifier).addServer(spi);
|
|
||||||
} else {
|
|
||||||
ref.read(serversNotifierProvider.notifier).updateServer(this.spi!, spi);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _Utils on _ServerEditPageState {
|
|
||||||
void _checkSSHConfigImport() async {
|
|
||||||
final prop = Stores.setting.firstTimeReadSSHCfg;
|
|
||||||
// Only check if it's first time and user hasn't disabled it
|
|
||||||
if (!prop.fetch()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if SSH config exists
|
|
||||||
final (_, configExists) = SSHConfig.configExists();
|
|
||||||
if (!configExists) return;
|
|
||||||
|
|
||||||
// Ask for permission
|
|
||||||
final hasPermission = await context.showRoundDialog<bool>(
|
|
||||||
title: l10n.sshConfigImport,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.sshConfigFound),
|
|
||||||
UIs.height7,
|
|
||||||
Text(l10n.sshConfigImportPermission),
|
|
||||||
UIs.height7,
|
|
||||||
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: Btnx.cancelOk,
|
|
||||||
);
|
|
||||||
|
|
||||||
prop.put(false);
|
|
||||||
|
|
||||||
if (hasPermission == true) {
|
|
||||||
// Parse and import SSH config
|
|
||||||
final servers = await SSHConfig.parseConfig();
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
context.showSnackBar(l10n.sshConfigNoServers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
|
||||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
|
||||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
|
||||||
|
|
||||||
if (!summary.hasItemsToImport) {
|
|
||||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import without asking again since user already gave permission
|
|
||||||
for (final server in resolved) {
|
|
||||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
|
||||||
}
|
|
||||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
_handleImportSSHCfgPermissionIssue(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
|
||||||
return context.showRoundDialog(
|
|
||||||
title: '${libL10n.disabled} ${l10n.cmd}',
|
|
||||||
child: SizedBox(
|
|
||||||
width: 270,
|
|
||||||
child: _disabledCmdTypes.listenVal((disabled) {
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: allCmdTypes.length,
|
|
||||||
itemExtent: 50,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final cmdType = allCmdTypes.elementAtOrNull(index);
|
|
||||||
if (cmdType == null) return UIs.placeholder;
|
|
||||||
final display = cmdType.displayName;
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(cmdType.sysType.icon, size: 20),
|
|
||||||
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
|
|
||||||
trailing: Checkbox(
|
|
||||||
value: disabled.contains(display),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
if (value) {
|
|
||||||
_disabledCmdTypes.value.add(display);
|
|
||||||
} else {
|
|
||||||
_disabledCmdTypes.value.remove(display);
|
|
||||||
}
|
|
||||||
_disabledCmdTypes.notify();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
final isDisabled = disabled.contains(display);
|
|
||||||
if (isDisabled) {
|
|
||||||
_disabledCmdTypes.value.remove(display);
|
|
||||||
} else {
|
|
||||||
_disabledCmdTypes.value.add(display);
|
|
||||||
}
|
|
||||||
_disabledCmdTypes.notify();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
actions: Btnx.oks,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initWithSpi(Spi spi) {
|
|
||||||
_nameController.text = spi.name;
|
|
||||||
_ipController.text = spi.ip;
|
|
||||||
_portController.text = spi.port.toString();
|
|
||||||
_usernameController.text = spi.user;
|
|
||||||
if (spi.keyId == null) {
|
|
||||||
_passwordController.text = spi.pwd ?? '';
|
|
||||||
} else {
|
|
||||||
_keyIdx.value = ref.read(privateKeyNotifierProvider).keys.indexWhere((e) => e.id == spi.keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List in dart is passed by pointer, so you need to copy it here
|
|
||||||
_tags.value = spi.tags?.toSet() ?? {};
|
|
||||||
|
|
||||||
_altUrlController.text = spi.alterUrl ?? '';
|
|
||||||
_autoConnect.value = spi.autoConnect;
|
|
||||||
_jumpServer.value = spi.jumpId;
|
|
||||||
|
|
||||||
final custom = spi.custom;
|
|
||||||
if (custom != null) {
|
|
||||||
_pveAddrCtrl.text = custom.pveAddr ?? '';
|
|
||||||
_pveIgnoreCert.value = custom.pveIgnoreCert;
|
|
||||||
_customCmds.value = custom.cmds ?? {};
|
|
||||||
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
|
||||||
_logoUrlCtrl.text = custom.logoUrl ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
final wol = spi.wolCfg;
|
|
||||||
if (wol != null) {
|
|
||||||
_wolMacCtrl.text = wol.mac;
|
|
||||||
_wolIpCtrl.text = wol.ip;
|
|
||||||
_wolPwdCtrl.text = wol.pwd ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
_env.value = spi.envs ?? {};
|
|
||||||
|
|
||||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
|
||||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
|
||||||
|
|
||||||
_systemType.value = spi.customSystemType;
|
|
||||||
|
|
||||||
final disabledCmdTypes = spi.disabledCmdTypes?.toSet() ?? {};
|
|
||||||
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
|
|
||||||
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
|
|
||||||
_disabledCmdTypes.value = disabledCmdTypes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
382
lib/view/page/server/edit/actions.dart
Normal file
382
lib/view/page/server/edit/actions.dart
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
part of 'edit.dart';
|
||||||
|
|
||||||
|
extension _Actions on _ServerEditPageState {
|
||||||
|
void _onTapSSHImport() async {
|
||||||
|
try {
|
||||||
|
final servers = await SSHConfig.parseConfig();
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.showSnackBar(l10n.sshConfigNoServers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dprint('Parsed ${servers.length} servers from SSH config');
|
||||||
|
await _processSSHServers(servers);
|
||||||
|
dprint('Finished processing SSH config servers');
|
||||||
|
} catch (e, s) {
|
||||||
|
_handleImportSSHCfgPermissionIssue(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
||||||
|
dprint('Error importing SSH config: $e');
|
||||||
|
// Check if it's a permission error and offer file picker as fallback
|
||||||
|
if (e is PathAccessException ||
|
||||||
|
e.toString().contains('Operation not permitted')) {
|
||||||
|
final useFilePicker = await context.showRoundDialog<bool>(
|
||||||
|
title: l10n.sshConfigImport,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.sshConfigPermissionDenied),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(l10n.sshConfigManualSelect),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (useFilePicker == true) {
|
||||||
|
await _onTapSSHImportWithFilePicker();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.showErrDialog(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processSSHServers(List<Spi> servers) async {
|
||||||
|
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||||
|
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||||
|
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||||
|
|
||||||
|
if (!summary.hasItemsToImport) {
|
||||||
|
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldImport = await context.showRoundDialog<bool>(
|
||||||
|
title: l10n.sshConfigImport,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
||||||
|
if (summary.hasDuplicates)
|
||||||
|
Text(
|
||||||
|
l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'),
|
||||||
|
style: UIs.textGrey,
|
||||||
|
),
|
||||||
|
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
...resolved.map(
|
||||||
|
(s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldImport == true) {
|
||||||
|
for (final server in resolved) {
|
||||||
|
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||||
|
}
|
||||||
|
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onTapSSHImportWithFilePicker() async {
|
||||||
|
try {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.any,
|
||||||
|
allowMultiple: false,
|
||||||
|
dialogTitle: 'SSH ${libL10n.select}',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result?.files.single.path case final path?) {
|
||||||
|
final servers = await SSHConfig.parseConfig(path);
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.showSnackBar(l10n.sshConfigNoServers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _processSSHServers(servers);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showErrDialog(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapCustomItem() async {
|
||||||
|
final res = await KvEditor.route.go(
|
||||||
|
context,
|
||||||
|
KvEditorArgs(data: _customCmds.value),
|
||||||
|
);
|
||||||
|
if (res == null) return;
|
||||||
|
_customCmds.value = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapDisabledCmdTypes() async {
|
||||||
|
final allCmdTypes = ShellCmdType.all;
|
||||||
|
|
||||||
|
// [TimeSeq] depends on the `time` cmd type, so it should be removed from the list
|
||||||
|
allCmdTypes.remove(StatusCmdType.time);
|
||||||
|
|
||||||
|
await _showCmdTypesDialog(allCmdTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSave() async {
|
||||||
|
if (_ipController.text.isEmpty) {
|
||||||
|
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||||
|
final ok = await context.showRoundDialog<bool>(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: Text(libL10n.askContinue(l10n.useNoPwd)),
|
||||||
|
actions: Btnx.cancelRedOk,
|
||||||
|
);
|
||||||
|
if (ok != true) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If [_pubKeyIndex] is -1, it means that the user has not selected
|
||||||
|
if (_keyIdx.value == -1) {
|
||||||
|
context.showSnackBar(libL10n.empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_usernameController.text.isEmpty) {
|
||||||
|
_usernameController.text = 'root';
|
||||||
|
}
|
||||||
|
if (_portController.text.isEmpty) {
|
||||||
|
_portController.text = '22';
|
||||||
|
}
|
||||||
|
final customCmds = _customCmds.value;
|
||||||
|
final custom = ServerCustom(
|
||||||
|
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
||||||
|
pveIgnoreCert: _pveIgnoreCert.value,
|
||||||
|
cmds: customCmds.isEmpty ? null : customCmds,
|
||||||
|
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
||||||
|
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
|
||||||
|
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
|
||||||
|
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
|
||||||
|
);
|
||||||
|
|
||||||
|
final wolEmpty =
|
||||||
|
_wolMacCtrl.text.isEmpty &&
|
||||||
|
_wolIpCtrl.text.isEmpty &&
|
||||||
|
_wolPwdCtrl.text.isEmpty;
|
||||||
|
final wol = wolEmpty
|
||||||
|
? null
|
||||||
|
: WakeOnLanCfg(
|
||||||
|
mac: _wolMacCtrl.text,
|
||||||
|
ip: _wolIpCtrl.text,
|
||||||
|
pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,
|
||||||
|
);
|
||||||
|
if (wol != null) {
|
||||||
|
final wolValidation = wol.validate();
|
||||||
|
if (!wolValidation.$2) {
|
||||||
|
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final spi = Spi(
|
||||||
|
name: _nameController.text.isEmpty
|
||||||
|
? _ipController.text
|
||||||
|
: _nameController.text,
|
||||||
|
ip: _ipController.text,
|
||||||
|
port: int.parse(_portController.text),
|
||||||
|
user: _usernameController.text,
|
||||||
|
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||||
|
keyId: _keyIdx.value != null
|
||||||
|
? ref
|
||||||
|
.read(privateKeyNotifierProvider)
|
||||||
|
.keys
|
||||||
|
.elementAt(_keyIdx.value!)
|
||||||
|
.id
|
||||||
|
: null,
|
||||||
|
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||||
|
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||||
|
autoConnect: _autoConnect.value,
|
||||||
|
jumpId: _jumpServer.value,
|
||||||
|
custom: custom,
|
||||||
|
wolCfg: wol,
|
||||||
|
envs: _env.value.isEmpty ? null : _env.value,
|
||||||
|
id: widget.args?.spi.id ?? ShortId.generate(),
|
||||||
|
customSystemType: _systemType.value,
|
||||||
|
disabledCmdTypes: _disabledCmdTypes.value.isEmpty
|
||||||
|
? null
|
||||||
|
: _disabledCmdTypes.value.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.spi == null) {
|
||||||
|
final existsIds = ServerStore.instance.box.keys;
|
||||||
|
if (existsIds.contains(spi.id)) {
|
||||||
|
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.read(serversNotifierProvider.notifier).addServer(spi);
|
||||||
|
} else {
|
||||||
|
ref.read(serversNotifierProvider.notifier).updateServer(this.spi!, spi);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _Utils on _ServerEditPageState {
|
||||||
|
void _checkSSHConfigImport() async {
|
||||||
|
final prop = Stores.setting.firstTimeReadSSHCfg;
|
||||||
|
// Only check if it's first time and user hasn't disabled it
|
||||||
|
if (!prop.fetch()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if SSH config exists
|
||||||
|
final (_, configExists) = SSHConfig.configExists();
|
||||||
|
if (!configExists) return;
|
||||||
|
|
||||||
|
// Ask for permission
|
||||||
|
final hasPermission = await context.showRoundDialog<bool>(
|
||||||
|
title: l10n.sshConfigImport,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.sshConfigFound),
|
||||||
|
UIs.height7,
|
||||||
|
Text(l10n.sshConfigImportPermission),
|
||||||
|
UIs.height7,
|
||||||
|
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
|
||||||
|
prop.put(false);
|
||||||
|
|
||||||
|
if (hasPermission == true) {
|
||||||
|
// Parse and import SSH config
|
||||||
|
final servers = await SSHConfig.parseConfig();
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.showSnackBar(l10n.sshConfigNoServers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||||
|
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||||
|
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||||
|
|
||||||
|
if (!summary.hasItemsToImport) {
|
||||||
|
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import without asking again since user already gave permission
|
||||||
|
for (final server in resolved) {
|
||||||
|
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||||
|
}
|
||||||
|
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
_handleImportSSHCfgPermissionIssue(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
||||||
|
return context.showRoundDialog(
|
||||||
|
title: '${libL10n.disabled} ${l10n.cmd}',
|
||||||
|
child: SizedBox(
|
||||||
|
width: 270,
|
||||||
|
child: _disabledCmdTypes.listenVal((disabled) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: allCmdTypes.length,
|
||||||
|
itemExtent: 50,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final cmdType = allCmdTypes.elementAtOrNull(index);
|
||||||
|
if (cmdType == null) return UIs.placeholder;
|
||||||
|
final display = cmdType.displayName;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(cmdType.sysType.icon, size: 20),
|
||||||
|
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
|
||||||
|
trailing: Checkbox(
|
||||||
|
value: disabled.contains(display),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
if (value) {
|
||||||
|
_disabledCmdTypes.value.add(display);
|
||||||
|
} else {
|
||||||
|
_disabledCmdTypes.value.remove(display);
|
||||||
|
}
|
||||||
|
_disabledCmdTypes.notify();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
final isDisabled = disabled.contains(display);
|
||||||
|
if (isDisabled) {
|
||||||
|
_disabledCmdTypes.value.remove(display);
|
||||||
|
} else {
|
||||||
|
_disabledCmdTypes.value.add(display);
|
||||||
|
}
|
||||||
|
_disabledCmdTypes.notify();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
actions: Btnx.oks,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initWithSpi(Spi spi) {
|
||||||
|
_nameController.text = spi.name;
|
||||||
|
_ipController.text = spi.ip;
|
||||||
|
_portController.text = spi.port.toString();
|
||||||
|
_usernameController.text = spi.user;
|
||||||
|
if (spi.keyId == null) {
|
||||||
|
_passwordController.text = spi.pwd ?? '';
|
||||||
|
} else {
|
||||||
|
_keyIdx.value = ref
|
||||||
|
.read(privateKeyNotifierProvider)
|
||||||
|
.keys
|
||||||
|
.indexWhere((e) => e.id == spi.keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List in dart is passed by pointer, so you need to copy it here
|
||||||
|
_tags.value = spi.tags?.toSet() ?? {};
|
||||||
|
|
||||||
|
_altUrlController.text = spi.alterUrl ?? '';
|
||||||
|
_autoConnect.value = spi.autoConnect;
|
||||||
|
_jumpServer.value = spi.jumpId;
|
||||||
|
|
||||||
|
final custom = spi.custom;
|
||||||
|
if (custom != null) {
|
||||||
|
_pveAddrCtrl.text = custom.pveAddr ?? '';
|
||||||
|
_pveIgnoreCert.value = custom.pveIgnoreCert;
|
||||||
|
_customCmds.value = custom.cmds ?? {};
|
||||||
|
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
||||||
|
_logoUrlCtrl.text = custom.logoUrl ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final wol = spi.wolCfg;
|
||||||
|
if (wol != null) {
|
||||||
|
_wolMacCtrl.text = wol.mac;
|
||||||
|
_wolIpCtrl.text = wol.ip;
|
||||||
|
_wolPwdCtrl.text = wol.pwd ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
_env.value = spi.envs ?? {};
|
||||||
|
|
||||||
|
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||||
|
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||||
|
|
||||||
|
_systemType.value = spi.customSystemType;
|
||||||
|
|
||||||
|
final disabledCmdTypes = spi.disabledCmdTypes?.toSet() ?? {};
|
||||||
|
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
|
||||||
|
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
|
||||||
|
_disabledCmdTypes.value = disabledCmdTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
221
lib/view/page/server/edit/edit.dart
Normal file
221
lib/view/page/server/edit/edit.dart
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:choice/choice.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/core/route.dart';
|
||||||
|
import 'package:server_box/core/utils/server_dedup.dart';
|
||||||
|
import 'package:server_box/core/utils/ssh_config.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
|
import 'package:server_box/data/model/server/custom.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
|
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||||
|
import 'package:server_box/data/provider/private_key.dart';
|
||||||
|
import 'package:server_box/data/provider/server/all.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/store/server.dart';
|
||||||
|
import 'package:server_box/view/page/private_key/edit.dart';
|
||||||
|
|
||||||
|
part 'actions.dart';
|
||||||
|
part 'widget.dart';
|
||||||
|
|
||||||
|
class ServerEditPage extends ConsumerStatefulWidget {
|
||||||
|
final SpiRequiredArgs? args;
|
||||||
|
|
||||||
|
const ServerEditPage({super.key, this.args});
|
||||||
|
|
||||||
|
static const route = AppRoute<bool, SpiRequiredArgs>(
|
||||||
|
page: ServerEditPage.new,
|
||||||
|
path: '/servers/edit',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServerEditPageState extends ConsumerState<ServerEditPage>
|
||||||
|
with AfterLayoutMixin {
|
||||||
|
late final spi = widget.args?.spi;
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _ipController = TextEditingController();
|
||||||
|
final _altUrlController = TextEditingController();
|
||||||
|
final _portController = TextEditingController();
|
||||||
|
final _usernameController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _pveAddrCtrl = TextEditingController();
|
||||||
|
final _preferTempDevCtrl = TextEditingController();
|
||||||
|
final _logoUrlCtrl = TextEditingController();
|
||||||
|
final _wolMacCtrl = TextEditingController();
|
||||||
|
final _wolIpCtrl = TextEditingController();
|
||||||
|
final _wolPwdCtrl = TextEditingController();
|
||||||
|
final _netDevCtrl = TextEditingController();
|
||||||
|
final _scriptDirCtrl = TextEditingController();
|
||||||
|
|
||||||
|
final _nameFocus = FocusNode();
|
||||||
|
final _ipFocus = FocusNode();
|
||||||
|
final _alterUrlFocus = FocusNode();
|
||||||
|
final _portFocus = FocusNode();
|
||||||
|
final _usernameFocus = FocusNode();
|
||||||
|
|
||||||
|
late FocusScopeNode _focusScope;
|
||||||
|
|
||||||
|
/// -1: non selected, null: password, others: index of private key
|
||||||
|
final _keyIdx = ValueNotifier<int?>(null);
|
||||||
|
final _autoConnect = ValueNotifier(true);
|
||||||
|
final _jumpServer = nvn<String?>();
|
||||||
|
final _pveIgnoreCert = ValueNotifier(false);
|
||||||
|
final _env = <String, String>{}.vn;
|
||||||
|
final _customCmds = <String, String>{}.vn;
|
||||||
|
final _tags = <String>{}.vn;
|
||||||
|
final _systemType = ValueNotifier<SystemType?>(null);
|
||||||
|
final _disabledCmdTypes = <String>{}.vn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_ipController.dispose();
|
||||||
|
_altUrlController.dispose();
|
||||||
|
_portController.dispose();
|
||||||
|
_usernameController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_preferTempDevCtrl.dispose();
|
||||||
|
_logoUrlCtrl.dispose();
|
||||||
|
_wolMacCtrl.dispose();
|
||||||
|
_wolIpCtrl.dispose();
|
||||||
|
_wolPwdCtrl.dispose();
|
||||||
|
_netDevCtrl.dispose();
|
||||||
|
_scriptDirCtrl.dispose();
|
||||||
|
|
||||||
|
_nameFocus.dispose();
|
||||||
|
_ipFocus.dispose();
|
||||||
|
_alterUrlFocus.dispose();
|
||||||
|
_portFocus.dispose();
|
||||||
|
_usernameFocus.dispose();
|
||||||
|
_pveAddrCtrl.dispose();
|
||||||
|
|
||||||
|
_keyIdx.dispose();
|
||||||
|
_autoConnect.dispose();
|
||||||
|
_jumpServer.dispose();
|
||||||
|
_pveIgnoreCert.dispose();
|
||||||
|
_env.dispose();
|
||||||
|
_customCmds.dispose();
|
||||||
|
_tags.dispose();
|
||||||
|
_systemType.dispose();
|
||||||
|
_disabledCmdTypes.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_focusScope = FocusScope.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final actions = <Widget>[];
|
||||||
|
if (spi != null) actions.add(_buildDelBtn());
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _focusScope.unfocus(),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
|
||||||
|
body: _buildForm(),
|
||||||
|
floatingActionButton: _buildFAB(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildForm() {
|
||||||
|
final topItems = [
|
||||||
|
_buildWriteScriptTip(),
|
||||||
|
if (isMobile) _buildQrScan(),
|
||||||
|
if (isDesktop) _buildSSHImport(),
|
||||||
|
];
|
||||||
|
final children = [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: topItems.joinWith(UIs.width13).toList(),
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
autoFocus: true,
|
||||||
|
controller: _nameController,
|
||||||
|
type: TextInputType.text,
|
||||||
|
node: _nameFocus,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
|
||||||
|
hint: libL10n.example,
|
||||||
|
label: libL10n.name,
|
||||||
|
icon: BoxIcons.bx_rename,
|
||||||
|
obscureText: false,
|
||||||
|
autoCorrect: true,
|
||||||
|
suggestion: true,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _ipController,
|
||||||
|
type: TextInputType.url,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
||||||
|
node: _ipFocus,
|
||||||
|
label: l10n.host,
|
||||||
|
icon: BoxIcons.bx_server,
|
||||||
|
hint: 'example.com',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _portController,
|
||||||
|
type: TextInputType.number,
|
||||||
|
node: _portFocus,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
|
||||||
|
label: l10n.port,
|
||||||
|
icon: Bootstrap.number_123,
|
||||||
|
hint: '22',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _usernameController,
|
||||||
|
type: TextInputType.text,
|
||||||
|
node: _usernameFocus,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),
|
||||||
|
label: libL10n.user,
|
||||||
|
icon: Icons.account_box,
|
||||||
|
hint: 'root',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
TagTile(
|
||||||
|
tags: _tags,
|
||||||
|
allTags: ref.watch(serversNotifierProvider).tags,
|
||||||
|
).cardx,
|
||||||
|
ListTile(
|
||||||
|
title: Text(l10n.autoConnect),
|
||||||
|
trailing: _autoConnect.listenVal(
|
||||||
|
(val) => Switch(
|
||||||
|
value: val,
|
||||||
|
onChanged: (val) {
|
||||||
|
_autoConnect.value = val;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildAuth(),
|
||||||
|
_buildSystemType(),
|
||||||
|
_buildJumpServer(),
|
||||||
|
_buildMore(),
|
||||||
|
];
|
||||||
|
return AutoMultiList(children: children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void afterFirstLayout(BuildContext context) {
|
||||||
|
if (spi != null) {
|
||||||
|
_initWithSpi(spi!);
|
||||||
|
} else {
|
||||||
|
// Only for new servers, check SSH config import on first time
|
||||||
|
_checkSSHConfigImport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
465
lib/view/page/server/edit/widget.dart
Normal file
465
lib/view/page/server/edit/widget.dart
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
part of 'edit.dart';
|
||||||
|
|
||||||
|
extension _Widgets on _ServerEditPageState {
|
||||||
|
Widget _buildAuth() {
|
||||||
|
final switch_ = ListTile(
|
||||||
|
title: Text(l10n.keyAuth),
|
||||||
|
trailing: _keyIdx.listenVal(
|
||||||
|
(v) => Switch(
|
||||||
|
value: v != null,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val) {
|
||||||
|
_keyIdx.value = -1;
|
||||||
|
} else {
|
||||||
|
_keyIdx.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Put [switch_] out of [ValueBuilder] to avoid rebuild
|
||||||
|
return _keyIdx.listenVal((v) {
|
||||||
|
final children = <Widget>[switch_];
|
||||||
|
if (v != null) {
|
||||||
|
children.add(_buildKeyAuth());
|
||||||
|
} else {
|
||||||
|
children.add(
|
||||||
|
Input(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: libL10n.pwd,
|
||||||
|
icon: Icons.password,
|
||||||
|
suggestion: false,
|
||||||
|
onSubmitted: (_) => _onSave(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(children: children);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildKeyAuth() {
|
||||||
|
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||||
|
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
||||||
|
final pkis = privateKeyState.keys;
|
||||||
|
|
||||||
|
final choice = _keyIdx.listenVal((val) {
|
||||||
|
final selectedPki = val != null && val >= 0 && val < pkis.length
|
||||||
|
? pkis[val]
|
||||||
|
: null;
|
||||||
|
return Choice<int>(
|
||||||
|
multiple: false,
|
||||||
|
clearable: true,
|
||||||
|
value: selectedPki != null ? [val!] : [],
|
||||||
|
builder: (state, _) => Column(
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
children: List<Widget>.generate(pkis.length, (index) {
|
||||||
|
final item = pkis[index];
|
||||||
|
return ChoiceChipX<int>(
|
||||||
|
label: item.id,
|
||||||
|
state: state,
|
||||||
|
value: index,
|
||||||
|
onSelected: (idx, on) {
|
||||||
|
if (on) {
|
||||||
|
_keyIdx.value = idx;
|
||||||
|
} else {
|
||||||
|
_keyIdx.value = -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
UIs.height7,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
if (selectedPki != null)
|
||||||
|
Btn.icon(
|
||||||
|
icon: const Icon(Icons.edit, size: 20),
|
||||||
|
text: libL10n.edit,
|
||||||
|
onTap: () => PrivateKeyEditPage.route.go(
|
||||||
|
context,
|
||||||
|
args: PrivateKeyEditPageArgs(pki: selectedPki),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Btn.icon(
|
||||||
|
icon: const Icon(Icons.add, size: 20),
|
||||||
|
text: libL10n.add,
|
||||||
|
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(Icons.key),
|
||||||
|
initiallyExpanded: _keyIdx.value != null && _keyIdx.value! >= 0,
|
||||||
|
childrenPadding: padding,
|
||||||
|
title: Text(l10n.privateKey),
|
||||||
|
children: [choice],
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEnvs() {
|
||||||
|
return _env.listenVal((val) {
|
||||||
|
final subtitle = val.isEmpty
|
||||||
|
? null
|
||||||
|
: Text(val.keys.join(','), style: UIs.textGrey);
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(HeroIcons.variable),
|
||||||
|
subtitle: subtitle,
|
||||||
|
title: Text(l10n.envVars),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: () async {
|
||||||
|
final res = await KvEditor.route.go(
|
||||||
|
context,
|
||||||
|
KvEditorArgs(data: spi?.envs ?? {}),
|
||||||
|
);
|
||||||
|
if (res == null) return;
|
||||||
|
_env.value = res;
|
||||||
|
},
|
||||||
|
).cardx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMore() {
|
||||||
|
return ExpandTile(
|
||||||
|
title: Text(l10n.more),
|
||||||
|
children: [
|
||||||
|
Input(
|
||||||
|
controller: _logoUrlCtrl,
|
||||||
|
type: TextInputType.url,
|
||||||
|
icon: Icons.image,
|
||||||
|
label: 'Logo URL',
|
||||||
|
hint: 'https://example.com/logo.png',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
_buildAltUrl(),
|
||||||
|
_buildScriptDir(),
|
||||||
|
_buildEnvs(),
|
||||||
|
_buildPVEs(),
|
||||||
|
_buildCustomCmds(),
|
||||||
|
_buildDisabledCmdTypes(),
|
||||||
|
_buildCustomDev(),
|
||||||
|
_buildWOLs(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScriptDir() {
|
||||||
|
return Input(
|
||||||
|
controller: _scriptDirCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: '${l10n.remotePath} (Shell ${l10n.install})',
|
||||||
|
icon: Icons.folder,
|
||||||
|
hint: '~/.config/server_box',
|
||||||
|
suggestion: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomDev() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CenterGreyTitle(l10n.specifyDev),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MingCute.question_line),
|
||||||
|
title: TipText(libL10n.note, l10n.specifyDevTip),
|
||||||
|
).cardx,
|
||||||
|
Input(
|
||||||
|
controller: _preferTempDevCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: l10n.temperature,
|
||||||
|
icon: MingCute.low_temperature_line,
|
||||||
|
hint: 'nvme-pci-0400',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _netDevCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: l10n.net,
|
||||||
|
icon: ZondIcons.network,
|
||||||
|
hint: 'eth0',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSystemType() {
|
||||||
|
return _systemType.listenVal((val) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(MingCute.laptop_2_line),
|
||||||
|
title: Text(l10n.system),
|
||||||
|
trailing: PopupMenu<SystemType?>(
|
||||||
|
initialValue: val,
|
||||||
|
items: [
|
||||||
|
PopupMenuItem(value: null, child: Text(libL10n.auto)),
|
||||||
|
PopupMenuItem(value: SystemType.linux, child: Text('Linux')),
|
||||||
|
PopupMenuItem(value: SystemType.bsd, child: Text('BSD')),
|
||||||
|
PopupMenuItem(value: SystemType.windows, child: Text('Windows')),
|
||||||
|
],
|
||||||
|
onSelected: (value) => _systemType.value = value,
|
||||||
|
child: Text(
|
||||||
|
val?.name ?? libL10n.auto,
|
||||||
|
style: TextStyle(color: val == null ? Colors.grey : null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).cardx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAltUrl() {
|
||||||
|
return Input(
|
||||||
|
controller: _altUrlController,
|
||||||
|
type: TextInputType.url,
|
||||||
|
node: _alterUrlFocus,
|
||||||
|
label: l10n.fallbackSshDest,
|
||||||
|
icon: MingCute.link_line,
|
||||||
|
hint: 'user@ip:port',
|
||||||
|
suggestion: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPVEs() {
|
||||||
|
const addr = 'https://127.0.0.1:8006';
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CenterGreyTitle('PVE'),
|
||||||
|
Input(
|
||||||
|
controller: _pveAddrCtrl,
|
||||||
|
type: TextInputType.url,
|
||||||
|
icon: MingCute.web_line,
|
||||||
|
label: 'URL',
|
||||||
|
hint: addr,
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MingCute.certificate_line),
|
||||||
|
title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),
|
||||||
|
trailing: _pveIgnoreCert.listenVal(
|
||||||
|
(v) => Switch(
|
||||||
|
value: v,
|
||||||
|
onChanged: (val) {
|
||||||
|
_pveIgnoreCert.value = val;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).cardx,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomCmds() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CenterGreyTitle(l10n.customCmd),
|
||||||
|
_customCmds.listenVal((vals) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(BoxIcons.bxs_file_json),
|
||||||
|
title: const Text('JSON'),
|
||||||
|
subtitle: vals.isEmpty
|
||||||
|
? null
|
||||||
|
: Text(vals.keys.join(','), style: UIs.textGrey),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: _onTapCustomItem,
|
||||||
|
);
|
||||||
|
}).cardx,
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MingCute.doc_line),
|
||||||
|
title: Text(libL10n.doc),
|
||||||
|
trailing: const Icon(Icons.open_in_new, size: 17),
|
||||||
|
onTap: l10n.customCmdDocUrl.launchUrl,
|
||||||
|
).cardx,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDisabledCmdTypes() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
|
||||||
|
_disabledCmdTypes.listenVal((disabled) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.disabled_by_default),
|
||||||
|
title: Text('${libL10n.disabled} ${l10n.cmd}'),
|
||||||
|
subtitle: disabled.isEmpty
|
||||||
|
? null
|
||||||
|
: Text(disabled.join(', '), style: UIs.textGrey),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: _onTapDisabledCmdTypes,
|
||||||
|
);
|
||||||
|
}).cardx,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWOLs() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CenterGreyTitle('Wake On LAN (beta)'),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(BoxIcons.bxs_help_circle),
|
||||||
|
title: TipText(libL10n.about, l10n.wolTip),
|
||||||
|
).cardx,
|
||||||
|
Input(
|
||||||
|
controller: _wolMacCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: 'MAC ${l10n.addr}',
|
||||||
|
icon: Icons.computer,
|
||||||
|
hint: '00:11:22:33:44:55',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _wolIpCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: 'IP ${l10n.addr}',
|
||||||
|
icon: ZondIcons.network,
|
||||||
|
hint: '192.168.1.x',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _wolPwdCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
obscureText: true,
|
||||||
|
label: libL10n.pwd,
|
||||||
|
icon: Icons.password,
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFAB() {
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: _onSave,
|
||||||
|
child: const Icon(Icons.save),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildJumpServer() {
|
||||||
|
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
||||||
|
final srvs = ref
|
||||||
|
.watch(serversNotifierProvider)
|
||||||
|
.servers
|
||||||
|
.values
|
||||||
|
.where((e) => e.jumpId == null)
|
||||||
|
.where((e) => e.id != spi?.id)
|
||||||
|
.toList();
|
||||||
|
final choice = _jumpServer.listenVal((val) {
|
||||||
|
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
||||||
|
return Choice<Spi>(
|
||||||
|
multiple: false,
|
||||||
|
clearable: true,
|
||||||
|
value: srv != null ? [srv] : [],
|
||||||
|
builder: (state, _) => Wrap(
|
||||||
|
children: List<Widget>.generate(srvs.length, (index) {
|
||||||
|
final item = srvs[index];
|
||||||
|
return ChoiceChipX<Spi>(
|
||||||
|
label: item.name,
|
||||||
|
state: state,
|
||||||
|
value: item,
|
||||||
|
onSelected: (srv, on) {
|
||||||
|
if (on) {
|
||||||
|
_jumpServer.value = srv.id;
|
||||||
|
} else {
|
||||||
|
_jumpServer.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(Icons.map),
|
||||||
|
initiallyExpanded: _jumpServer.value != null,
|
||||||
|
childrenPadding: padding,
|
||||||
|
title: Text(l10n.jumpServer),
|
||||||
|
children: [choice],
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWriteScriptTip() {
|
||||||
|
return Btn.tile(
|
||||||
|
text: libL10n.attention,
|
||||||
|
icon: const Icon(Icons.tips_and_updates, color: Colors.grey),
|
||||||
|
onTap: () {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: SimpleMarkdown(data: l10n.writeScriptTip),
|
||||||
|
actions: Btnx.oks,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
textStyle: UIs.textGrey,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQrScan() {
|
||||||
|
return Btn.tile(
|
||||||
|
text: libL10n.import,
|
||||||
|
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
||||||
|
onTap: () async {
|
||||||
|
final ret = await BarcodeScannerPage.route.go(
|
||||||
|
context,
|
||||||
|
args: const BarcodeScannerPageArgs(),
|
||||||
|
);
|
||||||
|
final code = ret?.text;
|
||||||
|
if (code == null) return;
|
||||||
|
try {
|
||||||
|
final spi = Spi.fromJson(json.decode(code));
|
||||||
|
_initWithSpi(spi);
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showErrDialog(e, s);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textStyle: UIs.textGrey,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSSHImport() {
|
||||||
|
return Btn.tile(
|
||||||
|
text: l10n.sshConfigImport,
|
||||||
|
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||||
|
onTap: _onTapSSHImport,
|
||||||
|
textStyle: UIs.textGrey,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDelBtn() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: Text(
|
||||||
|
libL10n.askContinue(
|
||||||
|
'${libL10n.delete} ${l10n.server}(${spi!.name})',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: Btn.ok(
|
||||||
|
onTap: () async {
|
||||||
|
context.pop();
|
||||||
|
ref.read(serversNotifierProvider.notifier).delServer(spi!.id);
|
||||||
|
context.pop(true);
|
||||||
|
},
|
||||||
|
red: true,
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import 'package:server_box/data/provider/server/single.dart';
|
|||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/server/detail/view.dart';
|
import 'package:server_box/view/page/server/detail/view.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||||
import 'package:server_box/view/page/setting/entry.dart';
|
import 'package:server_box/view/page/setting/entry.dart';
|
||||||
import 'package:server_box/view/widget/percent_circle.dart';
|
import 'package:server_box/view/widget/percent_circle.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ extension _App on _AppSettingsPageState {
|
|||||||
_buildThemeMode(),
|
_buildThemeMode(),
|
||||||
_buildAppColor(),
|
_buildAppColor(),
|
||||||
_buildCheckUpdate(),
|
_buildCheckUpdate(),
|
||||||
|
_buildHomeTabs(),
|
||||||
PlatformPublicSettings.buildBioAuth,
|
PlatformPublicSettings.buildBioAuth,
|
||||||
if (specific != null) specific,
|
if (specific != null) specific,
|
||||||
_buildAppMore(),
|
_buildAppMore(),
|
||||||
@@ -274,4 +275,15 @@ extension _App on _AppSettingsPageState {
|
|||||||
trailing: StoreSwitch(prop: _setting.hideTitleBar),
|
trailing: StoreSwitch(prop: _setting.hideTitleBar),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildHomeTabs() {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.tab),
|
||||||
|
title: Text(l10n.homeTabs),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: () {
|
||||||
|
HomeTabsConfigPage.route.go(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
lib/view/page/setting/entries/home_tabs.dart
Normal file
134
lib/view/page/setting/entries/home_tabs.dart
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/data/model/app/tab.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
|
class HomeTabsConfigPage extends ConsumerStatefulWidget {
|
||||||
|
const HomeTabsConfigPage({super.key});
|
||||||
|
|
||||||
|
static final route = AppRouteNoArg(page: HomeTabsConfigPage.new, path: '/settings/home-tabs');
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<HomeTabsConfigPage> createState() => _HomeTabsConfigPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeTabsConfigPageState extends ConsumerState<HomeTabsConfigPage> {
|
||||||
|
final _availableTabs = AppTab.values;
|
||||||
|
var _selectedTabs = List<AppTab>.from(Stores.setting.homeTabs.fetch());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: CustomAppBar(
|
||||||
|
title: Text(l10n.homeTabs),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: _resetToDefault, child: Text(libL10n.reset)),
|
||||||
|
TextButton(onPressed: _saveAndExit, child: Text(libL10n.save)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(l10n.homeTabsCustomizeDesc, style: context.theme.textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ReorderableListView.builder(
|
||||||
|
itemCount: _selectedTabs.length,
|
||||||
|
onReorder: _onReorder,
|
||||||
|
buildDefaultDragHandles: false,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final tab = _selectedTabs[index];
|
||||||
|
return _buildTabItem(tab, index, true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(l10n.availableTabs, style: context.theme.textTheme.titleMedium),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _availableTabs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final tab = _availableTabs[index];
|
||||||
|
if (_selectedTabs.contains(tab)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return _buildTabItem(tab, index, false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTabItem(AppTab tab, int index, bool isSelected) {
|
||||||
|
final canRemove = _selectedTabs.length > 1 && tab != AppTab.server;
|
||||||
|
final child = ListTile(
|
||||||
|
leading: tab.navDestination.icon,
|
||||||
|
title: Text(tab.navDestination.label),
|
||||||
|
trailing: isSelected
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: canRemove ? () => _removeTab(tab) : null,
|
||||||
|
color: canRemove ? null : Theme.of(context).disabledColor,
|
||||||
|
tooltip: canRemove ? libL10n.delete : (tab == AppTab.server ? l10n.serverTabRequired : l10n.atLeastOneTab),
|
||||||
|
)
|
||||||
|
: IconButton(icon: const Icon(Icons.add), onPressed: () => _addTab(tab)),
|
||||||
|
onTap: isSelected && canRemove ? () => _removeTab(tab) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
key: ValueKey(tab.name),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: isSelected ? ReorderableDragStartListener(index: index, child: child) : child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReorder(int oldIndex, int newIndex) {
|
||||||
|
setState(() {
|
||||||
|
if (newIndex > oldIndex) {
|
||||||
|
newIndex -= 1;
|
||||||
|
}
|
||||||
|
final tab = _selectedTabs.removeAt(oldIndex);
|
||||||
|
_selectedTabs.insert(newIndex, tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addTab(AppTab tab) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTabs.add(tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeTab(AppTab tab) {
|
||||||
|
if (_selectedTabs.length <= 1) {
|
||||||
|
context.showSnackBar(l10n.atLeastOneTab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tab == AppTab.server) {
|
||||||
|
context.showSnackBar(l10n.serverTabRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_selectedTabs.remove(tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveAndExit() {
|
||||||
|
Stores.setting.homeTabs.put(_selectedTabs);
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetToDefault() {
|
||||||
|
setState(() {
|
||||||
|
_selectedTabs = List<AppTab>.from(AppTab.values);
|
||||||
|
});
|
||||||
|
Stores.setting.homeTabs.put(_selectedTabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ extension _Server on _AppSettingsPageState {
|
|||||||
_buildNetViewType(),
|
_buildNetViewType(),
|
||||||
_buildServerSeq(),
|
_buildServerSeq(),
|
||||||
_buildServerDetailCardSeq(),
|
_buildServerDetailCardSeq(),
|
||||||
|
_buildConnectionStats(),
|
||||||
_buildDeleteServers(),
|
_buildDeleteServers(),
|
||||||
_buildCpuView(),
|
_buildCpuView(),
|
||||||
_buildServerMore(),
|
_buildServerMore(),
|
||||||
@@ -38,6 +39,22 @@ extension _Server on _AppSettingsPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildConnectionStats() {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.analytics, size: _kIconSize),
|
||||||
|
title: Text(l10n.connectionStats),
|
||||||
|
subtitle: Text(l10n.connectionStatsDesc),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const ConnectionStatsPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDeleteServers() {
|
Widget _buildDeleteServers() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(l10n.deleteServers),
|
title: Text(l10n.deleteServers),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import 'package:server_box/data/store/setting.dart';
|
|||||||
import 'package:server_box/generated/l10n/l10n.dart';
|
import 'package:server_box/generated/l10n/l10n.dart';
|
||||||
import 'package:server_box/view/page/backup.dart';
|
import 'package:server_box/view/page/backup.dart';
|
||||||
import 'package:server_box/view/page/private_key/list.dart';
|
import 'package:server_box/view/page/private_key/list.dart';
|
||||||
|
import 'package:server_box/view/page/server/connection_stats.dart';
|
||||||
|
import 'package:server_box/view/page/setting/entries/home_tabs.dart';
|
||||||
import 'package:server_box/view/page/setting/platform/android.dart';
|
import 'package:server_box/view/page/setting/platform/android.dart';
|
||||||
import 'package:server_box/view/page/setting/platform/ios.dart';
|
import 'package:server_box/view/page/setting/platform/ios.dart';
|
||||||
import 'package:server_box/view/page/setting/platform/platform_pub.dart';
|
import 'package:server_box/view/page/setting/platform/platform_pub.dart';
|
||||||
|
|||||||
@@ -108,14 +108,13 @@ class _IosSettingsPageState extends State<IosSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTapWatchApp(Map<String, dynamic> map) async {
|
void _onTapWatchApp(Map<String, dynamic> map) async {
|
||||||
final urls = Map<String, String>.from(map['urls'] as Map? ?? {});
|
final cfgs = List<String>.from(map['urls'] as List? ?? []);
|
||||||
final result = await KvEditor.route.go(context, KvEditorArgs(data: urls));
|
final result = await JsonListEditor.route.go(context, JsonListEditorArgs(data: cfgs));
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
final (_, err) = await context.showLoadingDialog(
|
final (_, err) = await context.showLoadingDialog(
|
||||||
fn: () async {
|
fn: () async {
|
||||||
final data = {'urls': result};
|
final data = {'urls': result};
|
||||||
|
|
||||||
// Try realtime update (app must be running foreground).
|
// Try realtime update (app must be running foreground).
|
||||||
try {
|
try {
|
||||||
if (await wc.isReachable) {
|
if (await wc.isReachable) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:icons_plus/icons_plus.dart';
|
|||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/provider/server/all.dart';
|
import 'package:server_box/data/provider/server/all.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||||
|
|
||||||
class SSHTabPage extends ConsumerStatefulWidget {
|
class SSHTabPage extends ConsumerStatefulWidget {
|
||||||
|
|||||||
@@ -471,7 +471,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||||
@@ -481,7 +481,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "Server Box";
|
PRODUCT_NAME = "Server Box";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
INFOPLIST_KEY_CFBundleDisplayName = "Server Box";
|
||||||
@@ -618,7 +618,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "Server Box";
|
PRODUCT_NAME = "Server Box";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1231;
|
CURRENT_PROJECT_VERSION = 1253;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -649,11 +649,11 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MARKETING_VERSION = 1.0.1231;
|
MARKETING_VERSION = 1.0.1253;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "Server Box";
|
PRODUCT_NAME = "Server Box";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = serverbox_lkmm;
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = hj_macmini_for_serverbox;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|||||||
@@ -18,18 +18,12 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
|
|
||||||
<array>
|
|
||||||
<string>.ssh/</string>
|
|
||||||
</array>
|
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array/>
|
<array/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -16,18 +16,12 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
|
|
||||||
<array>
|
|
||||||
<string>.ssh/</string>
|
|
||||||
</array>
|
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array/>
|
<array/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
84
pubspec.lock
84
pubspec.lock
@@ -189,18 +189,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_android_camerax
|
name: camera_android_camerax
|
||||||
sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8"
|
sha256: "2d438248554f44766bf9ea34c117a5bb0074e241342ef7c22c768fb431335234"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.19+1"
|
version: "0.6.21"
|
||||||
camera_avfoundation:
|
camera_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_avfoundation
|
name: camera_avfoundation
|
||||||
sha256: e4aca5bccaf897b70cac87e5fdd789393310985202442837922fd40325e2733b
|
sha256: "951ef122d01ebba68b7a54bfe294e8b25585635a90465c311b2f875ae72c412f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.21+1"
|
version: "0.9.21+2"
|
||||||
camera_platform_interface:
|
camera_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -464,10 +464,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2
|
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.3.1"
|
version: "10.3.2"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -480,8 +480,8 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.51"
|
ref: "v1.0.52"
|
||||||
resolved-ref: "430672b7c7608b68ceda785533ec11e1ac3e9ca7"
|
resolved-ref: "38e7d41ccd71bf44e286d86b4ad656f05c5c2548"
|
||||||
url: "https://github.com/lppcg/fl_build.git"
|
url: "https://github.com/lppcg/fl_build.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
@@ -489,16 +489,16 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fl_chart
|
name: fl_chart
|
||||||
sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7"
|
sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
fl_lib:
|
fl_lib:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.345"
|
ref: "v1.0.346"
|
||||||
resolved-ref: "1b797643ef7603dd825caf96a6c57b88dbd23c34"
|
resolved-ref: f277b7a4259e45889320ef6d80ab320662558784
|
||||||
url: "https://github.com/lppcg/fl_lib"
|
url: "https://github.com/lppcg/fl_lib"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -580,10 +580,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
|
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.29"
|
version: "2.0.30"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -644,10 +644,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: de82e6bf958cec7190fbc1c5298282c851228e35ae2b14e2b103e7f777818c64
|
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.13"
|
version: "2.2.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -682,6 +682,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
get_it:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: get_it
|
||||||
|
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.2.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -727,10 +735,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hive_ce_flutter
|
name: hive_ce_flutter
|
||||||
sha256: a0989670652eab097b47544f1e5a4456e861b1b01b050098ea0b80a5fabe9909
|
sha256: f5bd57fda84402bca7557fedb8c629c96c8ea10fab4a542968d7b60864ca02cc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.2"
|
||||||
hive_ce_generator:
|
hive_ce_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -911,10 +919,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: "316503f6772dea9c0c038bb7aac4f68ab00112d707d258c770f7fc3c250a2d88"
|
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.51"
|
version: "1.0.52"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1071,10 +1079,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.17"
|
version: "2.2.18"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1184,10 +1192,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: provider
|
name: provider
|
||||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5"
|
version: "6.1.5+1"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1224,10 +1232,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: qr_code_dart_scan
|
name: qr_code_dart_scan
|
||||||
sha256: b23879821242bc2c1b2d3a035d96d453a5554c86894a69e10726b559206bcafb
|
sha256: "1b317b47f475f6995c19e0f41d790902a8cd158b23c435d936763d86ba44309c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.2"
|
version: "0.11.3"
|
||||||
quiver:
|
quiver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1368,10 +1376,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
|
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.11"
|
version: "2.4.12"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1613,10 +1621,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
|
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.17"
|
version: "6.3.18"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1685,18 +1693,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_codec
|
name: vector_graphics_codec
|
||||||
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.11+1"
|
version: "1.1.13"
|
||||||
vector_graphics_compiler:
|
vector_graphics_compiler:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_compiler
|
name: vector_graphics_compiler
|
||||||
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.11+1"
|
version: "1.1.19"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1750,10 +1758,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1830,8 +1838,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v4.0.3"
|
ref: "v4.0.4"
|
||||||
resolved-ref: c64183346b924173eb7251800001a64771911185
|
resolved-ref: "5747837cdb7b113ef733ce0104e4f2bfa1eb4a36"
|
||||||
url: "https://github.com/lollipopkit/xterm.dart"
|
url: "https://github.com/lollipopkit/xterm.dart"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
|||||||
11
pubspec.yaml
11
pubspec.yaml
@@ -1,7 +1,7 @@
|
|||||||
name: server_box
|
name: server_box
|
||||||
description: server status & toolbox app.
|
description: server status & toolbox app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.0.1231+1231
|
version: 1.0.1253+1253
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.9.0"
|
sdk: ">=3.9.0"
|
||||||
@@ -47,7 +47,7 @@ dependencies:
|
|||||||
xterm:
|
xterm:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/xterm.dart
|
url: https://github.com/lollipopkit/xterm.dart
|
||||||
ref: v4.0.3
|
ref: v4.0.4
|
||||||
computer:
|
computer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/dart_computer
|
url: https://github.com/lollipopkit/dart_computer
|
||||||
@@ -63,8 +63,9 @@ dependencies:
|
|||||||
fl_lib:
|
fl_lib:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_lib
|
url: https://github.com/lppcg/fl_lib
|
||||||
ref: v1.0.345
|
ref: v1.0.346
|
||||||
flutter_gbk2utf8: ^1.0.1
|
flutter_gbk2utf8: ^1.0.1
|
||||||
|
get_it: ^8.2.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# webdav_client_plus:
|
# webdav_client_plus:
|
||||||
@@ -77,7 +78,7 @@ dependency_overrides:
|
|||||||
# path: ../fl_lib
|
# path: ../fl_lib
|
||||||
# fl_build:
|
# fl_build:
|
||||||
# path: ../fl_build
|
# path: ../fl_build
|
||||||
gtk:
|
gtk: # TODO: remove it after fixed in upstream
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lollipopkit/gtk.dart
|
url: https://github.com/lollipopkit/gtk.dart
|
||||||
ref: v0.0.36
|
ref: v0.0.36
|
||||||
@@ -101,7 +102,7 @@ dev_dependencies:
|
|||||||
fl_build:
|
fl_build:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_build.git
|
url: https://github.com/lppcg/fl_build.git
|
||||||
ref: v1.0.51
|
ref: v1.0.52
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
generate: true
|
generate: true
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:server_box/data/model/container/ps.dart';
|
import 'package:server_box/data/model/container/ps.dart';
|
||||||
|
import 'package:server_box/data/model/container/status.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('docker ps parse', () {
|
test('docker ps parse', () {
|
||||||
@@ -26,7 +27,92 @@ fa1215b4be74 Up 12 hours firefly
|
|||||||
expect(ps.names, names[idx - 1]);
|
expect(ps.names, names[idx - 1]);
|
||||||
expect(ps.image, images[idx - 1]);
|
expect(ps.image, images[idx - 1]);
|
||||||
expect(ps.state, states[idx - 1]);
|
expect(ps.state, states[idx - 1]);
|
||||||
expect(ps.running, true);
|
expect(ps.status, ContainerStatus.running);
|
||||||
|
expect(ps.status.isRunning, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('docker ps status detection', () {
|
||||||
|
// Test various Docker container states
|
||||||
|
final testCases = [
|
||||||
|
// Running states
|
||||||
|
{'state': 'Up 2 minutes', 'status': ContainerStatus.running},
|
||||||
|
{'state': 'Up 1 hour', 'status': ContainerStatus.running},
|
||||||
|
{'state': 'UP 30 seconds', 'status': ContainerStatus.running}, // Case insensitive
|
||||||
|
{'state': 'up 5 days', 'status': ContainerStatus.running}, // Case insensitive
|
||||||
|
|
||||||
|
// Non-running states
|
||||||
|
{'state': 'Exited (0) 5 minutes ago', 'status': ContainerStatus.exited},
|
||||||
|
{'state': 'Created', 'status': ContainerStatus.created},
|
||||||
|
{'state': 'Paused', 'status': ContainerStatus.paused},
|
||||||
|
{'state': 'Restarting', 'status': ContainerStatus.restarting},
|
||||||
|
{'state': 'Removing', 'status': ContainerStatus.removing},
|
||||||
|
{'state': 'Dead', 'status': ContainerStatus.dead},
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
{'state': null, 'status': ContainerStatus.unknown},
|
||||||
|
{'state': '', 'status': ContainerStatus.unknown},
|
||||||
|
{'state': 'Some Unknown Status', 'status': ContainerStatus.unknown},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final testCase in testCases) {
|
||||||
|
final ps = DockerPs(id: 'test', state: testCase['state'] as String?);
|
||||||
|
final expectedStatus = testCase['status'] as ContainerStatus;
|
||||||
|
expect(
|
||||||
|
ps.status,
|
||||||
|
expectedStatus,
|
||||||
|
reason: 'State "${testCase['state']}" should be ${expectedStatus.name}'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test status.isRunning method
|
||||||
|
expect(
|
||||||
|
ps.status.isRunning,
|
||||||
|
expectedStatus.isRunning,
|
||||||
|
reason: 'State "${testCase['state']}" isRunning should match status.isRunning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('podman ps status detection', () {
|
||||||
|
final testCases = [
|
||||||
|
{'exited': false, 'status': ContainerStatus.running},
|
||||||
|
{'exited': true, 'status': ContainerStatus.exited},
|
||||||
|
{'exited': null, 'status': ContainerStatus.unknown},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final testCase in testCases) {
|
||||||
|
final ps = PodmanPs(id: 'test', exited: testCase['exited'] as bool?);
|
||||||
|
final expectedStatus = testCase['status'] as ContainerStatus;
|
||||||
|
expect(
|
||||||
|
ps.status,
|
||||||
|
expectedStatus,
|
||||||
|
reason: 'Exited "${testCase['exited']}" should be ${expectedStatus.name}'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test status.isRunning method
|
||||||
|
expect(
|
||||||
|
ps.status.isRunning,
|
||||||
|
expectedStatus.isRunning,
|
||||||
|
reason: 'Exited "${testCase['exited']}" isRunning should match status.isRunning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('container status utility methods', () {
|
||||||
|
expect(ContainerStatus.running.isRunning, true);
|
||||||
|
expect(ContainerStatus.exited.isRunning, false);
|
||||||
|
expect(ContainerStatus.created.isRunning, false);
|
||||||
|
|
||||||
|
expect(ContainerStatus.exited.canStart, true);
|
||||||
|
expect(ContainerStatus.created.canStart, true);
|
||||||
|
expect(ContainerStatus.running.canStart, false);
|
||||||
|
|
||||||
|
expect(ContainerStatus.running.canStop, true);
|
||||||
|
expect(ContainerStatus.paused.canStop, true);
|
||||||
|
expect(ContainerStatus.exited.canStop, false);
|
||||||
|
|
||||||
|
expect(ContainerStatus.running.canRestart, true);
|
||||||
|
expect(ContainerStatus.removing.canRestart, false);
|
||||||
|
expect(ContainerStatus.unknown.canRestart, false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,138 @@ void main() {
|
|||||||
expect(usage.usedPercent, 50);
|
expect(usage.usedPercent, 50);
|
||||||
// This would use the "unknown" fallback for kname
|
// This would use the "unknown" fallback for kname
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parse df -k output (fallback mode)', () {
|
||||||
|
final disks = Disk.parse(_dfOutput);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
expect(disks.length, 3); // Should find 3 valid filesystems: udev, /dev/vda3, /dev/vda2
|
||||||
|
|
||||||
|
// Verify root filesystem
|
||||||
|
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(rootFs.path, '/dev/vda3');
|
||||||
|
expect(rootFs.usedPercent, 47);
|
||||||
|
expect(rootFs.size, BigInt.from(40910528 ~/ 1024)); // df -k output divided by 1024 = MB
|
||||||
|
expect(rootFs.used, BigInt.from(18067948 ~/ 1024));
|
||||||
|
expect(rootFs.avail, BigInt.from(20951380 ~/ 1024));
|
||||||
|
|
||||||
|
// Verify boot/efi filesystem
|
||||||
|
final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');
|
||||||
|
expect(efiFs.path, '/dev/vda2');
|
||||||
|
expect(efiFs.usedPercent, 7);
|
||||||
|
expect(efiFs.size, BigInt.from(192559 ~/ 1024));
|
||||||
|
|
||||||
|
// Verify udev filesystem is included (virtual filesystem)
|
||||||
|
final udevFs = disks.firstWhere((disk) => disk.path == 'udev');
|
||||||
|
expect(udevFs.mount, '/dev');
|
||||||
|
expect(udevFs.usedPercent, 0);
|
||||||
|
expect(udevFs.size, BigInt.from(864088 ~/ 1024));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle empty input gracefully', () {
|
||||||
|
final disks = Disk.parse('');
|
||||||
|
expect(disks, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle whitespace-only input', () {
|
||||||
|
final disks = Disk.parse(' \n\t \r\n ');
|
||||||
|
expect(disks, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle JSON with null filesystem fields', () {
|
||||||
|
final disks = Disk.parse(_jsonWithNullFields);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle null filesystem fields gracefully
|
||||||
|
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(disk.size, BigInt.zero);
|
||||||
|
expect(disk.used, BigInt.zero);
|
||||||
|
expect(disk.avail, BigInt.zero);
|
||||||
|
expect(disk.usedPercent, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle JSON with string "null" values', () {
|
||||||
|
final disks = Disk.parse(_jsonWithStringNulls);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle string "null" filesystem fields gracefully
|
||||||
|
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(disk.size, BigInt.zero);
|
||||||
|
expect(disk.used, BigInt.zero);
|
||||||
|
expect(disk.avail, BigInt.zero);
|
||||||
|
expect(disk.usedPercent, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle JSON with empty string values', () {
|
||||||
|
final disks = Disk.parse(_jsonWithEmptyStrings);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle empty string filesystem fields gracefully
|
||||||
|
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(disk.size, BigInt.zero);
|
||||||
|
expect(disk.used, BigInt.zero);
|
||||||
|
expect(disk.avail, BigInt.zero);
|
||||||
|
expect(disk.usedPercent, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle JSON with invalid percentage format', () {
|
||||||
|
final disks = Disk.parse(_jsonWithInvalidPercent);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle invalid percentage gracefully
|
||||||
|
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(disk.usedPercent, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle JSON with malformed numbers', () {
|
||||||
|
final disks = Disk.parse(_jsonWithMalformedNumbers);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle malformed numbers gracefully
|
||||||
|
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(disk.size, BigInt.zero);
|
||||||
|
expect(disk.used, BigInt.zero);
|
||||||
|
expect(disk.avail, BigInt.zero);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle JSON parsing errors gracefully', () {
|
||||||
|
final disks = Disk.parse(_malformedJson);
|
||||||
|
expect(disks, isEmpty); // Should fallback to legacy method, which also fails
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle df output with missing fields', () {
|
||||||
|
final disks = Disk.parse(_dfWithMissingFields);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle missing fields gracefully
|
||||||
|
final disk = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(disk.usedPercent, 47);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle df output with inconsistent formatting', () {
|
||||||
|
final disks = Disk.parse(_dfWithInconsistentFormatting);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should handle inconsistent formatting
|
||||||
|
expect(disks.length, greaterThan(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle lsblk with success marker', () {
|
||||||
|
final disks = Disk.parse(_lsblkWithSuccessMarker);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should parse JSON and ignore success marker
|
||||||
|
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
|
||||||
|
expect(rootFs.fsTyp, 'ext4');
|
||||||
|
expect(rootFs.usedPercent, 56);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle malformed lsblk output fallback', () {
|
||||||
|
final disks = Disk.parse(_malformedLsblkWithDfFallback);
|
||||||
|
expect(disks, isNotEmpty);
|
||||||
|
|
||||||
|
// Should fallback to df -k parsing when lsblk output is malformed
|
||||||
|
expect(disks.length, 3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,3 +410,151 @@ overlay 1907116416 5470
|
|||||||
v2000pro/pve 1906694784 125440 1906569344 1% /mnt/v2000pro/pve
|
v2000pro/pve 1906694784 125440 1906569344 1% /mnt/v2000pro/pve
|
||||||
v2000pro/download 1906569472 128 1906569344 1% /mnt/v2000pro/download''',
|
v2000pro/download 1906569472 128 1906569344 1% /mnt/v2000pro/download''',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const _dfOutput = '''
|
||||||
|
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||||
|
udev 864088 0 864088 0% /dev
|
||||||
|
tmpfs 176724 688 176036 1% /run
|
||||||
|
/dev/vda3 40910528 18067948 20951380 47% /
|
||||||
|
tmpfs 883612 0 883612 0% /dev/shm
|
||||||
|
tmpfs 5120 0 5120 0% /run/lock
|
||||||
|
/dev/vda2 192559 11807 180752 7% /boot/efi
|
||||||
|
tmpfs 176720 104 176616 1% /run/user/1000
|
||||||
|
''';
|
||||||
|
|
||||||
|
// Test data for edge cases
|
||||||
|
const _jsonWithNullFields = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": null,
|
||||||
|
"fsused": null,
|
||||||
|
"fsavail": null,
|
||||||
|
"fsuse%": null,
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _jsonWithStringNulls = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": "null",
|
||||||
|
"fsused": "null",
|
||||||
|
"fsavail": "null",
|
||||||
|
"fsuse%": "null",
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _jsonWithEmptyStrings = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": "",
|
||||||
|
"fsused": "",
|
||||||
|
"fsavail": "",
|
||||||
|
"fsuse%": "",
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _jsonWithInvalidPercent = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": "1000000",
|
||||||
|
"fsused": "500000",
|
||||||
|
"fsavail": "500000",
|
||||||
|
"fsuse%": "invalid_percent",
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _jsonWithMalformedNumbers = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": "not_a_number",
|
||||||
|
"fsused": "invalid",
|
||||||
|
"fsavail": "broken",
|
||||||
|
"fsuse%": "50%",
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _malformedJson = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": "1000000",
|
||||||
|
"fsused": "500000",
|
||||||
|
"fsavail": "500000",
|
||||||
|
"fsuse%": "50%",
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// Missing closing brace and malformed structure
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _dfWithMissingFields = '''
|
||||||
|
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||||
|
/dev/vda3 40910528 18067948 20951380 47% /
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _dfWithInconsistentFormatting = '''
|
||||||
|
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||||
|
/dev/sda1 1000000 500000 500000 50% /
|
||||||
|
/dev/sda2 2000000 1000000 1000000 50% /home
|
||||||
|
udev 864088 0 864088 0% /dev
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _lsblkWithSuccessMarker = '''
|
||||||
|
{
|
||||||
|
"blockdevices": [
|
||||||
|
{
|
||||||
|
"fstype": "ext4",
|
||||||
|
"mountpoint": "/",
|
||||||
|
"fssize": 982141468672,
|
||||||
|
"fsused": 552718364672,
|
||||||
|
"fsavail": 379457622016,
|
||||||
|
"fsuse%": "56%",
|
||||||
|
"path": "/dev/sda1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
LSBLK_SUCCESS
|
||||||
|
''';
|
||||||
|
|
||||||
|
const _malformedLsblkWithDfFallback = '''
|
||||||
|
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||||
|
udev 864088 0 864088 0% /dev
|
||||||
|
tmpfs 176724 688 176036 1% /run
|
||||||
|
/dev/vda3 40910528 18067948 20951380 47% /
|
||||||
|
tmpfs 883612 0 883612 0% /dev/shm
|
||||||
|
tmpfs 5120 0 5120 0% /run/lock
|
||||||
|
/dev/vda2 192559 11807 180752 7% /boot/efi
|
||||||
|
tmpfs 176720 104 176616 1% /run/user/1000
|
||||||
|
''';
|
||||||
|
|||||||
Reference in New Issue
Block a user