Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b56e033773 | ||
|
|
7dda63af8a | ||
|
|
00d303ac36 | ||
|
|
229983d82e | ||
|
|
4928ca600d | ||
|
|
89ec2d94d6 | ||
|
|
393c3e6388 | ||
|
|
dee458e926 | ||
|
|
f89228db40 | ||
|
|
3b6fb6194b | ||
|
|
02444fc2f0 | ||
|
|
aef317a140 | ||
|
|
47aedb2f2e | ||
|
|
eab06abcaf | ||
|
|
c062c12a0e | ||
|
|
d7669c94b8 | ||
|
|
50af289574 | ||
|
|
90b88ed795 | ||
|
|
d611fdcd50 | ||
|
|
db9b2dd818 | ||
|
|
edb49ead67 | ||
|
|
7f0dc656b8 | ||
|
|
b33d0bbc3e | ||
|
|
7d0ea8a58b | ||
|
|
c18732d8f3 | ||
|
|
157af0a354 | ||
|
|
2d9dc044f9 | ||
|
|
479250c207 | ||
|
|
aef7ec911f | ||
|
|
4f9ee7781f | ||
|
|
eb83d05c81 | ||
|
|
329fd33b69 | ||
|
|
931c5f0bf6 | ||
|
|
bcbf1fbc17 | ||
|
|
3e7315dac6 | ||
|
|
4cecfdf7a8 | ||
|
|
0346821cf5 |
2
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version: '3.24.0'
|
flutter-version: '3.24.1'
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Please only download pkgs from the source that **you trust**!
|
|||||||
## 🔖 Feature
|
## 🔖 Feature
|
||||||
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
|
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
|
||||||
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
|
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
|
||||||
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft); Español, Русский язык, Português, 日本語 (Generated by GPT)
|
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
|
||||||
|
|
||||||
|
|
||||||
## 🆘 Help
|
## 🆘 Help
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
|||||||
- 本地化
|
- 本地化
|
||||||
- English, 简体中文
|
- English, 简体中文
|
||||||
- Español, Русский язык, Português, 日本語 (Generated by GPT)
|
- Español, Русский язык, Português, 日本語 (Generated by GPT)
|
||||||
- Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft);
|
- Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);
|
||||||
- 感谢贡献者们!
|
- 感谢贡献者们!
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "tech.lolli.toolbox"
|
applicationId "tech.lolli.toolbox"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
@@ -15,7 +16,8 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:restoreAnyVersion="true">
|
android:restoreAnyVersion="true"
|
||||||
|
tools:targetApi="q">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -29,12 +31,12 @@
|
|||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
@@ -43,11 +45,6 @@
|
|||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="id.flutter.flutter_background_service.BackgroundService"
|
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.HomeWidget"
|
android:name=".widget.HomeWidget"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -67,7 +64,12 @@
|
|||||||
android:resource="@xml/home_widget" />
|
android:resource="@xml/home_widget" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".KeepAliveService"/>
|
<service
|
||||||
|
android:name=".ForegroundService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility?hl=en and
|
https://developer.android.com/training/package-visibility?hl=en and
|
||||||
@@ -76,8 +78,8 @@
|
|||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package tech.lolli.toolbox
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
class ForegroundService : Service() {
|
||||||
|
private val chanId = "ForegroundServiceChannel"
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
createNotificationChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
when (intent?.action) {
|
||||||
|
"ACTION_STOP_FOREGROUND" -> {
|
||||||
|
stopForegroundService()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val notification = createNotification()
|
||||||
|
startForeground(1, notification)
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val serviceChannel = NotificationChannel(
|
||||||
|
chanId,
|
||||||
|
chanId,
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(serviceChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
notificationIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
val deleteIntent = Intent(this, ForegroundService::class.java).apply {
|
||||||
|
action = "ACTION_STOP_FOREGROUND"
|
||||||
|
}
|
||||||
|
val deletePendingIntent = PendingIntent.getService(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
deleteIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Notification.Builder(this, chanId)
|
||||||
|
.setContentTitle("Server Box")
|
||||||
|
.setContentText("Open the app")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
Notification.Builder(this)
|
||||||
|
.setContentTitle("Server Box")
|
||||||
|
.setContentText("Open the app")
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopForegroundService() {
|
||||||
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package tech.lolli.toolbox
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
import android.os.IBinder
|
|
||||||
import org.jetbrains.annotations.Nullable
|
|
||||||
|
|
||||||
class KeepAliveService : Service() {
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
package tech.lolli.toolbox
|
package tech.lolli.toolbox
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
@@ -18,8 +23,18 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
"startService" -> {
|
"startService" -> {
|
||||||
val intent = Intent(this@MainActivity, KeepAliveService::class.java)
|
reqPerm()
|
||||||
startService(intent)
|
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(serviceIntent)
|
||||||
|
} else {
|
||||||
|
startService(serviceIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"stopService" -> {
|
||||||
|
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
|
||||||
|
stopService(serviceIntent)
|
||||||
|
result.success(null)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
result.notImplemented()
|
result.notImplemented()
|
||||||
@@ -28,4 +43,16 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reqPerm() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
123,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
102
ios/Podfile.lock
@@ -1,68 +1,18 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- app_links (0.0.2):
|
||||||
|
- Flutter
|
||||||
|
- camera_avfoundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_background_service_ios (0.0.3):
|
|
||||||
- Flutter
|
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- GoogleDataTransport (9.4.1):
|
|
||||||
- GoogleUtilities/Environment (~> 7.7)
|
|
||||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
|
||||||
- GoogleMLKit/BarcodeScanning (6.0.0):
|
|
||||||
- GoogleMLKit/MLKitCore
|
|
||||||
- MLKitBarcodeScanning (~> 5.0.0)
|
|
||||||
- GoogleMLKit/MLKitCore (6.0.0):
|
|
||||||
- MLKitCommon (~> 11.0.0)
|
|
||||||
- GoogleToolboxForMac/Defines (4.2.1)
|
|
||||||
- GoogleToolboxForMac/Logger (4.2.1):
|
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
|
||||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
|
||||||
- GoogleUtilities/Environment (7.13.3):
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
|
||||||
- GoogleUtilities/Logger (7.13.3):
|
|
||||||
- GoogleUtilities/Environment
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Privacy (7.13.3)
|
|
||||||
- GoogleUtilities/UserDefaults (7.13.3):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilitiesComponents (1.1.0):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GTMSessionFetcher/Core (3.5.0)
|
|
||||||
- icloud_storage (0.0.1):
|
- icloud_storage (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- local_auth_darwin (0.0.1):
|
- local_auth_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- MLImage (1.0.0-beta5)
|
|
||||||
- MLKitBarcodeScanning (5.0.0):
|
|
||||||
- MLKitCommon (~> 11.0)
|
|
||||||
- MLKitVision (~> 7.0)
|
|
||||||
- MLKitCommon (11.0.0):
|
|
||||||
- GoogleDataTransport (< 10.0, >= 9.4.1)
|
|
||||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
|
||||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
|
||||||
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
|
|
||||||
- GoogleUtilitiesComponents (~> 1.0)
|
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
|
||||||
- MLKitVision (7.0.0):
|
|
||||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
|
||||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
|
||||||
- MLImage (= 1.0.0-beta5)
|
|
||||||
- MLKitCommon (~> 11.0)
|
|
||||||
- mobile_scanner (5.1.1):
|
|
||||||
- Flutter
|
|
||||||
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
|
|
||||||
- nanopb (2.30910.0):
|
|
||||||
- nanopb/decode (= 2.30910.0)
|
|
||||||
- nanopb/encode (= 2.30910.0)
|
|
||||||
- nanopb/decode (2.30910.0)
|
|
||||||
- nanopb/encode (2.30910.0)
|
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
@@ -70,7 +20,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- plain_notification_token (0.0.1):
|
- plain_notification_token (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -86,13 +35,13 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
|
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
|
- icloud_storage (from `.symlinks/plugins/icloud_storage/ios`)
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
|
- plain_notification_token (from `.symlinks/plugins/plain_notification_token/ios`)
|
||||||
@@ -103,36 +52,21 @@ DEPENDENCIES:
|
|||||||
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
|
- watch_connectivity (from `.symlinks/plugins/watch_connectivity/ios`)
|
||||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
|
||||||
trunk:
|
|
||||||
- GoogleDataTransport
|
|
||||||
- GoogleMLKit
|
|
||||||
- GoogleToolboxForMac
|
|
||||||
- GoogleUtilities
|
|
||||||
- GoogleUtilitiesComponents
|
|
||||||
- GTMSessionFetcher
|
|
||||||
- MLImage
|
|
||||||
- MLKitBarcodeScanning
|
|
||||||
- MLKitCommon
|
|
||||||
- MLKitVision
|
|
||||||
- nanopb
|
|
||||||
- PromisesObjC
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
app_links:
|
||||||
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
|
camera_avfoundation:
|
||||||
|
:path: ".symlinks/plugins/camera_avfoundation/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_background_service_ios:
|
|
||||||
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
icloud_storage:
|
icloud_storage:
|
||||||
:path: ".symlinks/plugins/icloud_storage/ios"
|
:path: ".symlinks/plugins/icloud_storage/ios"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
mobile_scanner:
|
|
||||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
@@ -153,28 +87,16 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
|
||||||
|
camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4
|
||||||
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
|
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
|
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
|
||||||
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
|
|
||||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
|
||||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
|
||||||
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
|
||||||
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
|
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
|
||||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||||
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
|
||||||
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
|
|
||||||
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
|
|
||||||
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
|
|
||||||
mobile_scanner: 8564358885a9253c43f822435b70f9345c87224f
|
|
||||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
|
||||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
|
plain_notification_token: b36467dc91939a7b6754267c701bbaca14996ee1
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
|
|||||||
@@ -302,7 +302,6 @@
|
|||||||
E33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */,
|
E33A3E4A2A626DD0009744AB /* Embed Foundation Extensions */,
|
||||||
E39515D52AB5AD64003602C1 /* Embed Watch Content */,
|
E39515D52AB5AD64003602C1 /* Embed Watch Content */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
955896919A10AA2BEC131F36 /* [CP] Copy Pods Resources */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -452,23 +451,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
955896919A10AA2BEC131F36 /* [CP] Copy Pods Resources */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -690,7 +672,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -700,7 +682,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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";
|
||||||
@@ -826,7 +808,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -836,7 +818,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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";
|
||||||
@@ -854,7 +836,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -864,7 +846,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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";
|
||||||
@@ -885,7 +867,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -898,7 +880,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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;
|
||||||
@@ -924,7 +906,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -937,7 +919,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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)";
|
||||||
@@ -960,7 +942,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -973,7 +955,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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)";
|
||||||
@@ -996,7 +978,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1008,7 +990,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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;
|
||||||
@@ -1037,7 +1019,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1049,7 +1031,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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;
|
||||||
@@ -1075,7 +1057,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 = 1070;
|
CURRENT_PROJECT_VERSION = 1104;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1087,7 +1069,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1070;
|
MARKETING_VERSION = 1.0.1104;
|
||||||
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;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1 +1,37 @@
|
|||||||
{"images":[{"scale":"3x","idiom":"universal","filename":"AppIcon-29.0x29.0@3x.png","size":"29x29","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-29.0x29.0@2x.png","size":"29x29","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-64.0x64.0@3x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-20.0x20.0@2x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-60.0x60.0@2x.png","size":"60x60","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-40.0x40.0@3x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-76.0x76.0@2x.png","size":"76x76","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-38.0x38.0@3x.png","size":"38x38","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-68.0x68.0@2x.png","size":"68x68","platform":"ios"},{"scale":"1x","idiom":"universal","filename":"AppIcon-1024.0x1024.0@1x.png","size":"1024x1024","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-64.0x64.0@2x.png","size":"64x64","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-40.0x40.0@2x.png","size":"40x40","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-83.5x83.5@2x.png","size":"83.5x83.5","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-20.0x20.0@3x.png","size":"20x20","platform":"ios"},{"scale":"2x","idiom":"universal","filename":"AppIcon-38.0x38.0@2x.png","size":"38x38","platform":"ios"},{"scale":"3x","idiom":"universal","filename":"AppIcon-60.0x60.0@3x.png","size":"60x60","platform":"ios"}],"info":{"version":1,"author":"appicon"}}
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Icon-1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "icon-1024 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024 1.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
25
lib/app.dart
@@ -74,6 +74,7 @@ class MyApp extends StatelessWidget {
|
|||||||
final locale = Stores.setting.locale.fetch().toLocale;
|
final locale = Stores.setting.locale.fetch().toLocale;
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
key: ValueKey(locale),
|
||||||
locale: locale,
|
locale: locale,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
LibLocalizations.delegate,
|
LibLocalizations.delegate,
|
||||||
@@ -86,19 +87,21 @@ class MyApp extends StatelessWidget {
|
|||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
theme: light.fixWindowsFont,
|
theme: light.fixWindowsFont,
|
||||||
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
||||||
home: Builder(
|
home: VirtualWindowFrame(
|
||||||
builder: (context) {
|
child: Builder(
|
||||||
context.setLibL10n();
|
builder: (context) {
|
||||||
final appL10n = AppLocalizations.of(context);
|
context.setLibL10n();
|
||||||
if (appL10n != null) l10n = appL10n;
|
final appL10n = AppLocalizations.of(context);
|
||||||
|
if (appL10n != null) l10n = appL10n;
|
||||||
|
|
||||||
final intros = _IntroPage.builders;
|
final intros = _IntroPage.builders;
|
||||||
if (intros.isNotEmpty) {
|
if (intros.isNotEmpty) {
|
||||||
return _IntroPage(intros);
|
return _IntroPage(intros);
|
||||||
}
|
}
|
||||||
|
|
||||||
return const HomePage();
|
return const HomePage();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,8 @@ abstract final class BgRunMC {
|
|||||||
static void startService() {
|
static void startService() {
|
||||||
_channel.invokeMethod('startService');
|
_channel.invokeMethod('startService');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void stopService() {
|
||||||
|
_channel.invokeMethod('stopService');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import 'package:server_box/data/res/build_data.dart';
|
|
||||||
|
|
||||||
extension BuildDataX on BuildData {
|
|
||||||
static const versionStr = 'v1.0.${BuildData.build}';
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
import 'package:server_box/data/model/server/private_key_info.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/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/backup.dart';
|
|
||||||
import 'package:server_box/view/page/container.dart';
|
import 'package:server_box/view/page/container.dart';
|
||||||
import 'package:server_box/view/page/home/home.dart';
|
import 'package:server_box/view/page/home/home.dart';
|
||||||
import 'package:server_box/view/page/iperf.dart';
|
import 'package:server_box/view/page/iperf.dart';
|
||||||
import 'package:server_box/view/page/ping.dart';
|
import 'package:server_box/view/page/ping.dart';
|
||||||
import 'package:server_box/view/page/private_key/edit.dart';
|
import 'package:server_box/view/page/private_key/edit.dart';
|
||||||
import 'package:server_box/view/page/private_key/list.dart';
|
|
||||||
import 'package:server_box/view/page/pve.dart';
|
import 'package:server_box/view/page/pve.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/setting/platform/android.dart';
|
import 'package:server_box/view/page/setting/platform/android.dart';
|
||||||
@@ -18,18 +16,12 @@ import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
|
|||||||
import 'package:server_box/view/page/snippet/result.dart';
|
import 'package:server_box/view/page/snippet/result.dart';
|
||||||
import 'package:server_box/view/page/ssh/page.dart';
|
import 'package:server_box/view/page/ssh/page.dart';
|
||||||
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
||||||
import 'package:server_box/view/page/storage/local.dart';
|
|
||||||
|
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
import 'package:server_box/view/page/editor.dart';
|
|
||||||
import 'package:server_box/view/page/process.dart';
|
import 'package:server_box/view/page/process.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
|
||||||
import 'package:server_box/view/page/server/tab.dart';
|
import 'package:server_box/view/page/server/tab.dart';
|
||||||
import 'package:server_box/view/page/setting/entry.dart';
|
|
||||||
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
|
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
|
||||||
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
|
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
|
||||||
import 'package:server_box/view/page/snippet/edit.dart';
|
import 'package:server_box/view/page/snippet/edit.dart';
|
||||||
import 'package:server_box/view/page/snippet/list.dart';
|
|
||||||
import 'package:server_box/view/page/storage/sftp.dart';
|
import 'package:server_box/view/page/storage/sftp.dart';
|
||||||
import 'package:server_box/view/page/storage/sftp_mission.dart';
|
import 'package:server_box/view/page/storage/sftp_mission.dart';
|
||||||
|
|
||||||
@@ -66,13 +58,6 @@ class AppRoutes {
|
|||||||
return AppRoutes(ServerPage(key: key), 'server_tab');
|
return AppRoutes(ServerPage(key: key), 'server_tab');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes serverEdit({Key? key, Spi? spi}) {
|
|
||||||
return AppRoutes(
|
|
||||||
ServerEditPage(spi: spi),
|
|
||||||
'server_${spi == null ? 'add' : 'edit'}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
|
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
|
||||||
return AppRoutes(
|
return AppRoutes(
|
||||||
PrivateKeyEditPage(pki: pki),
|
PrivateKeyEditPage(pki: pki),
|
||||||
@@ -80,10 +65,6 @@ class AppRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes keyList({Key? key}) {
|
|
||||||
return AppRoutes(PrivateKeysListPage(key: key), 'key_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
|
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
|
||||||
return AppRoutes(
|
return AppRoutes(
|
||||||
SnippetEditPage(snippet: snippet),
|
SnippetEditPage(snippet: snippet),
|
||||||
@@ -91,10 +72,6 @@ class AppRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes snippetList({Key? key}) {
|
|
||||||
return AppRoutes(SnippetListPage(key: key), 'snippet_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes ssh({
|
static AppRoutes ssh({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Spi spi,
|
required Spi spi,
|
||||||
@@ -116,17 +93,6 @@ class AppRoutes {
|
|||||||
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes localStorage(
|
|
||||||
{Key? key, bool isPickFile = false, String? initDir}) {
|
|
||||||
return AppRoutes(
|
|
||||||
LocalStoragePage(
|
|
||||||
key: key,
|
|
||||||
isPickFile: isPickFile,
|
|
||||||
initDir: initDir,
|
|
||||||
),
|
|
||||||
'local_storage');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes sftpMission({Key? key}) {
|
static AppRoutes sftpMission({Key? key}) {
|
||||||
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
|
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
|
||||||
}
|
}
|
||||||
@@ -143,34 +109,10 @@ class AppRoutes {
|
|||||||
'sftp');
|
'sftp');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes backup({Key? key}) {
|
|
||||||
return AppRoutes(BackupPage(key: key), 'backup');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes docker({Key? key, required Spi spi}) {
|
static AppRoutes docker({Key? key, required Spi spi}) {
|
||||||
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
|
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Pop true if the text is changed & [path] is not null
|
|
||||||
/// - Pop text if [path] is null
|
|
||||||
static AppRoutes editor({
|
|
||||||
Key? key,
|
|
||||||
String? path,
|
|
||||||
String? text,
|
|
||||||
String? langCode,
|
|
||||||
String? title,
|
|
||||||
}) {
|
|
||||||
return AppRoutes(
|
|
||||||
EditorPage(
|
|
||||||
key: key,
|
|
||||||
path: path,
|
|
||||||
text: text,
|
|
||||||
langCode: langCode,
|
|
||||||
title: title,
|
|
||||||
),
|
|
||||||
'editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
// static AppRoutes fullscreen({Key? key}) {
|
// static AppRoutes fullscreen({Key? key}) {
|
||||||
// return AppRoutes(FullScreenPage(key: key), 'fullscreen');
|
// return AppRoutes(FullScreenPage(key: key), 'fullscreen');
|
||||||
// }
|
// }
|
||||||
@@ -187,10 +129,6 @@ class AppRoutes {
|
|||||||
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
|
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes settings({Key? key}) {
|
|
||||||
return AppRoutes(SettingPage(key: key), 'setting');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverOrder({Key? key}) {
|
static AppRoutes serverOrder({Key? key}) {
|
||||||
return AppRoutes(ServerOrderPage(key: key), 'server_order');
|
return AppRoutes(ServerOrderPage(key: key), 'server_order');
|
||||||
}
|
}
|
||||||
|
|||||||
40
lib/core/sync.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/data/model/app/backup.dart';
|
||||||
|
import 'package:server_box/data/store/no_backup.dart';
|
||||||
|
|
||||||
|
const bakSync = BakSyncer._();
|
||||||
|
|
||||||
|
final class BakSyncer extends SyncIface<Backup> {
|
||||||
|
const BakSyncer._() : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveToFile() => Backup.backup();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Backup> fromFile(String path) async {
|
||||||
|
final content = await File(path).readAsString();
|
||||||
|
return Backup.fromJsonString(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RemoteStorage?> get remoteStorage async {
|
||||||
|
if (isMacOS || isIOS) await icloud.init('iCloud.tech.lolli.serverbox');
|
||||||
|
final settings = NoBackupStore.instance;
|
||||||
|
await webdav.init(WebdavInitArgs(
|
||||||
|
url: settings.webdavUrl.fetch(),
|
||||||
|
user: settings.webdavUser.fetch(),
|
||||||
|
pwd: settings.webdavPwd.fetch(),
|
||||||
|
prefix: 'serverbox/',
|
||||||
|
));
|
||||||
|
|
||||||
|
final icloudEnabled = settings.icloudSync.fetch();
|
||||||
|
if (icloudEnabled) return icloud;
|
||||||
|
|
||||||
|
final webdavEnabled = settings.webdavSync.fetch();
|
||||||
|
if (webdavEnabled) return webdav;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
@@ -62,6 +63,8 @@ Future<SSHClient> genClient(
|
|||||||
}) async {
|
}) async {
|
||||||
onStatus?.call(GenSSHClientStatus.socket);
|
onStatus?.call(GenSSHClientStatus.socket);
|
||||||
|
|
||||||
|
String? alterUser;
|
||||||
|
|
||||||
final socket = await () async {
|
final socket = await () async {
|
||||||
// Proxy
|
// Proxy
|
||||||
final jumpSpi_ = () {
|
final jumpSpi_ = () {
|
||||||
@@ -91,15 +94,18 @@ Future<SSHClient> genClient(
|
|||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Loggers.app.warning('genClient', e);
|
||||||
if (spi.alterUrl == null) rethrow;
|
if (spi.alterUrl == null) rethrow;
|
||||||
try {
|
try {
|
||||||
final ipPort = spi.fromStringUrl();
|
final res = spi.fromStringUrl();
|
||||||
|
alterUser = res.$2;
|
||||||
return await SSHSocket.connect(
|
return await SSHSocket.connect(
|
||||||
ipPort.$1,
|
res.$1,
|
||||||
ipPort.$2,
|
res.$3,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Loggers.app.warning('genClient alterUrl', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +116,7 @@ Future<SSHClient> genClient(
|
|||||||
onStatus?.call(GenSSHClientStatus.pwd);
|
onStatus?.call(GenSSHClientStatus.pwd);
|
||||||
return SSHClient(
|
return SSHClient(
|
||||||
socket,
|
socket,
|
||||||
username: spi.user,
|
username: alterUser ?? spi.user,
|
||||||
onPasswordRequest: () => spi.pwd,
|
onPasswordRequest: () => spi.pwd,
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
// printDebug: debugPrint,
|
// printDebug: debugPrint,
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:icloud_storage/icloud_storage.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
|
||||||
import 'package:server_box/data/model/app/sync.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
|
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
|
||||||
|
|
||||||
abstract final class ICloud {
|
|
||||||
static const _containerId = 'iCloud.tech.lolli.serverbox';
|
|
||||||
|
|
||||||
static final _logger = Logger('iCloud');
|
|
||||||
|
|
||||||
/// Upload file to iCloud
|
|
||||||
///
|
|
||||||
/// - [relativePath] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [localPath] has higher priority than [relativePath], but only apply
|
|
||||||
/// to the local path instead of iCloud path
|
|
||||||
///
|
|
||||||
/// Return [null] if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<ICloudErr?> upload({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
final completer = Completer<ICloudErr?>();
|
|
||||||
try {
|
|
||||||
await ICloudStorage.upload(
|
|
||||||
containerId: _containerId,
|
|
||||||
filePath: localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
destinationRelativePath: relativePath,
|
|
||||||
onProgress: (stream) {
|
|
||||||
stream.listen(
|
|
||||||
null,
|
|
||||||
onDone: () => completer.complete(null),
|
|
||||||
onError: (e) => completer.complete(
|
|
||||||
ICloudErr(type: ICloudErrType.generic, message: '$e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Upload $relativePath failed', e, s);
|
|
||||||
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<ICloudFile>> getAll() async {
|
|
||||||
return await ICloudStorage.gather(
|
|
||||||
containerId: _containerId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> delete(String relativePath) async {
|
|
||||||
try {
|
|
||||||
await ICloudStorage.delete(
|
|
||||||
containerId: _containerId,
|
|
||||||
relativePath: relativePath,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Delete $relativePath failed', e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download file from iCloud
|
|
||||||
///
|
|
||||||
/// - [relativePath] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [localPath] has higher priority than [relativePath], but only apply
|
|
||||||
/// to the local path instead of iCloud path
|
|
||||||
///
|
|
||||||
/// Return `null` if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<ICloudErr?> download({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
final completer = Completer<ICloudErr?>();
|
|
||||||
try {
|
|
||||||
await ICloudStorage.download(
|
|
||||||
containerId: _containerId,
|
|
||||||
relativePath: relativePath,
|
|
||||||
destinationFilePath: localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
onProgress: (stream) {
|
|
||||||
stream.listen(
|
|
||||||
null,
|
|
||||||
onDone: () => completer.complete(null),
|
|
||||||
onError: (e) => completer.complete(
|
|
||||||
ICloudErr(type: ICloudErrType.generic, message: '$e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Download $relativePath failed', e, s);
|
|
||||||
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
|
|
||||||
}
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sync file between iCloud and local
|
|
||||||
///
|
|
||||||
/// - [relativePaths] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [bakPrefix] is the suffix of backup file, default to [null].
|
|
||||||
/// All files downloaded from cloud will be suffixed with [bakPrefix].
|
|
||||||
///
|
|
||||||
/// Return `null` if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<SyncResult<String, ICloudErr>> syncFiles({
|
|
||||||
required Iterable<String> relativePaths,
|
|
||||||
String? bakPrefix,
|
|
||||||
}) async {
|
|
||||||
final uploadFiles = <String>[];
|
|
||||||
final downloadFiles = <String>[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
final errs = <String, ICloudErr>{};
|
|
||||||
|
|
||||||
final allFiles = await getAll();
|
|
||||||
|
|
||||||
/// remove files not in relativePaths
|
|
||||||
allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath));
|
|
||||||
|
|
||||||
final missions = <Future<void>>[];
|
|
||||||
|
|
||||||
/// upload files not in iCloud
|
|
||||||
final missed = relativePaths.where((e) {
|
|
||||||
return !allFiles.any((f) => f.relativePath == e);
|
|
||||||
});
|
|
||||||
missions.addAll(missed.map((e) async {
|
|
||||||
final err = await upload(relativePath: e);
|
|
||||||
if (err != null) {
|
|
||||||
errs[e] = err;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
final docPath = Paths.doc;
|
|
||||||
|
|
||||||
/// compare files in iCloud and local
|
|
||||||
missions.addAll(allFiles.map((file) async {
|
|
||||||
final relativePath = file.relativePath;
|
|
||||||
|
|
||||||
/// Check date
|
|
||||||
final localFile = File('$docPath/$relativePath');
|
|
||||||
if (!localFile.existsSync()) {
|
|
||||||
/// Local file not found, download remote file
|
|
||||||
final err = await download(relativePath: relativePath);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final localDate = await localFile.lastModified();
|
|
||||||
final remoteDate = file.contentChangeDate;
|
|
||||||
|
|
||||||
/// Same date, skip
|
|
||||||
if (remoteDate.difference(localDate) == Duration.zero) return;
|
|
||||||
|
|
||||||
/// Local is newer than remote, so upload local file
|
|
||||||
if (remoteDate.isBefore(localDate)) {
|
|
||||||
await delete(relativePath);
|
|
||||||
final err = await upload(relativePath: relativePath);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
uploadFiles.add(relativePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remote is newer than local, so download remote
|
|
||||||
final localPath = '$docPath/${bakPrefix ?? ''}$relativePath';
|
|
||||||
final err = await download(
|
|
||||||
relativePath: relativePath,
|
|
||||||
localPath: localPath,
|
|
||||||
);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
downloadFiles.add(relativePath);
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Future.wait(missions);
|
|
||||||
|
|
||||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: errs);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Sync: $relativePaths failed', e, s);
|
|
||||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: {
|
|
||||||
'Generic': ICloudErr(type: ICloudErrType.generic, message: '$e')
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
_logger.info('Sync, up: $uploadFiles, down: $downloadFiles');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> sync() async {
|
|
||||||
final result = await download(relativePath: Miscs.bakFileName);
|
|
||||||
if (result != null) {
|
|
||||||
await backup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final dlFile = await File(Paths.bak).readAsString();
|
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
|
||||||
await dlBak.restore();
|
|
||||||
|
|
||||||
await backup();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> backup() async {
|
|
||||||
await Backup.backup();
|
|
||||||
final uploadResult = await upload(relativePath: Miscs.bakFileName);
|
|
||||||
if (uploadResult != null) {
|
|
||||||
_logger.warning('Upload backup failed: $uploadResult');
|
|
||||||
} else {
|
|
||||||
_logger.info('Upload backup success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
import 'package:webdav_client/webdav_client.dart';
|
|
||||||
|
|
||||||
abstract final class Webdav {
|
|
||||||
/// Some WebDAV provider only support non-root path
|
|
||||||
static const _prefix = 'srvbox/';
|
|
||||||
|
|
||||||
static var _client = WebdavClient(
|
|
||||||
url: Stores.setting.webdavUrl.fetch(),
|
|
||||||
user: Stores.setting.webdavUser.fetch(),
|
|
||||||
pwd: Stores.setting.webdavPwd.fetch(),
|
|
||||||
);
|
|
||||||
|
|
||||||
static final _logger = Logger('Webdav');
|
|
||||||
|
|
||||||
static Future<String?> test(String url, String user, String pwd) async {
|
|
||||||
final client = WebdavClient(url: url, user: user, pwd: pwd);
|
|
||||||
try {
|
|
||||||
await client.ping();
|
|
||||||
return null;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Test failed', e, s);
|
|
||||||
return e.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> upload({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _client.writeFile(
|
|
||||||
localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
_prefix + relativePath,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Upload $relativePath failed', e, s);
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> delete(String relativePath) async {
|
|
||||||
try {
|
|
||||||
await _client.remove(_prefix + relativePath);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Delete $relativePath failed', e, s);
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> download({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _client.readFile(
|
|
||||||
_prefix + relativePath,
|
|
||||||
localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Download $relativePath failed');
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<String>> list() async {
|
|
||||||
try {
|
|
||||||
final list = await _client.readDir(_prefix);
|
|
||||||
final names = <String>[];
|
|
||||||
for (final item in list) {
|
|
||||||
if ((item.isDir ?? true) || item.name == null) continue;
|
|
||||||
names.add(item.name!);
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('List failed', e, s);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void changeClient(String url, String user, String pwd) {
|
|
||||||
_client = WebdavClient(url: url, user: user, pwd: pwd);
|
|
||||||
Stores.setting.webdavUrl.put(url);
|
|
||||||
Stores.setting.webdavUser.put(user);
|
|
||||||
Stores.setting.webdavPwd.put(pwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> sync() async {
|
|
||||||
final result = await download(relativePath: Miscs.bakFileName);
|
|
||||||
if (result != null) {
|
|
||||||
await backup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final dlFile = await File(Paths.bak).readAsString();
|
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
|
||||||
await dlBak.restore();
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Restore failed: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
await backup();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a local backup and upload it to WebDAV
|
|
||||||
static Future<void> backup() async {
|
|
||||||
await Backup.backup();
|
|
||||||
final uploadResult = await upload(relativePath: Miscs.bakFileName);
|
|
||||||
if (uploadResult != null) {
|
|
||||||
_logger.warning('Upload failed: $uploadResult');
|
|
||||||
} else {
|
|
||||||
_logger.info('Upload success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ const backupFormatVersion = 1;
|
|||||||
final _logger = Logger('Backup');
|
final _logger = Logger('Backup');
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Backup {
|
class Backup implements Mergeable {
|
||||||
// backup format version
|
// backup format version
|
||||||
final int version;
|
final int version;
|
||||||
final String date;
|
final String date;
|
||||||
@@ -28,6 +28,7 @@ class Backup {
|
|||||||
final Map<String, dynamic> container;
|
final Map<String, dynamic> container;
|
||||||
final Map<String, dynamic> history;
|
final Map<String, dynamic> history;
|
||||||
final int? lastModTime;
|
final int? lastModTime;
|
||||||
|
final Map<String, dynamic>? settings;
|
||||||
|
|
||||||
const Backup({
|
const Backup({
|
||||||
required this.version,
|
required this.version,
|
||||||
@@ -37,6 +38,7 @@ class Backup {
|
|||||||
required this.keys,
|
required this.keys,
|
||||||
required this.container,
|
required this.container,
|
||||||
required this.history,
|
required this.history,
|
||||||
|
required this.settings,
|
||||||
this.lastModTime,
|
this.lastModTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,16 +54,18 @@ class Backup {
|
|||||||
keys = Stores.key.fetch(),
|
keys = Stores.key.fetch(),
|
||||||
container = Stores.container.box.toJson(),
|
container = Stores.container.box.toJson(),
|
||||||
lastModTime = Stores.lastModTime,
|
lastModTime = Stores.lastModTime,
|
||||||
history = Stores.history.box.toJson();
|
history = Stores.history.box.toJson(),
|
||||||
|
settings = Stores.setting.box.toJson();
|
||||||
|
|
||||||
static Future<String> backup([String? name]) async {
|
static Future<String> backup([String? name]) async {
|
||||||
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
|
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
|
||||||
final path = '${Paths.doc}/${name ?? Miscs.bakFileName}';
|
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
|
||||||
await File(path).writeAsString(result);
|
await File(path).writeAsString(result);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restore({bool force = false}) async {
|
@override
|
||||||
|
Future<void> merge({bool force = false}) async {
|
||||||
final curTime = Stores.lastModTime ?? 0;
|
final curTime = Stores.lastModTime ?? 0;
|
||||||
final bakTime = lastModTime ?? 0;
|
final bakTime = lastModTime ?? 0;
|
||||||
final shouldRestore = force || curTime < bakTime;
|
final shouldRestore = force || curTime < bakTime;
|
||||||
@@ -176,6 +180,29 @@ class Backup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
final settings_ = settings;
|
||||||
|
if (settings_ != null) {
|
||||||
|
if (force) {
|
||||||
|
Stores.setting.box.putAll(settings_);
|
||||||
|
} else {
|
||||||
|
final nowSettings = Stores.setting.box.keys.toSet();
|
||||||
|
final bakSettings = settings_.keys.toSet();
|
||||||
|
final newSettings = bakSettings.difference(nowSettings);
|
||||||
|
final delSettings = nowSettings.difference(bakSettings);
|
||||||
|
final updateSettings = nowSettings.intersection(bakSettings);
|
||||||
|
for (final s in newSettings) {
|
||||||
|
Stores.setting.box.put(s, settings_[s]);
|
||||||
|
}
|
||||||
|
for (final s in delSettings) {
|
||||||
|
Stores.setting.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateSettings) {
|
||||||
|
Stores.setting.box.put(s, settings_[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Provider.reload();
|
Provider.reload();
|
||||||
RNodes.app.notify();
|
RNodes.app.notify();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Backup _$BackupFromJson(Map<String, dynamic> json) => Backup(
|
|||||||
.toList(),
|
.toList(),
|
||||||
container: json['container'] as Map<String, dynamic>,
|
container: json['container'] as Map<String, dynamic>,
|
||||||
history: json['history'] as Map<String, dynamic>,
|
history: json['history'] as Map<String, dynamic>,
|
||||||
|
settings: json['settings'] as Map<String, dynamic>?,
|
||||||
lastModTime: (json['lastModTime'] as num?)?.toInt(),
|
lastModTime: (json['lastModTime'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -32,4 +33,5 @@ Map<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{
|
|||||||
'container': instance.container,
|
'container': instance.container,
|
||||||
'history': instance.history,
|
'history': instance.history,
|
||||||
'lastModTime': instance.lastModTime,
|
'lastModTime': instance.lastModTime,
|
||||||
|
'settings': instance.settings,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
|
||||||
|
final _seperator = Pfs.seperator;
|
||||||
|
|
||||||
/// It's used on platform's file system.
|
/// It's used on platform's file system.
|
||||||
/// So use [Platform.pathSeparator] to join path.
|
/// So use [Platform.pathSeparator] to join path.
|
||||||
class LocalPath {
|
class LocalPath {
|
||||||
final String _prefixPath;
|
final String _prefixPath;
|
||||||
String _path = '/';
|
String _path = _seperator;
|
||||||
String? _prePath;
|
String? _prePath;
|
||||||
String get path => _prefixPath + _path;
|
String get path => _prefixPath + _path;
|
||||||
|
|
||||||
@@ -13,20 +15,20 @@ class LocalPath {
|
|||||||
void update(String newPath) {
|
void update(String newPath) {
|
||||||
_prePath = _path;
|
_prePath = _path;
|
||||||
if (newPath == '..') {
|
if (newPath == '..') {
|
||||||
_path = _path.substring(0, _path.lastIndexOf('/'));
|
_path = _path.substring(0, _path.lastIndexOf(_seperator));
|
||||||
if (_path == '') {
|
if (_path == '') {
|
||||||
_path = '/';
|
_path = _seperator;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPath == '/') {
|
if (newPath == _seperator) {
|
||||||
_path = '/';
|
_path = _seperator;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_path = _path.joinPath(newPath);
|
_path = _path.joinPath(newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get canBack => path != '$_prefixPath/';
|
bool get canBack => path != '$_prefixPath$_seperator';
|
||||||
|
|
||||||
bool undo() {
|
bool undo() {
|
||||||
if (_prePath == null || _path == _prePath) {
|
if (_prePath == null || _path == _prePath) {
|
||||||
@@ -38,7 +40,7 @@ class LocalPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _trimSuffix(String prefixPath) {
|
String _trimSuffix(String prefixPath) {
|
||||||
if (prefixPath.endsWith('/')) {
|
if (prefixPath.endsWith(_seperator)) {
|
||||||
return prefixPath.substring(0, prefixPath.length - 1);
|
return prefixPath.substring(0, prefixPath.length - 1);
|
||||||
}
|
}
|
||||||
return prefixPath;
|
return prefixPath;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ enum ShellFunc {
|
|||||||
return '''
|
return '''
|
||||||
mkdir -p $scriptDir
|
mkdir -p $scriptDir
|
||||||
cat > $scriptPath
|
cat > $scriptPath
|
||||||
chmod 744 $scriptPath
|
chmod 755 $scriptPath
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,62 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/view/page/ping.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/view/page/server/tab.dart';
|
import 'package:server_box/view/page/server/tab.dart';
|
||||||
|
import 'package:server_box/view/page/setting/entry.dart';
|
||||||
import 'package:server_box/view/page/snippet/list.dart';
|
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:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/view/page/storage/local.dart';
|
||||||
|
|
||||||
enum AppTab {
|
enum AppTab {
|
||||||
server,
|
server,
|
||||||
ssh,
|
ssh,
|
||||||
|
file,
|
||||||
snippet,
|
snippet,
|
||||||
ping,
|
settings,
|
||||||
;
|
;
|
||||||
|
|
||||||
Widget get page {
|
Widget get page {
|
||||||
switch (this) {
|
return switch (this) {
|
||||||
case server:
|
server => const ServerPage(),
|
||||||
return const ServerPage();
|
settings => const SettingsPage(),
|
||||||
case snippet:
|
ssh => const SSHTabPage(),
|
||||||
return const SnippetListPage();
|
file => const LocalFilePage(),
|
||||||
case ssh:
|
snippet => const SnippetListPage(),
|
||||||
return const SSHTabPage();
|
};
|
||||||
case ping:
|
}
|
||||||
return const PingPage();
|
|
||||||
}
|
NavigationDestination get navDestination {
|
||||||
|
return switch (this) {
|
||||||
|
server => NavigationDestination(
|
||||||
|
icon: const Icon(BoxIcons.bx_server),
|
||||||
|
label: l10n.server,
|
||||||
|
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||||
|
),
|
||||||
|
settings => NavigationDestination(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
label: libL10n.setting,
|
||||||
|
selectedIcon: const Icon(Icons.settings),
|
||||||
|
),
|
||||||
|
ssh => const NavigationDestination(
|
||||||
|
icon: Icon(Icons.terminal_outlined),
|
||||||
|
label: 'SSH',
|
||||||
|
selectedIcon: Icon(Icons.terminal),
|
||||||
|
),
|
||||||
|
snippet => NavigationDestination(
|
||||||
|
icon: const Icon(Icons.code),
|
||||||
|
label: l10n.snippet,
|
||||||
|
selectedIcon: const Icon(Icons.code),
|
||||||
|
),
|
||||||
|
file => NavigationDestination(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
label: libL10n.file,
|
||||||
|
selectedIcon: const Icon(Icons.folder),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NavigationDestination> get navDestinations {
|
||||||
|
return AppTab.values.map((e) => e.navDestination).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
abstract class TagPickable {
|
|
||||||
bool containsTag(String tag);
|
|
||||||
|
|
||||||
String get tagName;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.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/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
import 'package:server_box/data/model/app/tag_pickable.dart';
|
|
||||||
import 'package:server_box/data/model/server/battery.dart';
|
import 'package:server_box/data/model/server/battery.dart';
|
||||||
import 'package:server_box/data/model/server/conn.dart';
|
import 'package:server_box/data/model/server/conn.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.dart';
|
import 'package:server_box/data/model/server/cpu.dart';
|
||||||
@@ -14,7 +13,7 @@ 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/system.dart';
|
||||||
import 'package:server_box/data/model/server/temp.dart';
|
import 'package:server_box/data/model/server/temp.dart';
|
||||||
|
|
||||||
class Server implements TagPickable {
|
class Server {
|
||||||
Spi spi;
|
Spi spi;
|
||||||
ServerStatus status;
|
ServerStatus status;
|
||||||
SSHClient? client;
|
SSHClient? client;
|
||||||
@@ -27,14 +26,6 @@ class Server implements TagPickable {
|
|||||||
this.client,
|
this.client,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
bool containsTag(String tag) {
|
|
||||||
return spi.tags?.contains(tag) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tagName => spi.id;
|
|
||||||
|
|
||||||
bool get needGenClient => conn < ServerConn.connecting;
|
bool get needGenClient => conn < ServerConn.connecting;
|
||||||
|
|
||||||
bool get canViewDetails => conn == ServerConn.finished;
|
bool get canViewDetails => conn == ServerConn.finished;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ extension Spix on Spi {
|
|||||||
custom?.cmds != old.custom?.cmds;
|
custom?.cmds != old.custom?.cmds;
|
||||||
}
|
}
|
||||||
|
|
||||||
(String, int) fromStringUrl() {
|
(String ip, String usr, int port) fromStringUrl() {
|
||||||
if (alterUrl == null) {
|
if (alterUrl == null) {
|
||||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
|
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
|
||||||
}
|
}
|
||||||
@@ -106,18 +106,22 @@ extension Spix on Spi {
|
|||||||
if (splited.length != 2) {
|
if (splited.length != 2) {
|
||||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');
|
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no @');
|
||||||
}
|
}
|
||||||
final splited2 = splited[1].split(':');
|
final usr = splited[0];
|
||||||
if (splited2.length != 2) {
|
final idx = splited[1].lastIndexOf(':');
|
||||||
|
if (idx == -1) {
|
||||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');
|
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl no :');
|
||||||
}
|
}
|
||||||
final ip_ = splited2[0];
|
final ip_ = splited[1].substring(0, idx);
|
||||||
final port_ = int.tryParse(splited2[1]) ?? 22;
|
final port_ = int.tryParse(splited[1].substring(idx + 1));
|
||||||
if (port <= 0 || port > 65535) {
|
if (port_ == null || port_ <= 0 || port_ > 65535) {
|
||||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
|
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl port error');
|
||||||
}
|
}
|
||||||
return (ip_, port_);
|
return (ip_, usr, port_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just for showing the struct of the class.
|
||||||
|
///
|
||||||
|
/// **NOT** the default value.
|
||||||
static const example = Spi(
|
static const example = Spi(
|
||||||
name: 'name',
|
name: 'name',
|
||||||
ip: 'ip',
|
ip: 'ip',
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import 'package:json_annotation/json_annotation.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:xterm/core.dart';
|
import 'package:xterm/core.dart';
|
||||||
|
|
||||||
import 'package:server_box/data/model/app/tag_pickable.dart';
|
|
||||||
|
|
||||||
part 'snippet.g.dart';
|
part 'snippet.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@HiveType(typeId: 2)
|
@HiveType(typeId: 2)
|
||||||
class Snippet implements TagPickable {
|
class Snippet {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
final String name;
|
final String name;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
@@ -39,14 +37,6 @@ class Snippet implements TagPickable {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SnippetToJson(this);
|
Map<String, dynamic> toJson() => _$SnippetToJson(this);
|
||||||
|
|
||||||
@override
|
|
||||||
bool containsTag(String tag) {
|
|
||||||
return tags?.contains(tag) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tagName => name;
|
|
||||||
|
|
||||||
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
static final fmtFinder = RegExp(r'\$\{[^{}]+\}');
|
||||||
|
|
||||||
String fmtWithSpi(Spi spi) {
|
String fmtWithSpi(Spi spi) {
|
||||||
@@ -109,11 +99,21 @@ class Snippet implements TagPickable {
|
|||||||
if (special != null) {
|
if (special != null) {
|
||||||
final raw = key.substring(special.key.length + 1, key.length - 1);
|
final raw = key.substring(special.key.length + 1, key.length - 1);
|
||||||
await special.value((term: terminal, raw: raw));
|
await special.value((term: terminal, raw: raw));
|
||||||
|
} else {
|
||||||
|
// Term keys
|
||||||
|
final termKey = _find(fmtTermKeys, key);
|
||||||
|
if (termKey != null) {
|
||||||
|
await _doTermKeys(terminal, termKey, key);
|
||||||
|
} else {
|
||||||
|
// Normal input
|
||||||
|
terminal.textInput(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Term keys
|
// Text between this and next match
|
||||||
final termKey = _find(fmtTermKeys, key);
|
if (idx < starts.length - 1) {
|
||||||
if (termKey != null) await _doTermKeys(terminal, termKey, key);
|
terminal.textInput(argsFmted.substring(end, starts[idx + 1]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End term input
|
// End term input
|
||||||
@@ -129,10 +129,10 @@ class Snippet implements TagPickable {
|
|||||||
MapEntry<String, TerminalKey> termKey,
|
MapEntry<String, TerminalKey> termKey,
|
||||||
String key,
|
String key,
|
||||||
) async {
|
) async {
|
||||||
if (termKey.value == TerminalKey.enter) {
|
// if (termKey.value == TerminalKey.enter) {
|
||||||
terminal.keyInput(TerminalKey.enter);
|
// terminal.keyInput(TerminalKey.enter);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
final ctrlAlt = switch (termKey.value) {
|
final ctrlAlt = switch (termKey.value) {
|
||||||
TerminalKey.control => (ctrl: true, alt: false),
|
TerminalKey.control => (ctrl: true, alt: false),
|
||||||
@@ -140,6 +140,8 @@ class Snippet implements TagPickable {
|
|||||||
_ => (ctrl: false, alt: false),
|
_ => (ctrl: false, alt: false),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!key.contains('+')) return;
|
||||||
|
|
||||||
// `${ctrl+ad}` -> `ctrla + d`
|
// `${ctrl+ad}` -> `ctrla + d`
|
||||||
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
final chars = key.substring(termKey.key.length + 1, key.length - 1);
|
||||||
if (chars.isEmpty) return;
|
if (chars.isEmpty) return;
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
|
|
||||||
class AbsolutePath {
|
|
||||||
String _path;
|
|
||||||
final _prePath = <String>[];
|
|
||||||
|
|
||||||
AbsolutePath(this._path);
|
|
||||||
|
|
||||||
String get path => _path;
|
|
||||||
|
|
||||||
/// Update path, not set path
|
|
||||||
set path(String newPath) {
|
|
||||||
_prePath.add(_path);
|
|
||||||
if (newPath == '..') {
|
|
||||||
_path = _path.substring(0, _path.lastIndexOf('/'));
|
|
||||||
if (_path == '') {
|
|
||||||
_path = '/';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newPath.startsWith('/')) {
|
|
||||||
_path = newPath;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_path = _path.joinPath(newPath, seperator: '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool undo() {
|
|
||||||
if (_prePath.isEmpty) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_path = _prePath.removeLast();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,49 @@
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:server_box/data/model/sftp/absolute_path.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
|
||||||
|
/// Remote server only can be linux-like system, so use '/' as seperator
|
||||||
|
const _sep = '/';
|
||||||
|
|
||||||
class SftpBrowserStatus {
|
class SftpBrowserStatus {
|
||||||
final List<SftpName> files = [];
|
final List<SftpName> files = [];
|
||||||
final AbsolutePath path = AbsolutePath('/');
|
final path = _AbsolutePath(_sep);
|
||||||
SftpClient? client;
|
SftpClient? client;
|
||||||
|
|
||||||
SftpBrowserStatus(SSHClient client) {
|
SftpBrowserStatus(SSHClient client) {
|
||||||
client.sftp().then((value) => this.client = value);
|
client.sftp().then((value) => this.client = value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AbsolutePath {
|
||||||
|
String _path;
|
||||||
|
final _prePath = <String>[];
|
||||||
|
|
||||||
|
_AbsolutePath(this._path);
|
||||||
|
|
||||||
|
String get path => _path;
|
||||||
|
|
||||||
|
/// Update path, not set path
|
||||||
|
set path(String newPath) {
|
||||||
|
_prePath.add(_path);
|
||||||
|
if (newPath == '..') {
|
||||||
|
_path = _path.substring(0, _path.lastIndexOf(_sep));
|
||||||
|
if (_path == '') {
|
||||||
|
_path = _sep;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPath.startsWith(_sep)) {
|
||||||
|
_path = newPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_path = _path.joinPath(newPath, seperator: _sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool undo() {
|
||||||
|
if (_prePath.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_path = _prePath.removeLast();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:xterm/core.dart';
|
import 'package:xterm/core.dart';
|
||||||
|
|
||||||
part 'virtual_key.g.dart';
|
part 'virtual_key.g.dart';
|
||||||
|
|
||||||
|
enum VirtualKeyFunc { toggleIME, backspace, clipboard, snippet, file }
|
||||||
|
|
||||||
@HiveType(typeId: 4)
|
@HiveType(typeId: 4)
|
||||||
enum VirtKey {
|
enum VirtKey {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
@@ -38,23 +42,80 @@ enum VirtKey {
|
|||||||
@HiveField(14)
|
@HiveField(14)
|
||||||
pgup,
|
pgup,
|
||||||
@HiveField(15)
|
@HiveField(15)
|
||||||
pgdn;
|
pgdn,
|
||||||
|
@HiveField(16)
|
||||||
|
slash,
|
||||||
|
@HiveField(17)
|
||||||
|
backSlash,
|
||||||
|
@HiveField(18)
|
||||||
|
underscore,
|
||||||
|
@HiveField(19)
|
||||||
|
plus,
|
||||||
|
@HiveField(20)
|
||||||
|
equal,
|
||||||
|
@HiveField(21)
|
||||||
|
minus,
|
||||||
|
@HiveField(22)
|
||||||
|
parenLeft,
|
||||||
|
@HiveField(23)
|
||||||
|
parenRight,
|
||||||
|
@HiveField(24)
|
||||||
|
bracketLeft,
|
||||||
|
@HiveField(25)
|
||||||
|
bracketRight,
|
||||||
|
@HiveField(26)
|
||||||
|
braceLeft,
|
||||||
|
@HiveField(27)
|
||||||
|
braceRight,
|
||||||
|
@HiveField(28)
|
||||||
|
chevronLeft,
|
||||||
|
@HiveField(29)
|
||||||
|
chevronRight,
|
||||||
|
@HiveField(30)
|
||||||
|
colon,
|
||||||
|
@HiveField(31)
|
||||||
|
semicolon,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VirtKeyX on VirtKey {
|
||||||
|
/// Used for input to terminal
|
||||||
|
String? get inputRaw => switch (this) {
|
||||||
|
VirtKey.slash => '/',
|
||||||
|
VirtKey.backSlash => '\\',
|
||||||
|
VirtKey.underscore => '_',
|
||||||
|
VirtKey.plus => '+',
|
||||||
|
VirtKey.equal => '=',
|
||||||
|
VirtKey.minus => '-',
|
||||||
|
VirtKey.parenLeft => '(',
|
||||||
|
VirtKey.parenRight => ')',
|
||||||
|
VirtKey.bracketLeft => '[',
|
||||||
|
VirtKey.bracketRight => ']',
|
||||||
|
VirtKey.braceLeft => '{',
|
||||||
|
VirtKey.braceRight => '}',
|
||||||
|
VirtKey.chevronLeft => '<',
|
||||||
|
VirtKey.chevronRight => '>',
|
||||||
|
VirtKey.colon => ':',
|
||||||
|
VirtKey.semicolon => ';',
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Used for displaying on UI
|
||||||
String get text {
|
String get text {
|
||||||
switch (this) {
|
final t = inputRaw;
|
||||||
case VirtKey.pgdn:
|
if (t != null) return t;
|
||||||
return 'PgDn';
|
|
||||||
case VirtKey.pgup:
|
if (this == VirtKey.pgdn) return 'PgDn';
|
||||||
return 'PgUp';
|
if (this == VirtKey.pgup) return 'PgUp';
|
||||||
default:
|
|
||||||
if (name.length > 1) {
|
if (name.length > 1) {
|
||||||
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final defaultOrder = [
|
/// Default order of virtual keys
|
||||||
|
static const defaultOrder = [
|
||||||
VirtKey.esc,
|
VirtKey.esc,
|
||||||
VirtKey.alt,
|
VirtKey.alt,
|
||||||
VirtKey.home,
|
VirtKey.home,
|
||||||
@@ -71,99 +132,56 @@ enum VirtKey {
|
|||||||
VirtKey.ime,
|
VirtKey.ime,
|
||||||
];
|
];
|
||||||
|
|
||||||
TerminalKey? get key {
|
/// Corresponding [TerminalKey]
|
||||||
switch (this) {
|
TerminalKey? get key => switch (this) {
|
||||||
case VirtKey.esc:
|
VirtKey.esc => TerminalKey.escape,
|
||||||
return TerminalKey.escape;
|
VirtKey.alt => TerminalKey.alt,
|
||||||
case VirtKey.alt:
|
VirtKey.home => TerminalKey.home,
|
||||||
return TerminalKey.alt;
|
VirtKey.up => TerminalKey.arrowUp,
|
||||||
case VirtKey.home:
|
VirtKey.end => TerminalKey.end,
|
||||||
return TerminalKey.home;
|
VirtKey.tab => TerminalKey.tab,
|
||||||
case VirtKey.up:
|
VirtKey.ctrl => TerminalKey.control,
|
||||||
return TerminalKey.arrowUp;
|
VirtKey.left => TerminalKey.arrowLeft,
|
||||||
case VirtKey.end:
|
VirtKey.down => TerminalKey.arrowDown,
|
||||||
return TerminalKey.end;
|
VirtKey.right => TerminalKey.arrowRight,
|
||||||
case VirtKey.tab:
|
VirtKey.pgup => TerminalKey.pageUp,
|
||||||
return TerminalKey.tab;
|
VirtKey.pgdn => TerminalKey.pageDown,
|
||||||
case VirtKey.ctrl:
|
_ => null,
|
||||||
return TerminalKey.control;
|
};
|
||||||
case VirtKey.left:
|
|
||||||
return TerminalKey.arrowLeft;
|
|
||||||
case VirtKey.down:
|
|
||||||
return TerminalKey.arrowDown;
|
|
||||||
case VirtKey.right:
|
|
||||||
return TerminalKey.arrowRight;
|
|
||||||
case VirtKey.pgup:
|
|
||||||
return TerminalKey.pageUp;
|
|
||||||
case VirtKey.pgdn:
|
|
||||||
return TerminalKey.pageDown;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData? get icon {
|
/// Icons for virtual keys
|
||||||
switch (this) {
|
IconData? get icon => switch (this) {
|
||||||
case VirtKey.up:
|
VirtKey.up => Icons.arrow_upward,
|
||||||
return Icons.arrow_upward;
|
VirtKey.left => Icons.arrow_back,
|
||||||
case VirtKey.left:
|
VirtKey.down => Icons.arrow_downward,
|
||||||
return Icons.arrow_back;
|
VirtKey.right => Icons.arrow_forward,
|
||||||
case VirtKey.down:
|
VirtKey.sftp => Icons.file_open,
|
||||||
return Icons.arrow_downward;
|
VirtKey.snippet => Icons.code,
|
||||||
case VirtKey.right:
|
VirtKey.clipboard => Icons.paste,
|
||||||
return Icons.arrow_forward;
|
VirtKey.ime => Icons.keyboard,
|
||||||
case VirtKey.sftp:
|
_ => null,
|
||||||
return Icons.file_open;
|
};
|
||||||
case VirtKey.snippet:
|
|
||||||
return Icons.code;
|
|
||||||
case VirtKey.clipboard:
|
|
||||||
return Icons.paste;
|
|
||||||
case VirtKey.ime:
|
|
||||||
return Icons.keyboard;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use [VirtualKeyFunc] instead of [VirtKey]
|
// Use [VirtualKeyFunc] instead of [VirtKey]
|
||||||
// This can help linter to enum all [VirtualKeyFunc]
|
// This can help linter to enum all [VirtualKeyFunc]
|
||||||
// and make sure all [VirtualKeyFunc] are handled
|
// and make sure all [VirtualKeyFunc] are handled
|
||||||
VirtualKeyFunc? get func {
|
VirtualKeyFunc? get func => switch (this) {
|
||||||
switch (this) {
|
VirtKey.sftp => VirtualKeyFunc.file,
|
||||||
case VirtKey.sftp:
|
VirtKey.snippet => VirtualKeyFunc.snippet,
|
||||||
return VirtualKeyFunc.file;
|
VirtKey.clipboard => VirtualKeyFunc.clipboard,
|
||||||
case VirtKey.snippet:
|
VirtKey.ime => VirtualKeyFunc.toggleIME,
|
||||||
return VirtualKeyFunc.snippet;
|
_ => null,
|
||||||
case VirtKey.clipboard:
|
};
|
||||||
return VirtualKeyFunc.clipboard;
|
|
||||||
case VirtKey.ime:
|
|
||||||
return VirtualKeyFunc.toggleIME;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get toggleable {
|
bool get toggleable => switch (this) {
|
||||||
switch (this) {
|
VirtKey.alt || VirtKey.ctrl => true,
|
||||||
case VirtKey.alt:
|
_ => false,
|
||||||
case VirtKey.ctrl:
|
};
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get canLongPress {
|
bool get canLongPress => switch (this) {
|
||||||
switch (this) {
|
VirtKey.up || VirtKey.left || VirtKey.down || VirtKey.right => true,
|
||||||
case VirtKey.up:
|
_ => false,
|
||||||
case VirtKey.left:
|
};
|
||||||
case VirtKey.down:
|
|
||||||
case VirtKey.right:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? get help => switch (this) {
|
String? get help => switch (this) {
|
||||||
VirtKey.sftp => l10n.virtKeyHelpSFTP,
|
VirtKey.sftp => l10n.virtKeyHelpSFTP,
|
||||||
@@ -171,6 +189,18 @@ enum VirtKey {
|
|||||||
VirtKey.ime => l10n.virtKeyHelpIME,
|
VirtKey.ime => l10n.virtKeyHelpIME,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
enum VirtualKeyFunc { toggleIME, backspace, clipboard, snippet, file }
|
/// - [saveDefaultIfErr] if the stored raw values is invalid, save default order to store
|
||||||
|
static List<VirtKey> loadFromStore({bool saveDefaultIfErr = true}) {
|
||||||
|
try {
|
||||||
|
final ints = Stores.setting.sshVirtKeys.fetch();
|
||||||
|
return ints.map((e) => VirtKey.values[e]).toList();
|
||||||
|
} on RangeError {
|
||||||
|
final ints = defaultOrder.map((e) => e.index).toList();
|
||||||
|
Stores.setting.sshVirtKeys.put(ints);
|
||||||
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to load sshVirtKeys', e, s);
|
||||||
|
}
|
||||||
|
return defaultOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,38 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
|
|||||||
return VirtKey.pgup;
|
return VirtKey.pgup;
|
||||||
case 15:
|
case 15:
|
||||||
return VirtKey.pgdn;
|
return VirtKey.pgdn;
|
||||||
|
case 16:
|
||||||
|
return VirtKey.slash;
|
||||||
|
case 17:
|
||||||
|
return VirtKey.backSlash;
|
||||||
|
case 18:
|
||||||
|
return VirtKey.underscore;
|
||||||
|
case 19:
|
||||||
|
return VirtKey.plus;
|
||||||
|
case 20:
|
||||||
|
return VirtKey.equal;
|
||||||
|
case 21:
|
||||||
|
return VirtKey.minus;
|
||||||
|
case 22:
|
||||||
|
return VirtKey.parenLeft;
|
||||||
|
case 23:
|
||||||
|
return VirtKey.parenRight;
|
||||||
|
case 24:
|
||||||
|
return VirtKey.bracketLeft;
|
||||||
|
case 25:
|
||||||
|
return VirtKey.bracketRight;
|
||||||
|
case 26:
|
||||||
|
return VirtKey.braceLeft;
|
||||||
|
case 27:
|
||||||
|
return VirtKey.braceRight;
|
||||||
|
case 28:
|
||||||
|
return VirtKey.chevronLeft;
|
||||||
|
case 29:
|
||||||
|
return VirtKey.chevronRight;
|
||||||
|
case 30:
|
||||||
|
return VirtKey.colon;
|
||||||
|
case 31:
|
||||||
|
return VirtKey.semicolon;
|
||||||
default:
|
default:
|
||||||
return VirtKey.esc;
|
return VirtKey.esc;
|
||||||
}
|
}
|
||||||
@@ -101,6 +133,54 @@ class VirtKeyAdapter extends TypeAdapter<VirtKey> {
|
|||||||
case VirtKey.pgdn:
|
case VirtKey.pgdn:
|
||||||
writer.writeByte(15);
|
writer.writeByte(15);
|
||||||
break;
|
break;
|
||||||
|
case VirtKey.slash:
|
||||||
|
writer.writeByte(16);
|
||||||
|
break;
|
||||||
|
case VirtKey.backSlash:
|
||||||
|
writer.writeByte(17);
|
||||||
|
break;
|
||||||
|
case VirtKey.underscore:
|
||||||
|
writer.writeByte(18);
|
||||||
|
break;
|
||||||
|
case VirtKey.plus:
|
||||||
|
writer.writeByte(19);
|
||||||
|
break;
|
||||||
|
case VirtKey.equal:
|
||||||
|
writer.writeByte(20);
|
||||||
|
break;
|
||||||
|
case VirtKey.minus:
|
||||||
|
writer.writeByte(21);
|
||||||
|
break;
|
||||||
|
case VirtKey.parenLeft:
|
||||||
|
writer.writeByte(22);
|
||||||
|
break;
|
||||||
|
case VirtKey.parenRight:
|
||||||
|
writer.writeByte(23);
|
||||||
|
break;
|
||||||
|
case VirtKey.bracketLeft:
|
||||||
|
writer.writeByte(24);
|
||||||
|
break;
|
||||||
|
case VirtKey.bracketRight:
|
||||||
|
writer.writeByte(25);
|
||||||
|
break;
|
||||||
|
case VirtKey.braceLeft:
|
||||||
|
writer.writeByte(26);
|
||||||
|
break;
|
||||||
|
case VirtKey.braceRight:
|
||||||
|
writer.writeByte(27);
|
||||||
|
break;
|
||||||
|
case VirtKey.chevronLeft:
|
||||||
|
writer.writeByte(28);
|
||||||
|
break;
|
||||||
|
case VirtKey.chevronRight:
|
||||||
|
writer.writeByte(29);
|
||||||
|
break;
|
||||||
|
case VirtKey.colon:
|
||||||
|
writer.writeByte(30);
|
||||||
|
break;
|
||||||
|
case VirtKey.semicolon:
|
||||||
|
writer.writeByte(31);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
@@ -18,12 +19,14 @@ class PrivateKeyProvider extends Provider {
|
|||||||
pkis.value.add(info);
|
pkis.value.add(info);
|
||||||
pkis.notify();
|
pkis.notify();
|
||||||
Stores.key.put(info);
|
Stores.key.put(info);
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(PrivateKeyInfo info) {
|
static void delete(PrivateKeyInfo info) {
|
||||||
pkis.value.removeWhere((e) => e.id == info.id);
|
pkis.value.removeWhere((e) => e.id == info.id);
|
||||||
pkis.notify();
|
pkis.notify();
|
||||||
Stores.key.delete(info);
|
Stores.key.delete(info);
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
static void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
||||||
@@ -37,5 +40,6 @@ class PrivateKeyProvider extends Provider {
|
|||||||
Stores.key.put(newInfo);
|
Stores.key.put(newInfo);
|
||||||
}
|
}
|
||||||
pkis.notify();
|
pkis.notify();
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:computer/computer.dart';
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
import 'package:server_box/core/extension/ssh_client.dart';
|
||||||
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
import 'package:server_box/core/utils/ssh_auth.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/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
@@ -87,16 +88,15 @@ class ServerProvider extends Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void _updateTags() {
|
static void _updateTags() {
|
||||||
|
final tags = <String>{};
|
||||||
for (final s in servers.values) {
|
for (final s in servers.values) {
|
||||||
final tags = s.value.spi.tags;
|
final spiTags = s.value.spi.tags;
|
||||||
if (tags == null) continue;
|
if (spiTags == null) continue;
|
||||||
for (final t in tags) {
|
for (final t in spiTags) {
|
||||||
if (!_tags.value.contains(t)) {
|
tags.add(t);
|
||||||
_tags.value.add(t);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_tags.value = (_tags.value.toList()..sort()).toSet();
|
_tags.value = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Server genServer(Spi spi) {
|
static Server genServer(Spi spi) {
|
||||||
@@ -190,6 +190,7 @@ class ServerProvider extends Provider {
|
|||||||
Stores.setting.serverOrder.put(serverOrder.value);
|
Stores.setting.serverOrder.put(serverOrder.value);
|
||||||
_updateTags();
|
_updateTags();
|
||||||
refresh(spi: spi);
|
refresh(spi: spi);
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delServer(String id) {
|
static void delServer(String id) {
|
||||||
@@ -199,6 +200,7 @@ class ServerProvider extends Provider {
|
|||||||
Stores.setting.serverOrder.put(serverOrder.value);
|
Stores.setting.serverOrder.put(serverOrder.value);
|
||||||
Stores.server.delete(id);
|
Stores.server.delete(id);
|
||||||
_updateTags();
|
_updateTags();
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void deleteAll() {
|
static void deleteAll() {
|
||||||
@@ -234,6 +236,7 @@ class ServerProvider extends Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_updateTags();
|
_updateTags();
|
||||||
|
bakSync.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _setServerState(VNode<Server> s, ServerConn ss) {
|
static void _setServerState(VNode<Server> s, ServerConn ss) {
|
||||||
@@ -302,11 +305,10 @@ class ServerProvider extends Provider {
|
|||||||
|
|
||||||
_setServerState(s, ServerConn.connected);
|
_setServerState(s, ServerConn.connected);
|
||||||
|
|
||||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final (_, writeScriptResult) = await sv.client!.exec(
|
final (_, writeScriptResult) = await sv.client!.exec(
|
||||||
(session) async {
|
(session) async {
|
||||||
|
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||||
session.stdin.add(scriptRaw);
|
session.stdin.add(scriptRaw);
|
||||||
session.stdin.close();
|
session.stdin.close();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ class SnippetProvider extends Provider {
|
|||||||
snippets.notify();
|
snippets.notify();
|
||||||
Stores.snippet.put(snippet);
|
Stores.snippet.put(snippet);
|
||||||
_updateTags();
|
_updateTags();
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void del(Snippet snippet) {
|
static void del(Snippet snippet) {
|
||||||
@@ -51,6 +53,7 @@ class SnippetProvider extends Provider {
|
|||||||
snippets.notify();
|
snippets.notify();
|
||||||
Stores.snippet.delete(snippet);
|
Stores.snippet.delete(snippet);
|
||||||
_updateTags();
|
_updateTags();
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void update(Snippet old, Snippet newOne) {
|
static void update(Snippet old, Snippet newOne) {
|
||||||
@@ -60,6 +63,7 @@ class SnippetProvider extends Provider {
|
|||||||
Stores.snippet.delete(old);
|
Stores.snippet.delete(old);
|
||||||
Stores.snippet.put(newOne);
|
Stores.snippet.put(newOne);
|
||||||
_updateTags();
|
_updateTags();
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void renameTag(String old, String newOne) {
|
static void renameTag(String old, String newOne) {
|
||||||
@@ -71,5 +75,6 @@ class SnippetProvider extends Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_updateTags();
|
_updateTags();
|
||||||
|
bakSync.sync(milliDelay: 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ final class SystemdProvider {
|
|||||||
final isBusy = false.vn;
|
final isBusy = false.vn;
|
||||||
final units = <SystemdUnit>[].vn;
|
final units = <SystemdUnit>[].vn;
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
isBusy.dispose();
|
||||||
|
units.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getUnits() async {
|
Future<void> getUnits() async {
|
||||||
isBusy.value = true;
|
isBusy.value = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// This file is generated by fl_build. Do not edit.
|
// This file is generated by fl_build. Do not edit.
|
||||||
// ignore_for_file: prefer_single_quotes
|
// ignore_for_file: prefer_single_quotes
|
||||||
|
|
||||||
class BuildData {
|
abstract class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 1070;
|
static const int build = 1104;
|
||||||
static const int script = 58;
|
static const int script = 58;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ abstract final class GithubIds {
|
|||||||
'Liloupar',
|
'Liloupar',
|
||||||
'dccif',
|
'dccif',
|
||||||
'mikropsoft',
|
'mikropsoft',
|
||||||
|
'CakesTwix',
|
||||||
};
|
};
|
||||||
|
|
||||||
static const participants = <GhId>{
|
static const participants = <GhId>{
|
||||||
|
|||||||
@@ -1,35 +1,38 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.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/no_backup.dart';
|
||||||
import 'package:server_box/data/store/private_key.dart';
|
import 'package:server_box/data/store/private_key.dart';
|
||||||
import 'package:server_box/data/store/server.dart';
|
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';
|
||||||
|
|
||||||
abstract final class Stores {
|
abstract final class Stores {
|
||||||
static final setting = SettingStore();
|
static final setting = SettingStore.instance;
|
||||||
static final server = ServerStore();
|
static final server = ServerStore.instance;
|
||||||
static final container = ContainerStore();
|
static final container = ContainerStore.instance;
|
||||||
static final history = HistoryStore();
|
static final key = PrivateKeyStore.instance;
|
||||||
static final key = PrivateKeyStore();
|
static final snippet = SnippetStore.instance;
|
||||||
static final snippet = SnippetStore();
|
static final history = HistoryStore.instance;
|
||||||
|
|
||||||
static final List<PersistentStore> all = [
|
/// All stores that need backup
|
||||||
setting,
|
static final List<PersistentStore> _allBackup = [
|
||||||
server,
|
SettingStore.instance,
|
||||||
container,
|
ServerStore.instance,
|
||||||
history,
|
ContainerStore.instance,
|
||||||
key,
|
PrivateKeyStore.instance,
|
||||||
snippet,
|
SnippetStore.instance,
|
||||||
|
HistoryStore.instance,
|
||||||
];
|
];
|
||||||
|
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
await Future.wait(all.map((store) => store.init()));
|
await Future.wait(_allBackup.map((store) => store.init()));
|
||||||
|
await NoBackupStore.instance.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
static int? get lastModTime {
|
static int? get lastModTime {
|
||||||
int? lastModTime = 0;
|
int? lastModTime = 0;
|
||||||
for (final store in all) {
|
for (final store in _allBackup) {
|
||||||
final last = store.box.lastModified ?? 0;
|
final last = store.box.lastModified ?? 0;
|
||||||
if (last > (lastModTime ?? 0)) {
|
if (last > (lastModTime ?? 0)) {
|
||||||
lastModTime = last;
|
lastModTime = last;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import 'package:server_box/data/res/store.dart';
|
|||||||
const _keyConfig = 'providerConfig';
|
const _keyConfig = 'providerConfig';
|
||||||
|
|
||||||
class ContainerStore extends PersistentStore {
|
class ContainerStore extends PersistentStore {
|
||||||
ContainerStore() : super('docker');
|
ContainerStore._() : super('docker');
|
||||||
|
|
||||||
|
static final instance = ContainerStore._();
|
||||||
|
|
||||||
String? fetch(String? id) {
|
String? fetch(String? id) {
|
||||||
return box.get(id);
|
return box.get(id);
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ class _MapHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class HistoryStore extends PersistentStore {
|
class HistoryStore extends PersistentStore {
|
||||||
HistoryStore() : super('history');
|
HistoryStore._() : super('history');
|
||||||
|
|
||||||
|
static final instance = HistoryStore._();
|
||||||
|
|
||||||
/// Paths that user has visited by 'Locate' button
|
/// Paths that user has visited by 'Locate' button
|
||||||
late final sftpGoPath = _ListHistory(box: box, name: 'sftpPath');
|
late final sftpGoPath = _ListHistory(box: box, name: 'sftpPath');
|
||||||
|
|||||||
49
lib/data/store/no_backup.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
|
final class NoBackupStore extends PersistentStore {
|
||||||
|
NoBackupStore._() : super('no_backup');
|
||||||
|
|
||||||
|
static final instance = NoBackupStore._();
|
||||||
|
|
||||||
|
/// Only valid on iOS and macOS
|
||||||
|
late final icloudSync = property('icloudSync', false);
|
||||||
|
|
||||||
|
/// Webdav sync
|
||||||
|
late final webdavSync = property('webdavSync', false);
|
||||||
|
late final webdavUrl = property('webdavUrl', '');
|
||||||
|
late final webdavUser = property('webdavUser', '');
|
||||||
|
late final webdavPwd = property('webdavPwd', '');
|
||||||
|
|
||||||
|
void migrate() {
|
||||||
|
if (BuildData.build > 1076) return;
|
||||||
|
|
||||||
|
final settings = Stores.setting;
|
||||||
|
final icloudSync_ = settings.box.get('icloudSync');
|
||||||
|
if (icloudSync_ is bool) {
|
||||||
|
icloudSync.put(icloudSync_);
|
||||||
|
settings.box.delete('icloudSync');
|
||||||
|
}
|
||||||
|
final webdavSync_ = settings.box.get('webdavSync');
|
||||||
|
if (webdavSync_ is bool) {
|
||||||
|
webdavSync.put(webdavSync_);
|
||||||
|
settings.box.delete('webdavSync');
|
||||||
|
}
|
||||||
|
final webdavUrl_ = settings.box.get('webdavUrl');
|
||||||
|
if (webdavUrl_ is String) {
|
||||||
|
webdavUrl.put(webdavUrl_);
|
||||||
|
settings.box.delete('webdavUrl');
|
||||||
|
}
|
||||||
|
final webdavUser_ = settings.box.get('webdavUser');
|
||||||
|
if (webdavUser_ is String) {
|
||||||
|
webdavUser.put(webdavUser_);
|
||||||
|
settings.box.delete('webdavUser');
|
||||||
|
}
|
||||||
|
final webdavPwd_ = settings.box.get('webdavPwd');
|
||||||
|
if (webdavPwd_ is String) {
|
||||||
|
webdavPwd.put(webdavPwd_);
|
||||||
|
settings.box.delete('webdavPwd');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||||
|
|
||||||
class PrivateKeyStore extends PersistentStore {
|
class PrivateKeyStore extends PersistentStore {
|
||||||
PrivateKeyStore() : super('key');
|
PrivateKeyStore._() : super('key');
|
||||||
|
|
||||||
|
static final instance = PrivateKeyStore._();
|
||||||
|
|
||||||
void put(PrivateKeyInfo info) {
|
void put(PrivateKeyInfo info) {
|
||||||
box.put(info.id, info);
|
box.put(info.id, info);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
|
||||||
class ServerStore extends PersistentStore {
|
class ServerStore extends PersistentStore {
|
||||||
ServerStore() : super('server');
|
ServerStore._() : super('server');
|
||||||
|
|
||||||
|
static final instance = ServerStore._();
|
||||||
|
|
||||||
void put(Spi info) {
|
void put(Spi info) {
|
||||||
box.put(info.id, info);
|
box.put(info.id, info);
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import 'package:server_box/data/model/app/net_view.dart';
|
|||||||
import 'package:server_box/data/res/default.dart';
|
import 'package:server_box/data/res/default.dart';
|
||||||
|
|
||||||
class SettingStore extends PersistentStore {
|
class SettingStore extends PersistentStore {
|
||||||
SettingStore() : super('setting');
|
SettingStore._() : super('setting');
|
||||||
|
|
||||||
|
static final instance = SettingStore._();
|
||||||
|
|
||||||
// ------BEGIN------
|
// ------BEGIN------
|
||||||
//
|
//
|
||||||
@@ -104,7 +106,7 @@ class SettingStore extends PersistentStore {
|
|||||||
|
|
||||||
late final sshVirtKeys = listProperty(
|
late final sshVirtKeys = listProperty(
|
||||||
'sshVirtKeys',
|
'sshVirtKeys',
|
||||||
VirtKey.defaultOrder.map((e) => e.index).toList(),
|
VirtKeyX.defaultOrder.map((e) => e.index).toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
late final netViewType = property('netViewType', NetViewType.speed);
|
late final netViewType = property('netViewType', NetViewType.speed);
|
||||||
@@ -125,9 +127,6 @@ class SettingStore extends PersistentStore {
|
|||||||
/// 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 = property('useSystemPrimaryColor', false);
|
late final useSystemPrimaryColor = property('useSystemPrimaryColor', false);
|
||||||
|
|
||||||
/// Only valid on iOS and macOS
|
|
||||||
late final icloudSync = property('icloudSync', false);
|
|
||||||
|
|
||||||
/// Only valid on iOS / Android / Windows
|
/// Only valid on iOS / Android / Windows
|
||||||
late final useBioAuth = property('useBioAuth', false);
|
late final useBioAuth = property('useBioAuth', false);
|
||||||
|
|
||||||
@@ -143,12 +142,6 @@ class SettingStore extends PersistentStore {
|
|||||||
/// Show tip of suspend
|
/// Show tip of suspend
|
||||||
late final showSuspendTip = property('showSuspendTip', true);
|
late final showSuspendTip = property('showSuspendTip', true);
|
||||||
|
|
||||||
/// Webdav sync
|
|
||||||
late final webdavSync = property('webdavSync', false);
|
|
||||||
late final webdavUrl = property('webdavUrl', '', updateLastModified: false);
|
|
||||||
late final webdavUser = property('webdavUser', '', updateLastModified: false);
|
|
||||||
late final webdavPwd = property('webdavPwd', '', updateLastModified: false);
|
|
||||||
|
|
||||||
/// Whether collapse UI items by default
|
/// Whether collapse UI items by default
|
||||||
late final collapseUIDefault = property('collapseUIDefault', true);
|
late final collapseUIDefault = property('collapseUIDefault', true);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
|
|
||||||
class SnippetStore extends PersistentStore {
|
class SnippetStore extends PersistentStore {
|
||||||
SnippetStore() : super('snippet');
|
SnippetStore._() : super('snippet');
|
||||||
|
|
||||||
|
static final instance = SnippetStore._();
|
||||||
|
|
||||||
void put(Snippet snippet) {
|
void put(Snippet snippet) {
|
||||||
box.put(snippet.name, snippet);
|
box.put(snippet.name, snippet);
|
||||||
|
|||||||
@@ -70,14 +70,9 @@ final class _IntroPage extends StatelessWidget {
|
|||||||
).cardx,
|
).cardx,
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(MingCute.chart_line_line, size: _kIconSize),
|
leading: const Icon(MingCute.chart_line_line, size: _kIconSize),
|
||||||
title: TipText(l10n.stat, l10n.parseContainerStatsTip),
|
title: TipText('Docker ${l10n.stat}', l10n.parseContainerStatsTip),
|
||||||
trailing: StoreSwitch(prop: _setting.containerParseStat),
|
trailing: StoreSwitch(prop: _setting.containerParseStat),
|
||||||
).cardx,
|
).cardx,
|
||||||
ListTile(
|
|
||||||
leading: const Icon(OctIcons.cpu),
|
|
||||||
title: TipText(l10n.noLineChartForCpu, l10n.cpuViewAsProgressTip),
|
|
||||||
trailing: StoreSwitch(prop: _setting.cpuViewAsProgress),
|
|
||||||
).cardx,
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Bootstrap.alphabet),
|
leading: const Icon(Bootstrap.alphabet),
|
||||||
title: TipText(l10n.letterCache, l10n.letterCacheTip),
|
title: TipText(l10n.letterCache, l10n.letterCacheTip),
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Routen",
|
"route": "Routen",
|
||||||
"run": "Ausführen",
|
"run": "Ausführen",
|
||||||
"running": "läuft",
|
"running": "läuft",
|
||||||
|
"sameIdServerExist": "Ein Server mit derselben ID existiert bereits",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"saved": "Gerettet",
|
"saved": "Gerettet",
|
||||||
"second": "s",
|
"second": "s",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Routing",
|
"route": "Routing",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"running": "Running",
|
"running": "Running",
|
||||||
|
"sameIdServerExist": "A server with the same ID already exists",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"second": "s",
|
"second": "s",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Enrutamiento",
|
"route": "Enrutamiento",
|
||||||
"run": "Ejecutar",
|
"run": "Ejecutar",
|
||||||
"running": "En ejecución",
|
"running": "En ejecución",
|
||||||
|
"sameIdServerExist": "Ya existe un servidor con el mismo ID",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"saved": "Guardado",
|
"saved": "Guardado",
|
||||||
"second": "Segundo",
|
"second": "Segundo",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Routage",
|
"route": "Routage",
|
||||||
"run": "Exécuter",
|
"run": "Exécuter",
|
||||||
"running": "En cours d'exécution",
|
"running": "En cours d'exécution",
|
||||||
|
"sameIdServerExist": "Un serveur avec le même ID existe déjà",
|
||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
"saved": "Enregistré",
|
"saved": "Enregistré",
|
||||||
"second": "s",
|
"second": "s",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Routing",
|
"route": "Routing",
|
||||||
"run": "Berlari",
|
"run": "Berlari",
|
||||||
"running": "berlari",
|
"running": "berlari",
|
||||||
|
"sameIdServerExist": "Server dengan ID yang sama sudah ada",
|
||||||
"save": "Menyimpan",
|
"save": "Menyimpan",
|
||||||
"saved": "Diselamatkan",
|
"saved": "Diselamatkan",
|
||||||
"second": "S",
|
"second": "S",
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
"license": "オープンソースライセンス",
|
"license": "オープンソースライセンス",
|
||||||
"location": "場所",
|
"location": "場所",
|
||||||
"loss": "パケットロス",
|
"loss": "パケットロス",
|
||||||
"madeWithLove": "❤️で作成された {myGithub}",
|
"madeWithLove": "{myGithub}によって❤️で作成済み",
|
||||||
"manual": "マニュアル",
|
"manual": "マニュアル",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
"maxRetryCount": "サーバーの再接続試行回数",
|
"maxRetryCount": "サーバーの再接続試行回数",
|
||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "ルーティング",
|
"route": "ルーティング",
|
||||||
"run": "実行",
|
"run": "実行",
|
||||||
"running": "実行中",
|
"running": "実行中",
|
||||||
|
"sameIdServerExist": "同じIDのサーバーが既に存在します",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saved": "保存されました",
|
"saved": "保存されました",
|
||||||
"second": "秒",
|
"second": "秒",
|
||||||
@@ -164,7 +165,7 @@
|
|||||||
"specifyDev": "デバイスを指定",
|
"specifyDev": "デバイスを指定",
|
||||||
"specifyDevTip": "例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。",
|
"specifyDevTip": "例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。",
|
||||||
"speed": "速度",
|
"speed": "速度",
|
||||||
"spentTime": "時間を費やしました: {time}",
|
"spentTime": "費した時間: {time}",
|
||||||
"sshTermHelp": "ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。",
|
"sshTermHelp": "ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。",
|
||||||
"sshTip": "この機能は現在テスト段階にあります。\n\n問題がある場合は、{url}でフィードバックしてください。",
|
"sshTip": "この機能は現在テスト段階にあります。\n\n問題がある場合は、{url}でフィードバックしてください。",
|
||||||
"sshVirtualKeyAutoOff": "仮想キーの自動オフ",
|
"sshVirtualKeyAutoOff": "仮想キーの自動オフ",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Route",
|
"route": "Route",
|
||||||
"run": "Uitvoeren",
|
"run": "Uitvoeren",
|
||||||
"running": "Uitgevoerd",
|
"running": "Uitgevoerd",
|
||||||
|
"sameIdServerExist": "Er bestaat al een server met dezelfde ID",
|
||||||
"save": "Opslaan",
|
"save": "Opslaan",
|
||||||
"saved": "Opgeslagen",
|
"saved": "Opgeslagen",
|
||||||
"second": "s",
|
"second": "s",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Roteamento",
|
"route": "Roteamento",
|
||||||
"run": "Executar",
|
"run": "Executar",
|
||||||
"running": "Executando",
|
"running": "Executando",
|
||||||
|
"sameIdServerExist": "Já existe um servidor com o mesmo ID",
|
||||||
"save": "Salvar",
|
"save": "Salvar",
|
||||||
"saved": "Salvo",
|
"saved": "Salvo",
|
||||||
"second": "Segundo",
|
"second": "Segundo",
|
||||||
|
|||||||
@@ -3,77 +3,77 @@
|
|||||||
"aboutThanks": "Благодарности всем участникам.",
|
"aboutThanks": "Благодарности всем участникам.",
|
||||||
"acceptBeta": "Принять обновления тестовой версии",
|
"acceptBeta": "Принять обновления тестовой версии",
|
||||||
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
|
||||||
"added2List": "добавлено в список задач",
|
"added2List": "Добавлено в список задач",
|
||||||
"addr": "Адрес",
|
"addr": "Адрес",
|
||||||
"alreadyLastDir": "Уже в корневом каталоге",
|
"alreadyLastDir": "Уже в корневом каталоге",
|
||||||
"authFailTip": "Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.",
|
"authFailTip": "Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.",
|
||||||
"autoBackupConflict": "Может быть включено только одно автоматическое резервное копирование",
|
"autoBackupConflict": "Может быть включено только одно автоматическое резервное копирование",
|
||||||
"autoConnect": "автоматическое подключение",
|
"autoConnect": "Автоматическое подключение",
|
||||||
"autoRun": "автозапуск",
|
"autoRun": "Автозапуск",
|
||||||
"autoUpdateHomeWidget": "автоматическое обновление виджета на главном экране",
|
"autoUpdateHomeWidget": "Автоматическое обновление виджета на главном экране",
|
||||||
"backupTip": "Экспортированные данные зашифрованы простым способом, пожалуйста, храните их в безопасности.",
|
"backupTip": "Экспортированные данные зашифрованы простым способом \nПожалуйста, храните их в безопасности.",
|
||||||
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
|
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
|
||||||
"battery": "батарея",
|
"battery": "Батарея",
|
||||||
"bgRun": "работа в фоновом режиме",
|
"bgRun": "Работа в фоновом режиме",
|
||||||
"bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените стратегию энергосбережения на «Без ограничений».",
|
"bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».",
|
||||||
"cmd": "команда",
|
"cmd": "Команда",
|
||||||
"collapseUITip": "Свернуть длинные списки в UI по умолчанию",
|
"collapseUITip": "Свернуть длинные списки в UI по умолчанию",
|
||||||
"conn": "подключение",
|
"conn": "Подключение",
|
||||||
"container": "контейнер",
|
"container": "Контейнер",
|
||||||
"containerTrySudoTip": "Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию",
|
"containerTrySudoTip": "Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию",
|
||||||
"convert": "конвертировать",
|
"convert": "Конвертировать",
|
||||||
"copyPath": "копировать путь",
|
"copyPath": "Копировать путь",
|
||||||
"cpuViewAsProgressTip": "Отобразите уровень использования каждого процессора в виде индикатора выполнения (старый стиль)",
|
"cpuViewAsProgressTip": "Отобразите уровень использования каждого процессора в виде индикатора выполнения (старый стиль)",
|
||||||
"cursorType": "Тип курсора",
|
"cursorType": "Тип курсора",
|
||||||
"customCmd": "Пользовательские команды",
|
"customCmd": "Пользовательские команды",
|
||||||
"customCmdDocUrl": "https://github.com/lollipopkit/flutter_server_box/wiki#custom-commands",
|
"customCmdDocUrl": "https://github.com/lollipopkit/flutter_server_box/wiki#custom-commands",
|
||||||
"customCmdHint": "\"Имя команды\": \"Команда\"",
|
"customCmdHint": "\"Имя команды\": \"Команда\"",
|
||||||
"decode": "декодировать",
|
"decode": "Декодировать",
|
||||||
"decompress": "разархивировать",
|
"decompress": "Разархивировать",
|
||||||
"deleteServers": "удалить серверы пакетно",
|
"deleteServers": "Удалить серверы пакетно",
|
||||||
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
|
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
|
||||||
"disconnected": "отключено",
|
"disconnected": "Отключено",
|
||||||
"disk": "диск",
|
"disk": "Диск",
|
||||||
"diskIgnorePath": "путь игнорирования диска",
|
"diskIgnorePath": "Игнорировать путь к диску",
|
||||||
"displayCpuIndex": "Показать индекс ЦПУ",
|
"displayCpuIndex": "Отобразить индекс ЦП",
|
||||||
"dl2Local": "Загрузить {fileName} на локальный диск?",
|
"dl2Local": "Загрузить {fileName} на локальный диск?",
|
||||||
"dockerEmptyRunningItems": "Нет запущенных контейнеров.\nЭто может быть из-за:\n- пользователя Docker, отличного от пользователя, настроенного в приложении\n- переменной окружения DOCKER_HOST, которая не была правильно считана. Вы можете выполнить `echo $DOCKER_HOST` в терминале, чтобы увидеть ее значение.",
|
"dockerEmptyRunningItems": "Нет запущенных контейнеров.\nЭто может быть из-за:\n- пользователя Docker, отличного от пользователя, настроенного в приложении\n- переменной окружения DOCKER_HOST, которая не была правильно считана. Вы можете выполнить `echo $DOCKER_HOST` в терминале, чтобы увидеть ее значение.",
|
||||||
"dockerImagesFmt": "Всего {count} образов",
|
"dockerImagesFmt": "Всего {count} образов",
|
||||||
"dockerNotInstalled": "Docker не установлен",
|
"dockerNotInstalled": "Docker не установлен",
|
||||||
"dockerStatusRunningAndStoppedFmt": "{runningCount} запущено, {stoppedCount} остановлено",
|
"dockerStatusRunningAndStoppedFmt": "{runningCount} запущено, {stoppedCount} остановлено",
|
||||||
"dockerStatusRunningFmt": "{count} контейнеров запущено",
|
"dockerStatusRunningFmt": "{count} контейнеров запущено",
|
||||||
"doubleColumnMode": "режим двойной колонки",
|
"doubleColumnMode": "Режим двойной колонки",
|
||||||
"doubleColumnTip": "Эта опция лишь включает функцию; фактическое применение зависит от ширины устройства",
|
"doubleColumnTip": "Эта опция лишь включает функцию; фактическое применение зависит от ширины устройства",
|
||||||
"editVirtKeys": "редактировать виртуальные клавиши",
|
"editVirtKeys": "Редактировать виртуальные клавиши",
|
||||||
"editor": "редактор",
|
"editor": "Редактор",
|
||||||
"editorHighlightTip": "Текущая производительность подсветки кода неудовлетворительна, можно отключить для улучшения.",
|
"editorHighlightTip": "Текущая производительность подсветки кода неудовлетворительна, можно отключить для улучшения.",
|
||||||
"encode": "кодировать",
|
"encode": "Кодировать",
|
||||||
"envVars": "Переменная окружения",
|
"envVars": "Переменная окружения",
|
||||||
"experimentalFeature": "экспериментальная функция",
|
"experimentalFeature": "Экспериментальная функция",
|
||||||
"extraArgs": "дополнительные аргументы",
|
"extraArgs": "Дополнительные аргументы",
|
||||||
"fallbackSshDest": "Резервное место назначения SSH",
|
"fallbackSshDest": "Резервное место назначения SSH",
|
||||||
"fdroidReleaseTip": "Если вы скачали это приложение с F-Droid, рекомендуется отключить эту опцию.",
|
"fdroidReleaseTip": "Если вы скачали это приложение с F-Droid, рекомендуется отключить эту опцию.",
|
||||||
"fileTooLarge": "Файл '{file}' слишком большой '{size}', превышает {sizeMax}",
|
"fileTooLarge": "Файл '{file}' слишком большой '{size}', превышает {sizeMax}",
|
||||||
"followSystem": "следовать за системой",
|
"followSystem": "Следовать за системой",
|
||||||
"font": "шрифт",
|
"font": "Шрифт",
|
||||||
"fontSize": "размер шрифта",
|
"fontSize": "Размер шрифта",
|
||||||
"force": "принудительно",
|
"force": "Принудительно",
|
||||||
"fullScreen": "полноэкранный режим",
|
"fullScreen": "Полноэкранный режим",
|
||||||
"fullScreenJitter": "дрожание в полноэкранном режиме",
|
"fullScreenJitter": "Вибрация в полноэкранном режиме",
|
||||||
"fullScreenJitterHelp": "предотвращение выгорания экрана",
|
"fullScreenJitterHelp": "Предотвращение выгорания экрана",
|
||||||
"fullScreenTip": "Следует ли включить полноэкранный режим, когда устройство поворачивается в альбомный режим? Эта опция применяется только к вкладке сервера.",
|
"fullScreenTip": "Следует ли включить полноэкранный режим, когда устройство поворачивается в альбомный режим? Эта опция применяется только к вкладке сервера.",
|
||||||
"goBackQ": "Вернуться?",
|
"goBackQ": "Вернуться?",
|
||||||
"goto": "перейти к",
|
"goto": "Перейти к",
|
||||||
"hideTitleBar": "Скрыть заголовок",
|
"hideTitleBar": "Скрыть заголовок",
|
||||||
"highlight": "подсветка кода",
|
"highlight": "Подсветка кода",
|
||||||
"homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана",
|
"homeWidgetUrlConfig": "Конфигурация URL виджета домашнего экрана",
|
||||||
"host": "хост",
|
"host": "Хост",
|
||||||
"httpFailedWithCode": "Ошибка запроса, код: {code}",
|
"httpFailedWithCode": "ошибка запроса, код: {code}",
|
||||||
"ignoreCert": "Игнорировать сертификат",
|
"ignoreCert": "Игнорировать сертификат",
|
||||||
"image": "образ",
|
"image": "Образ",
|
||||||
"imagesList": "список образов",
|
"imagesList": "Список образов",
|
||||||
"init": "Инициализировать",
|
"init": "Инициализировать",
|
||||||
"inner": "встроенный",
|
"inner": "Встроенный",
|
||||||
"install": "установить",
|
"install": "установить",
|
||||||
"installDockerWithUrl": "Пожалуйста, сначала установите Docker по адресу https://docs.docker.com/engine/install",
|
"installDockerWithUrl": "Пожалуйста, сначала установите Docker по адресу https://docs.docker.com/engine/install",
|
||||||
"invalid": "Недействительный",
|
"invalid": "Недействительный",
|
||||||
@@ -81,135 +81,136 @@
|
|||||||
"keepForeground": "Пожалуйста, держите приложение в фокусе!",
|
"keepForeground": "Пожалуйста, держите приложение в фокусе!",
|
||||||
"keepStatusWhenErr": "Сохранять статус сервера при ошибке",
|
"keepStatusWhenErr": "Сохранять статус сервера при ошибке",
|
||||||
"keepStatusWhenErrTip": "Применимо только в случае ошибки выполнения скрипта",
|
"keepStatusWhenErrTip": "Применимо только в случае ошибки выполнения скрипта",
|
||||||
"keyAuth": "аутентификация по ключу",
|
"keyAuth": "Аутентификация по ключу",
|
||||||
"letterCache": "Кэширование букв",
|
"letterCache": "Кэширование букв",
|
||||||
"letterCacheTip": "Рекомендуется отключить, но после отключения будет невозможно вводить символы CJK.",
|
"letterCacheTip": "Рекомендуется отключить, но после отключения будет невозможно вводить символы CJK.",
|
||||||
"license": "лицензия",
|
"license": "Лицензия",
|
||||||
"location": "местоположение",
|
"location": "Местоположение",
|
||||||
"loss": "потери пакетов",
|
"loss": "Потери пакетов",
|
||||||
"madeWithLove": "Создано с ❤️ by {myGithub}",
|
"madeWithLove": "Создано с ❤️ by {myGithub}",
|
||||||
"manual": "вручную",
|
"manual": "Вручную",
|
||||||
"max": "максимум",
|
"max": "максимум",
|
||||||
"maxRetryCount": "максимальное количество попыток переподключения к серверу",
|
"maxRetryCount": "Максимальное количество попыток переподключения к серверу",
|
||||||
"maxRetryCountEqual0": "будет бесконечно пытаться переподключиться",
|
"maxRetryCountEqual0": "Будет бесконечно пытаться переподключиться",
|
||||||
"min": "минимум",
|
"min": "минимум",
|
||||||
"mission": "задача",
|
"mission": "Задача",
|
||||||
"more": "больше",
|
"more": "Больше",
|
||||||
"moveOutServerFuncBtnsHelp": "Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.",
|
"moveOutServerFuncBtnsHelp": "Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.",
|
||||||
"ms": "мс",
|
"ms": "мс",
|
||||||
"needHomeDir": "Если вы пользователь Synology, [смотрите здесь](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Пользователям других систем нужно искать, как создать домашний каталог.",
|
"needHomeDir": "Если вы пользователь Synology, [смотрите здесь](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Пользователям других систем нужно искать, как создать домашний каталог.",
|
||||||
"needRestart": "требуется перезапуск приложения",
|
"needRestart": "Требуется перезапуск приложения",
|
||||||
"net": "сеть",
|
"net": "Сеть",
|
||||||
"netViewType": "тип визуализации сети",
|
"netViewType": "Тип визуализации сети",
|
||||||
"newContainer": "создать контейнер",
|
"newContainer": "Создать контейнер",
|
||||||
"noLineChart": "Не использовать линейные графики",
|
"noLineChart": "Не использовать линейные графики",
|
||||||
"noLineChartForCpu": "Не используйте линейные графики для ЦП",
|
"noLineChartForCpu": "Не используйте линейные графики для ЦП",
|
||||||
"noPrivateKeyTip": "Приватный ключ не существует, возможно, он был удален или есть ошибка в настройках.",
|
"noPrivateKeyTip": "Приватный ключ не существует, возможно, он был удален или есть ошибка в настройках.",
|
||||||
"noPromptAgain": "Больше не спрашивать",
|
"noPromptAgain": "Больше не спрашивать",
|
||||||
"node": "Узел",
|
"node": "Узел",
|
||||||
"notAvailable": "Недоступно",
|
"notAvailable": "Недоступно",
|
||||||
"onServerDetailPage": "на странице деталей сервера",
|
"onServerDetailPage": "На странице деталей сервера",
|
||||||
"onlyOneLine": "Отображать только в одной строке (прокручиваемо)",
|
"onlyOneLine": "Отображать только в одной строке (прокручивается)",
|
||||||
"onlyWhenCoreBiggerThan8": "Действует только при количестве ядер > 8",
|
"onlyWhenCoreBiggerThan8": "Действует только при количестве ядер больше 8",
|
||||||
"openLastPath": "открыть последний путь",
|
"openLastPath": "Открыть последний путь",
|
||||||
"openLastPathTip": "Для разных серверов будут сохранены разные записи, записывается путь при выходе",
|
"openLastPathTip": "Для разных серверов будут сохранены разные записи, записывается путь при выходе",
|
||||||
"parseContainerStatsTip": "Анализ статуса использования Docker может быть медленным",
|
"parseContainerStatsTip": "Анализ статуса использования Docker может быть медленным",
|
||||||
"percentOfSize": "{percent}% от {size}",
|
"percentOfSize": "{percent}% от {size}",
|
||||||
"permission": "Разрешения",
|
"permission": "Разрешения",
|
||||||
"pingAvg": "Среднее:",
|
"pingAvg": "В среднем:",
|
||||||
"pingInputIP": "Пожалуйста, введите целевой IP или доменное имя",
|
"pingInputIP": "Пожалуйста, введите целевой IP или домен",
|
||||||
"pingNoServer": "Нет доступных серверов для Ping\nПожалуйста, добавьте серверы на вкладке серверов и попробуйте снова",
|
"pingNoServer": "Нет доступных серверов для Ping\nПожалуйста, добавьте их на вкладке «Сервер» и попробуйте снова",
|
||||||
"pkg": "менеджер пакетов",
|
"pkg": "Менеджер пакетов",
|
||||||
"plugInType": "Тип вставки",
|
"plugInType": "Тип вставки",
|
||||||
"port": "порт",
|
"port": "Порт",
|
||||||
"preview": "предпросмотр",
|
"preview": "Предпросмотр",
|
||||||
"privateKey": "приватный ключ",
|
"privateKey": "Приватный ключ",
|
||||||
"process": "процесс",
|
"process": "Процесс",
|
||||||
"pushToken": "токен уведомлений",
|
"pushToken": "Токен уведомлений",
|
||||||
"pveIgnoreCertTip": "Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.",
|
"pveIgnoreCertTip": "Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.",
|
||||||
"pveLoginFailed": "Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.",
|
"pveLoginFailed": "Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.",
|
||||||
"pveVersionLow": "Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.",
|
"pveVersionLow": "Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.",
|
||||||
"pwd": "пароль",
|
"pwd": "Пароль",
|
||||||
"read": "чтение",
|
"read": "Чтение",
|
||||||
"reboot": "перезагрузка",
|
"reboot": "Перезагрузка",
|
||||||
"rememberPwdInMem": "Запомнить пароль в памяти",
|
"rememberPwdInMem": "Запомнить пароль в памяти",
|
||||||
"rememberPwdInMemTip": "Используется для контейнеров, приостановки и т. д.",
|
"rememberPwdInMemTip": "Используется для контейнеров, приостановки и т. д.",
|
||||||
"rememberWindowSize": "Запомнить размер окна",
|
"rememberWindowSize": "Запомнить размер окна",
|
||||||
"remotePath": "удаленный путь",
|
"remotePath": "Удаленный путь",
|
||||||
"restart": "перезапустить",
|
"restart": "Перезапустить",
|
||||||
"result": "результат",
|
"result": "Результат",
|
||||||
"rotateAngel": "угол поворота",
|
"rotateAngel": "Угол поворота",
|
||||||
"route": "Маршрутизация",
|
"route": "Маршрутизация",
|
||||||
"run": "запустить",
|
"run": "Запустить",
|
||||||
"running": "работает",
|
"running": "Запущено",
|
||||||
"save": "сохранить",
|
"sameIdServerExist": "Сервер с таким ID уже существует",
|
||||||
"saved": "сохранено",
|
"save": "Сохранить",
|
||||||
"second": "секунда",
|
"saved": "Сохранено",
|
||||||
"sensors": "датчики",
|
"second": "с",
|
||||||
"sequence": "последовательность",
|
"sensors": "Датчики",
|
||||||
"server": "сервер",
|
"sequence": "Последовательность",
|
||||||
"serverDetailOrder": "порядок элементов на странице деталей сервера",
|
"server": "Сервер",
|
||||||
"serverFuncBtns": "кнопки функций сервера",
|
"serverDetailOrder": "Порядок элементов на странице деталей сервера",
|
||||||
"serverOrder": "порядок серверов",
|
"serverFuncBtns": "Кнопки функций сервера",
|
||||||
"sftpDlPrepare": "Подготовка к подключению к серверу...",
|
"serverOrder": "Порядок серверов",
|
||||||
|
"sftpDlPrepare": "Подготовка подключения...",
|
||||||
"sftpEditorTip": "Если пусто, используйте встроенный редактор файлов приложения. Если значение указано, используйте редактор удаленного сервера, например, `vim` (рекомендуется автоматически определять согласно `EDITOR`).",
|
"sftpEditorTip": "Если пусто, используйте встроенный редактор файлов приложения. Если значение указано, используйте редактор удаленного сервера, например, `vim` (рекомендуется автоматически определять согласно `EDITOR`).",
|
||||||
"sftpRmrDirSummary": "Использовать `rm -r` в SFTP для удаления папок",
|
"sftpRmrDirSummary": "Использовать `rm -r` в SFTP для удаления папок",
|
||||||
"sftpSSHConnected": "SFTP подключен...",
|
"sftpSSHConnected": "SFTP подключен...",
|
||||||
"sftpShowFoldersFirst": "показывать папки в начале",
|
"sftpShowFoldersFirst": "Показывать папки в начале",
|
||||||
"showDistLogo": "показать лого дистрибутива",
|
"showDistLogo": "Показать лого дистрибутива",
|
||||||
"shutdown": "выключение",
|
"shutdown": "Выключение",
|
||||||
"size": "размер",
|
"size": "Размер",
|
||||||
"snippet": "фрагмент",
|
"snippet": "Фрагмент",
|
||||||
"softWrap": "Мягкий перенос",
|
"softWrap": "Мягкий перенос",
|
||||||
"specifyDev": "Указать устройство",
|
"specifyDev": "Указать устройство",
|
||||||
"specifyDevTip": "Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.",
|
"specifyDevTip": "Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.",
|
||||||
"speed": "скорость",
|
"speed": "Скорость",
|
||||||
"spentTime": "Затрачено времени: {time}",
|
"spentTime": "Затрачено времени: {time}",
|
||||||
"sshTermHelp": "Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.",
|
"sshTermHelp": "Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.",
|
||||||
"sshTip": "Эта функция находится в стадии тестирования.\n\nПожалуйста, отправляйте отчеты о проблемах на {url} или присоединяйтесь к нашей разработке.",
|
"sshTip": "Эта функция находится в стадии тестирования.\n\nПожалуйста, отправляйте отчеты о проблемах на {url} или присоединяйтесь к нашей разработке.",
|
||||||
"sshVirtualKeyAutoOff": "автоматическое отключение виртуальных клавиш",
|
"sshVirtualKeyAutoOff": "Автоматическое переключение виртуальных клавиш",
|
||||||
"start": "старт",
|
"start": "Старт",
|
||||||
"stat": "Статистика",
|
"stat": "Статистика",
|
||||||
"stats": "статистика",
|
"stats": "Статистика",
|
||||||
"stop": "остановить",
|
"stop": "Остановить",
|
||||||
"stopped": "остановлено",
|
"stopped": "Остановлено",
|
||||||
"storage": "Хранение",
|
"storage": "Хранение",
|
||||||
"supportFmtArgs": "Поддерживаются следующие форматы аргументов:",
|
"supportFmtArgs": "Поддерживаются следующие форматы аргументов:",
|
||||||
"suspend": "приостановить",
|
"suspend": "Приостановить",
|
||||||
"suspendTip": "Функция приостановки требует прав root и поддержки systemd.",
|
"suspendTip": "Функция приостановки требует прав root и поддержки systemd.",
|
||||||
"switchTo": "переключиться на {val}",
|
"switchTo": "Переключиться на {val}",
|
||||||
"sync": "Синхронизировать",
|
"sync": "Синхронизировать",
|
||||||
"syncTip": "Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.",
|
"syncTip": "Возможно, потребуется перезагрузка, чтобы некоторые изменения вступили в силу.",
|
||||||
"system": "система",
|
"system": "Система",
|
||||||
"tag": "тег",
|
"tag": "Теги",
|
||||||
"temperature": "температура",
|
"temperature": "Температура",
|
||||||
"termFontSizeTip": "Эта настройка повлияет на размер терминала (ширина и высота). Вы можете масштабировать страницу терминала, чтобы调整 размер шрифта текущей сессии.",
|
"termFontSizeTip": "Эта настройка повлияет на размер терминала (ширина и высота). Вы можете масштабировать страницу терминала, чтобы调整 размер шрифта текущей сессии.",
|
||||||
"terminal": "терминал",
|
"terminal": "Терминал",
|
||||||
"test": "тест",
|
"test": "Тест",
|
||||||
"textScaler": "масштабирование текста",
|
"textScaler": "Масштабирование текста",
|
||||||
"textScalerTip": "1.0 => 100% (исходный размер), применяется только к части шрифтов на странице сервера, изменение не рекомендуется.",
|
"textScalerTip": "1.0 => 100% (исходный размер), применяется только к части шрифтов на странице сервера, изменение не рекомендуется.",
|
||||||
"theme": "тема",
|
"theme": "Тема",
|
||||||
"time": "время",
|
"time": "Время",
|
||||||
"times": "раз",
|
"times": "Раз",
|
||||||
"total": "всего",
|
"total": "Всего",
|
||||||
"traffic": "трафик",
|
"traffic": "Трафик",
|
||||||
"trySudo": "попробовать использовать sudo",
|
"trySudo": "Попробовать использовать sudo",
|
||||||
"ttl": "TTL",
|
"ttl": "TTL",
|
||||||
"unknown": "неизвестно",
|
"unknown": "Неизвестно",
|
||||||
"unkownConvertMode": "неизвестный режим конвертации",
|
"unkownConvertMode": "Неизвестный режим конвертации",
|
||||||
"update": "обновление",
|
"update": "Обновление",
|
||||||
"updateIntervalEqual0": "Если установлено в 0, статус сервера не будет автоматически обновляться.\nТакже не будет рассчитано использование CPU.",
|
"updateIntervalEqual0": "Если установлено 0, статус сервера не будет автоматически обновляться.\nТакже не будет рассчитано использование ЦП.",
|
||||||
"updateServerStatusInterval": "интервал обновления статуса сервера",
|
"updateServerStatusInterval": "Интервал обновления статуса сервера",
|
||||||
"upload": "загрузить",
|
"upload": "Загрузить",
|
||||||
"upsideDown": "перевернуть",
|
"upsideDown": "Перевернуть",
|
||||||
"uptime": "время работы",
|
"uptime": "Время работы",
|
||||||
"useCdn": "Использование CDN",
|
"useCdn": "Использование CDN",
|
||||||
"useCdnTip": "Не китайским пользователям рекомендуется использовать CDN. Хотели бы вы его использовать?",
|
"useCdnTip": "Не китайским пользователям рекомендуется использовать CDN. Хотели бы вы его использовать?",
|
||||||
"useNoPwd": "будет использоваться без пароля",
|
"useNoPwd": "Будет использоваться без пароля",
|
||||||
"usePodmanByDefault": "использовать Podman по умолчанию",
|
"usePodmanByDefault": "Использовать Podman по умолчанию",
|
||||||
"used": "использовано",
|
"used": "Использовано",
|
||||||
"view": "Вид",
|
"view": "Вид",
|
||||||
"viewErr": "просмотр ошибок",
|
"viewErr": "Просмотр ошибок",
|
||||||
"virtKeyHelpClipboard": "Если в терминале выделен текст, то он копируется в буфер обмена, в противном случае содержимое буфера вставляется в терминал.",
|
"virtKeyHelpClipboard": "Если в терминале выделен текст, то он копируется в буфер обмена, в противном случае содержимое буфера вставляется в терминал.",
|
||||||
"virtKeyHelpIME": "Включить/выключить клавиатуру",
|
"virtKeyHelpIME": "Включить/выключить клавиатуру",
|
||||||
"virtKeyHelpSFTP": "Открыть текущий путь в SFTP.",
|
"virtKeyHelpSFTP": "Открыть текущий путь в SFTP.",
|
||||||
@@ -217,9 +218,9 @@
|
|||||||
"wakeLock": "Держать включенным",
|
"wakeLock": "Держать включенным",
|
||||||
"watchNotPaired": "Apple Watch не сопряжены",
|
"watchNotPaired": "Apple Watch не сопряжены",
|
||||||
"webdavSettingEmpty": "Настройки Webdav пусты",
|
"webdavSettingEmpty": "Настройки Webdav пусты",
|
||||||
"whenOpenApp": "при открытии приложения",
|
"whenOpenApp": "При открытии приложения",
|
||||||
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
||||||
"write": "запись",
|
"write": "Запись",
|
||||||
"writeScriptFailTip": "Запись в скрипт не удалась, возможно, из-за отсутствия прав или директории не существует.",
|
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
||||||
"writeScriptTip": "После подключения к серверу скрипт будет записан в ~/.config/server_box для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
"writeScriptTip": "После подключения к серверу скрипт будет записан в ~/.config/server_box для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
||||||
}
|
}
|
||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "Yönlendirme",
|
"route": "Yönlendirme",
|
||||||
"run": "Çalıştır",
|
"run": "Çalıştır",
|
||||||
"running": "Çalışıyor",
|
"running": "Çalışıyor",
|
||||||
|
"sameIdServerExist": "Aynı kimliğe sahip bir sunucu zaten var",
|
||||||
"save": "Kaydet",
|
"save": "Kaydet",
|
||||||
"saved": "Kaydedildi",
|
"saved": "Kaydedildi",
|
||||||
"second": "s",
|
"second": "s",
|
||||||
|
|||||||
226
lib/l10n/app_uk.arb
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
{
|
||||||
|
"@@locale": "uk",
|
||||||
|
"aboutThanks": "Дякуємо наступним особам, які взяли участь.",
|
||||||
|
"acceptBeta": "Прийняти оновлення бета-версії",
|
||||||
|
"addSystemPrivateKeyTip": "Наразі приватних ключів нема, хочете додати той, що йде з системою (~/.ssh/id_rsa)?",
|
||||||
|
"added2List": "Додано до списку завдань",
|
||||||
|
"addr": "Адреса",
|
||||||
|
"alreadyLastDir": "Вже в останньому каталозі.",
|
||||||
|
"authFailTip": "Авторизація не вдалася, будь ласка, перевірте правильність облікових даних",
|
||||||
|
"autoBackupConflict": "Тільки одне автоматичне резервне копіювання може бути активне одночасно.",
|
||||||
|
"autoConnect": "Авто підключення",
|
||||||
|
"autoRun": "Авто запуск",
|
||||||
|
"autoUpdateHomeWidget": "Автоматичне оновлення віджетів на головному екрані",
|
||||||
|
"backupTip": "Експортовані дані слабо зашифровані. \nБудь ласка, зберігайте їх у безпеці.",
|
||||||
|
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
|
||||||
|
"battery": "Акумулятор",
|
||||||
|
"bgRun": "Запуск у фоновому режимі",
|
||||||
|
"bgRunTip": "Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".",
|
||||||
|
"cmd": "Команда",
|
||||||
|
"collapseUITip": "Сховати довгі списки, що є у UI за замовчуванням",
|
||||||
|
"conn": "З'єднання",
|
||||||
|
"container": "Контейнер",
|
||||||
|
"containerTrySudoTip": "Наприклад: У застосунку користувач це aaa, але Docker встановлений під користувачем root. У цьому випадку вам потрібно активувати цю опцію.",
|
||||||
|
"convert": "Конвертувати",
|
||||||
|
"copyPath": "Скопіювати шлях",
|
||||||
|
"cpuViewAsProgressTip": "Відобразити використання кожного процесора у вигляді стовпчикової діаграми (старий стиль)",
|
||||||
|
"cursorType": "Тип курсора",
|
||||||
|
"customCmd": "Користувацькі команди",
|
||||||
|
"customCmdDocUrl": "https://github.com/lollipopkit/flutter_server_box/wiki#custom-commands",
|
||||||
|
"customCmdHint": "\"Ім'я Команди\": \"Команда\"",
|
||||||
|
"decode": "Декодувати",
|
||||||
|
"decompress": "Розпакувати",
|
||||||
|
"deleteServers": "Масове видалення серверів",
|
||||||
|
"dirEmpty": "Переконайтеся, що директорія пуста.",
|
||||||
|
"disconnected": "Відключено",
|
||||||
|
"disk": "Диск",
|
||||||
|
"diskIgnorePath": "Ігнорувати шлях для диска",
|
||||||
|
"displayCpuIndex": "Відобразити індекс ЦП",
|
||||||
|
"dl2Local": "Завантажити {fileName} на локальний комп'ютер?",
|
||||||
|
"dockerEmptyRunningItems": "Немає запущених контейнерів.\nЦе може бути через:\n- Користувача Docker, відмінного від користувача, налаштованого в додатку\n- змінну оточення DOCKER_HOST, яка не була правильно зчитана. Ви можете виконати `echo $DOCKER_HOST` у терміналі, щоб побачити її значення.",
|
||||||
|
"dockerImagesFmt": "Всього {count} образів",
|
||||||
|
"dockerNotInstalled": "Docker не встановлено",
|
||||||
|
"dockerStatusRunningAndStoppedFmt": "{runningCount} запущено, {stoppedCount} контейнерів зупинено.",
|
||||||
|
"dockerStatusRunningFmt": "{count} контейнер(и) запущено.",
|
||||||
|
"doubleColumnMode": "Режим подвійної колонки",
|
||||||
|
"doubleColumnTip": "Ця опція лише активує функцію, чи можна її насправді включити, залежить від ширини пристрою",
|
||||||
|
"editVirtKeys": "Редагувати віртуальні клавіші",
|
||||||
|
"editor": "Редактор",
|
||||||
|
"editorHighlightTip": "Поточна підсвітка коду не ідеальна і може бути вимкнена для покращення.",
|
||||||
|
"encode": "Кодувати",
|
||||||
|
"envVars": "Змінні середовища",
|
||||||
|
"experimentalFeature": "Експериментальна функція",
|
||||||
|
"extraArgs": "Додаткові аргументи",
|
||||||
|
"fallbackSshDest": "Резервна SSH адреса",
|
||||||
|
"fdroidReleaseTip": "Якщо ви завантажили цей застосунок з F-Droid, рекомендується відключити цю опцію.",
|
||||||
|
"fileTooLarge": "Файл '{file}' занадто великий ({size}), макс {sizeMax}",
|
||||||
|
"followSystem": "Слідувати системі",
|
||||||
|
"font": "Шрифт",
|
||||||
|
"fontSize": "Розмір шрифту",
|
||||||
|
"force": "Примусово",
|
||||||
|
"fullScreen": "Повноекранний режим",
|
||||||
|
"fullScreenJitter": "Тремтіння в повноекранному режимі",
|
||||||
|
"fullScreenJitterHelp": "Щоб уникнути вигоряння екрану",
|
||||||
|
"fullScreenTip": "Чи слід увімкнути повноекранний режим під час повороту пристрою в горизонтальне положення? Ця опція стосується лише вкладки сервера.",
|
||||||
|
"goBackQ": "Повернутися назад?",
|
||||||
|
"goto": "Перейти до",
|
||||||
|
"hideTitleBar": "Сховати заголовок",
|
||||||
|
"highlight": "Підсвітка коду",
|
||||||
|
"homeWidgetUrlConfig": "Налаштувати URL віджета на головному екрані",
|
||||||
|
"host": "Хост",
|
||||||
|
"httpFailedWithCode": "Запит не вдався, код статусу: {code}",
|
||||||
|
"ignoreCert": "Ігнорувати сертифікат",
|
||||||
|
"image": "Зображення",
|
||||||
|
"imagesList": "Список зображень",
|
||||||
|
"init": "Ініціалізувати",
|
||||||
|
"inner": "Внутрішній",
|
||||||
|
"install": "Встановити",
|
||||||
|
"installDockerWithUrl": "Будь ласка, спочатку встановіть Docker. (https://docs.docker.com/engine/install)",
|
||||||
|
"invalid": "Недійсний",
|
||||||
|
"jumpServer": "Стрибковий Сервер",
|
||||||
|
"keepForeground": "Тримати застосунок на передньому плані!",
|
||||||
|
"keepStatusWhenErr": "Зберегати останній стан сервера",
|
||||||
|
"keepStatusWhenErrTip": "Тільки в разі виникнення помилки під час виконання скрипту",
|
||||||
|
"keyAuth": "Аутентифікація ключем",
|
||||||
|
"letterCache": "Кешування букв",
|
||||||
|
"letterCacheTip": "Рекомендується відключити, але після вимкнення стане неможливим введення CJK (китайських, японських, корейських) символів.",
|
||||||
|
"license": "Ліцензія",
|
||||||
|
"location": "Місцезнаходження",
|
||||||
|
"loss": "втрата пакетів",
|
||||||
|
"madeWithLove": "Зроблено з ❤️ від {myGithub}",
|
||||||
|
"manual": "Посібник",
|
||||||
|
"max": "макс.",
|
||||||
|
"maxRetryCount": "Кількість повторних спроб підключення до сервера",
|
||||||
|
"maxRetryCountEqual0": "Знову і знову буде намагатися повторно підключитися.",
|
||||||
|
"min": "мін.",
|
||||||
|
"mission": "Місія",
|
||||||
|
"more": "Більше",
|
||||||
|
"moveOutServerFuncBtnsHelp": "Включено: може відображатися під кожною карткою на вкладці Сервер. Вимкнено: може відображатися вгорі на сторінці деталей сервера.",
|
||||||
|
"ms": "мс.",
|
||||||
|
"needHomeDir": "Якщо ви користувач Synology, [дивіться тут](https://kb.synology.com/DSM/tutorial/user_enable_home_service). Користувачі інших систем повинні знайти інформацію про те, як створити домашній каталог.",
|
||||||
|
"needRestart": "Необхідно перезапустити застосунок",
|
||||||
|
"net": "Мережа",
|
||||||
|
"netViewType": "Тип перегляду мережі",
|
||||||
|
"newContainer": "Новий контейнер",
|
||||||
|
"noLineChart": "Не використовувати лінійні діаграми",
|
||||||
|
"noLineChartForCpu": "Не використовувати лінійні діаграми для ЦП",
|
||||||
|
"noPrivateKeyTip": "Приватного ключа немає, можливо, він був видалений або сталася помилка конфігурації.",
|
||||||
|
"noPromptAgain": "Більше не запитувати",
|
||||||
|
"node": "Вузол",
|
||||||
|
"notAvailable": "Недоступний",
|
||||||
|
"onServerDetailPage": "На сторінці деталі сервера",
|
||||||
|
"onlyOneLine": "Відображати лише в один рядок (прокрутка)",
|
||||||
|
"onlyWhenCoreBiggerThan8": "Працює лише тоді, коли кількість ядер перевищує 8",
|
||||||
|
"openLastPath": "Відкрити останній шлях",
|
||||||
|
"openLastPathTip": "Для різних серверів будуть збережені різні логи. Записується шлях при виході",
|
||||||
|
"parseContainerStatsTip": "Парсинг статусу зайнятості Docker є відносно повільним.",
|
||||||
|
"percentOfSize": "{percent}% з {size}",
|
||||||
|
"permission": "Дозволи",
|
||||||
|
"pingAvg": "Середнє:",
|
||||||
|
"pingInputIP": "Будь ласка, введіть цільовий IP / Домен.",
|
||||||
|
"pingNoServer": "Немає сервера для пінгування.\nБудь ласка, додайте сервер у вкладці `Сервер`.",
|
||||||
|
"pkg": "Пакет",
|
||||||
|
"plugInType": "Тип вставки",
|
||||||
|
"port": "Порт",
|
||||||
|
"preview": "Попередній перегляд",
|
||||||
|
"privateKey": "Приватний ключ",
|
||||||
|
"process": "Процес",
|
||||||
|
"pushToken": "Надіслати токен",
|
||||||
|
"pveIgnoreCertTip": "Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.",
|
||||||
|
"pveLoginFailed": "Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.",
|
||||||
|
"pveVersionLow": "Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.",
|
||||||
|
"pwd": "Пароль",
|
||||||
|
"read": "Читати",
|
||||||
|
"reboot": "Перезавантажити",
|
||||||
|
"rememberPwdInMem": "Запам'ятати пароль у пам'яті",
|
||||||
|
"rememberPwdInMemTip": "Використовується для контейнерів, призупинення тощо.",
|
||||||
|
"rememberWindowSize": "Запам'ятати розмір вікна",
|
||||||
|
"remotePath": "Віддалений шлях",
|
||||||
|
"restart": "Перезапустити",
|
||||||
|
"result": "Результат",
|
||||||
|
"rotateAngel": "Кут повороту",
|
||||||
|
"route": "Маршрут",
|
||||||
|
"run": "Запустити",
|
||||||
|
"running": "Виконання",
|
||||||
|
"sameIdServerExist": "Сервер з таким ID вже існує",
|
||||||
|
"save": "Зберегти",
|
||||||
|
"saved": "Збережено",
|
||||||
|
"second": "сек.",
|
||||||
|
"sensors": "Датчики",
|
||||||
|
"sequence": "Послідовність",
|
||||||
|
"server": "Сервер",
|
||||||
|
"serverDetailOrder": "Порядок віджетів на сторінці деталі",
|
||||||
|
"serverFuncBtns": "Кнопки функцій сервера",
|
||||||
|
"serverOrder": "Порядок сервера",
|
||||||
|
"sftpDlPrepare": "Підготовка до підключення...",
|
||||||
|
"sftpEditorTip": "Якщо порожньо, використовуйте вбудований редактор файлів програми. Якщо є значення, використовуйте редактор віддаленого сервера, наприклад, `vim` (рекомендується автоматично визначити відповідно до `EDITOR`).",
|
||||||
|
"sftpRmrDirSummary": "Використовуйте `rm -r`, щоб видалити папку в SFTP.",
|
||||||
|
"sftpSSHConnected": "SFTP підключено",
|
||||||
|
"sftpShowFoldersFirst": "Спочатку відображати директорії",
|
||||||
|
"showDistLogo": "Показати логотип дистрибутива",
|
||||||
|
"shutdown": "Вимкнення",
|
||||||
|
"size": "Розмір",
|
||||||
|
"snippet": "Фрагмент",
|
||||||
|
"softWrap": "М'ягкий перенос",
|
||||||
|
"specifyDev": "Вказати пристрій",
|
||||||
|
"specifyDevTip": "Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.",
|
||||||
|
"speed": "Швидкість",
|
||||||
|
"spentTime": "Витрачений час: {time}",
|
||||||
|
"sshTermHelp": "Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.",
|
||||||
|
"sshTip": "Ця функція наразі в експериментальній стадії. Будь ласка, повідомте про помилки за адресою {url} або приєднуйтеся до нашої розробки.",
|
||||||
|
"sshVirtualKeyAutoOff": "Автоматичне переключення віртуальних клавіш",
|
||||||
|
"start": "Старт",
|
||||||
|
"stat": "Статистика",
|
||||||
|
"stats": "Статистики",
|
||||||
|
"stop": "Зупинити",
|
||||||
|
"stopped": "Зупинено",
|
||||||
|
"storage": "Сховище",
|
||||||
|
"supportFmtArgs": "Підтримуються такі параметри форматування:",
|
||||||
|
"suspend": "Призупинити",
|
||||||
|
"suspendTip": "Функція призупинення потребує адміністративних прав та підтримки systemd.",
|
||||||
|
"switchTo": "Переключитися на {val}",
|
||||||
|
"sync": "Синхронізація",
|
||||||
|
"syncTip": "Може знадобитися перезапуск, щоб деякі зміни набрали чинності.",
|
||||||
|
"system": "Система",
|
||||||
|
"tag": "Теги",
|
||||||
|
"temperature": "Температура",
|
||||||
|
"termFontSizeTip": "Це налаштування вплине на розмір терміналу (ширину та висоту). Ви можете масштабувати на сторінці терміналу, щоб налаштувати розмір шрифту поточної сесії.",
|
||||||
|
"terminal": "Термінал",
|
||||||
|
"test": "Тест",
|
||||||
|
"textScaler": "Масштабування тексту",
|
||||||
|
"textScalerTip": "1.0 => 100% (оригінальний розмір), працює лише на частині шрифта сторінки сервера, не рекомендується змінювати.",
|
||||||
|
"theme": "Тема",
|
||||||
|
"time": "Час",
|
||||||
|
"times": "Рази",
|
||||||
|
"total": "Всього",
|
||||||
|
"traffic": "Трафік",
|
||||||
|
"trySudo": "Спробуйте використовувати sudo",
|
||||||
|
"ttl": "TTL",
|
||||||
|
"unknown": "Невідомо",
|
||||||
|
"unkownConvertMode": "Невідомий режим конвертації",
|
||||||
|
"update": "Оновити",
|
||||||
|
"updateIntervalEqual0": "Ви встановили 0, автоматичне оновлення не відбудеться.\nНе можна розрахувати статус ЦП.",
|
||||||
|
"updateServerStatusInterval": "Інтервал оновлення статусу сервера",
|
||||||
|
"upload": "Завантаження",
|
||||||
|
"upsideDown": "Доверху дном",
|
||||||
|
"uptime": "Час роботи",
|
||||||
|
"useCdn": "Використання CDN",
|
||||||
|
"useCdnTip": "Нереспонсивним користувачам рекомендується використовувати CDN. Чи хочете ви його використовувати?",
|
||||||
|
"useNoPwd": "Пароль не буде використовуватися",
|
||||||
|
"usePodmanByDefault": "Використовувати Podman за замовчуванням",
|
||||||
|
"used": "Використано",
|
||||||
|
"view": "Переглянути",
|
||||||
|
"viewErr": "Переглянути помилку",
|
||||||
|
"virtKeyHelpClipboard": "Копіювати в буфер обміну, якщо вибраний термінал не порожній, в іншому випадку вставити вміст буфера обміну в термінал.",
|
||||||
|
"virtKeyHelpIME": "Увімкнути/вимкнути клавіатуру",
|
||||||
|
"virtKeyHelpSFTP": "Відкрити поточний каталог у SFTP.",
|
||||||
|
"waitConnection": "Будь ласка, зачекайте, доки з'єднання буде встановлено.",
|
||||||
|
"wakeLock": "Залишити активним",
|
||||||
|
"watchNotPaired": "Немає спарованого Apple Watch",
|
||||||
|
"webdavSettingEmpty": "Налаштування WebDav порожнє",
|
||||||
|
"whenOpenApp": "При відкритті програми",
|
||||||
|
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
||||||
|
"write": "Записати",
|
||||||
|
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
||||||
|
"writeScriptTip": "Після підключення до сервера скрипт буде записано у ~/.config/server_box для моніторингу стану системи. Ви можете переглянути вміст скрипта."
|
||||||
|
}
|
||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "路由",
|
"route": "路由",
|
||||||
"run": "运行",
|
"run": "运行",
|
||||||
"running": "运行中",
|
"running": "运行中",
|
||||||
|
"sameIdServerExist": "已存在相同 id 的服务器",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saved": "已保存",
|
"saved": "已保存",
|
||||||
"second": "秒",
|
"second": "秒",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
"route": "路由",
|
"route": "路由",
|
||||||
"run": "運行",
|
"run": "運行",
|
||||||
"running": "運作中",
|
"running": "運作中",
|
||||||
|
"sameIdServerExist": "已存在相同 ID 的伺服器",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saved": "已保存",
|
"saved": "已保存",
|
||||||
"second": "秒",
|
"second": "秒",
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:server_box/app.dart';
|
import 'package:server_box/app.dart';
|
||||||
import 'package:server_box/core/utils/sync/icloud.dart';
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/core/utils/sync/webdav.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';
|
||||||
@@ -25,8 +24,8 @@ import 'package:server_box/data/provider/server.dart';
|
|||||||
import 'package:server_box/data/provider/sftp.dart';
|
import 'package:server_box/data/provider/sftp.dart';
|
||||||
import 'package:server_box/data/provider/snippet.dart';
|
import 'package:server_box/data/provider/snippet.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/misc.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/store/no_backup.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
_runInZone(() async {
|
_runInZone(() async {
|
||||||
@@ -52,7 +51,7 @@ void _runInZone(void Function() body) {
|
|||||||
Future<void> _initApp() async {
|
Future<void> _initApp() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await Paths.init(BuildData.name, bakName: Miscs.bakFileName);
|
await Paths.init(BuildData.name, bakName: 'srvbox_bak.json');
|
||||||
await _initData();
|
await _initData();
|
||||||
_setupDebug();
|
_setupDebug();
|
||||||
|
|
||||||
@@ -67,7 +66,6 @@ Future<void> _initApp() async {
|
|||||||
FontUtils.loadFrom(Stores.setting.fontPath.fetch());
|
FontUtils.loadFrom(Stores.setting.fontPath.fetch());
|
||||||
|
|
||||||
_doPlatformRelated();
|
_doPlatformRelated();
|
||||||
_doVersionRelated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initData() async {
|
Future<void> _initData() async {
|
||||||
@@ -93,6 +91,9 @@ Future<void> _initData() async {
|
|||||||
SftpProvider.instance.load();
|
SftpProvider.instance.load();
|
||||||
|
|
||||||
if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta;
|
if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta;
|
||||||
|
|
||||||
|
// It may effect the following logic, so await it.
|
||||||
|
await _doVersionRelated();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupDebug() {
|
void _setupDebug() {
|
||||||
@@ -115,10 +116,7 @@ void _doPlatformRelated() async {
|
|||||||
// Plus 1 to avoid 0.
|
// Plus 1 to avoid 0.
|
||||||
Computer.shared.turnOn(workersCount: (serversCount / 3).round() + 1);
|
Computer.shared.turnOn(workersCount: (serversCount / 3).round() + 1);
|
||||||
|
|
||||||
if (isIOS || isMacOS) {
|
bakSync.sync();
|
||||||
if (Stores.setting.icloudSync.fetch()) ICloud.sync();
|
|
||||||
}
|
|
||||||
if (Stores.setting.webdavSync.fetch()) Webdav.sync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// It may contains some async heavy funcs.
|
// It may contains some async heavy funcs.
|
||||||
@@ -130,6 +128,7 @@ Future<void> _doVersionRelated() async {
|
|||||||
if (curVer < newVer) {
|
if (curVer < newVer) {
|
||||||
ServerDetailCards.autoAddNewCards(newVer);
|
ServerDetailCards.autoAddNewCards(newVer);
|
||||||
ServerFuncBtn.autoAddNewFuncs(newVer);
|
ServerFuncBtn.autoAddNewFuncs(newVer);
|
||||||
|
NoBackupStore.instance.migrate();
|
||||||
Stores.setting.lastVer.put(newVer);
|
Stores.setting.lastVer.put(newVer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import 'package:computer/computer.dart';
|
|||||||
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:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/utils/sync/icloud.dart';
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/core/utils/sync/webdav.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
import 'package:server_box/data/model/app/backup.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/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
@@ -14,17 +13,32 @@ import 'package:server_box/data/provider/snippet.dart';
|
|||||||
import 'package:server_box/data/res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/data/store/no_backup.dart';
|
||||||
|
|
||||||
final icloudLoading = false.vn;
|
class BackupPage extends StatefulWidget {
|
||||||
final webdavLoading = false.vn;
|
|
||||||
|
|
||||||
class BackupPage extends StatelessWidget {
|
|
||||||
const BackupPage({super.key});
|
const BackupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BackupPage> createState() => _BackupPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _BackupPageState extends State<BackupPage>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
final _noBak = NoBackupStore.instance;
|
||||||
|
final icloudLoading = false.vn;
|
||||||
|
final webdavLoading = false.vn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
icloudLoading.dispose();
|
||||||
|
webdavLoading.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: Text(libL10n.backup)),
|
|
||||||
body: _buildBody(context),
|
body: _buildBody(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,9 +105,9 @@ class BackupPage extends StatelessWidget {
|
|||||||
leading: const Icon(Icons.cloud),
|
leading: const Icon(Icons.cloud),
|
||||||
title: const Text('iCloud'),
|
title: const Text('iCloud'),
|
||||||
trailing: StoreSwitch(
|
trailing: StoreSwitch(
|
||||||
prop: Stores.setting.icloudSync,
|
prop: _noBak.icloudSync,
|
||||||
validator: (p0) {
|
validator: (p0) {
|
||||||
if (p0 && Stores.setting.webdavSync.fetch()) {
|
if (p0 && _noBak.webdavSync.fetch()) {
|
||||||
context.showSnackBar(l10n.autoBackupConflict);
|
context.showSnackBar(l10n.autoBackupConflict);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -102,7 +116,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
callback: (val) async {
|
callback: (val) async {
|
||||||
if (val) {
|
if (val) {
|
||||||
icloudLoading.value = true;
|
icloudLoading.value = true;
|
||||||
await ICloud.sync();
|
await bakSync.sync(rs: icloud);
|
||||||
icloudLoading.value = false;
|
icloudLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -126,17 +140,17 @@ class BackupPage extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(libL10n.auto),
|
title: Text(libL10n.auto),
|
||||||
trailing: StoreSwitch(
|
trailing: StoreSwitch(
|
||||||
prop: Stores.setting.webdavSync,
|
prop: _noBak.webdavSync,
|
||||||
validator: (p0) {
|
validator: (p0) {
|
||||||
if (p0) {
|
if (p0) {
|
||||||
if (Stores.setting.webdavUrl.fetch().isEmpty ||
|
if (_noBak.webdavUrl.fetch().isEmpty ||
|
||||||
Stores.setting.webdavUser.fetch().isEmpty ||
|
_noBak.webdavUser.fetch().isEmpty ||
|
||||||
Stores.setting.webdavPwd.fetch().isEmpty) {
|
_noBak.webdavPwd.fetch().isEmpty) {
|
||||||
context.showSnackBar(l10n.webdavSettingEmpty);
|
context.showSnackBar(l10n.webdavSettingEmpty);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Stores.setting.icloudSync.fetch()) {
|
if (_noBak.icloudSync.fetch()) {
|
||||||
context.showSnackBar(l10n.autoBackupConflict);
|
context.showSnackBar(l10n.autoBackupConflict);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -145,7 +159,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
callback: (val) async {
|
callback: (val) async {
|
||||||
if (val) {
|
if (val) {
|
||||||
webdavLoading.value = true;
|
webdavLoading.value = true;
|
||||||
await Webdav.sync();
|
await bakSync.sync(rs: webdav);
|
||||||
webdavLoading.value = false;
|
webdavLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -156,7 +170,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
trailing: ListenableBuilder(
|
trailing: ListenableBuilder(
|
||||||
listenable: webdavLoading,
|
listenable: webdavLoading,
|
||||||
builder: (_, __) {
|
builder: (_, __) {
|
||||||
if (webdavLoading.value) return SizedLoading.centerSmall;
|
if (webdavLoading.value) return SizedLoading.small;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -298,7 +312,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await backup.restore(force: true);
|
await backup.merge(force: true);
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
).toList,
|
).toList,
|
||||||
@@ -312,7 +326,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
Future<void> _onTapWebdavDl(BuildContext context) async {
|
Future<void> _onTapWebdavDl(BuildContext context) async {
|
||||||
webdavLoading.value = true;
|
webdavLoading.value = true;
|
||||||
try {
|
try {
|
||||||
final files = await Webdav.list();
|
final files = await webdav.list();
|
||||||
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
|
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
|
||||||
|
|
||||||
final fileName = await context.showPickSingleDialog(
|
final fileName = await context.showPickSingleDialog(
|
||||||
@@ -321,13 +335,10 @@ class BackupPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (fileName == null) return;
|
if (fileName == null) return;
|
||||||
|
|
||||||
final result = await Webdav.download(relativePath: fileName);
|
await webdav.download(relativePath: fileName);
|
||||||
if (result != null) {
|
|
||||||
throw result;
|
|
||||||
}
|
|
||||||
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
||||||
await dlBak.restore(force: true);
|
await dlBak.merge(force: true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
context.showErrDialog(e, s, libL10n.restore);
|
context.showErrDialog(e, s, libL10n.restore);
|
||||||
Loggers.app.warning('Download webdav backup failed', e, s);
|
Loggers.app.warning('Download webdav backup failed', e, s);
|
||||||
@@ -342,10 +353,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
final bakName = '$date-${Miscs.bakFileName}';
|
final bakName = '$date-${Miscs.bakFileName}';
|
||||||
try {
|
try {
|
||||||
await Backup.backup(bakName);
|
await Backup.backup(bakName);
|
||||||
final uploadResult = await Webdav.upload(relativePath: bakName);
|
await webdav.upload(relativePath: bakName);
|
||||||
if (uploadResult != null) {
|
|
||||||
throw uploadResult;
|
|
||||||
}
|
|
||||||
Loggers.app.info('Upload webdav backup success');
|
Loggers.app.info('Upload webdav backup success');
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
context.showErrDialog(e, s, l10n.upload);
|
context.showErrDialog(e, s, l10n.upload);
|
||||||
@@ -356,9 +364,9 @@ class BackupPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onTapWebdavSetting(BuildContext context) async {
|
Future<void> _onTapWebdavSetting(BuildContext context) async {
|
||||||
final url = TextEditingController(text: Stores.setting.webdavUrl.fetch());
|
final url = TextEditingController(text: _noBak.webdavUrl.fetch());
|
||||||
final user = TextEditingController(text: Stores.setting.webdavUser.fetch());
|
final user = TextEditingController(text: _noBak.webdavUser.fetch());
|
||||||
final pwd = TextEditingController(text: Stores.setting.webdavPwd.fetch());
|
final pwd = TextEditingController(text: _noBak.webdavPwd.fetch());
|
||||||
final nodeUser = FocusNode();
|
final nodeUser = FocusNode();
|
||||||
final nodePwd = FocusNode();
|
final nodePwd = FocusNode();
|
||||||
final result = await context.showRoundDialog<bool>(
|
final result = await context.showRoundDialog<bool>(
|
||||||
@@ -392,13 +400,18 @@ class BackupPage extends StatelessWidget {
|
|||||||
actions: Btnx.oks,
|
actions: Btnx.oks,
|
||||||
);
|
);
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
final result = await Webdav.test(url.text, user.text, pwd.text);
|
try {
|
||||||
if (result != null) {
|
await Webdav.test(url.text, user.text, pwd.text);
|
||||||
context.showSnackBar(result);
|
context.showSnackBar(libL10n.success);
|
||||||
return;
|
webdav.init(WebdavInitArgs(
|
||||||
|
url: url.text,
|
||||||
|
user: user.text,
|
||||||
|
pwd: pwd.text,
|
||||||
|
prefix: 'serverbox/',
|
||||||
|
));
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showErrDialog(e, s, 'Webdav');
|
||||||
}
|
}
|
||||||
context.showSnackBar(libL10n.success);
|
|
||||||
Webdav.changeClient(url.text, user.text, pwd.text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +440,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await backup.restore(force: true);
|
await backup.merge(force: true);
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
).toList,
|
).toList,
|
||||||
@@ -476,4 +489,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
Loggers.app.warning('Import servers failed', e, s);
|
Loggers.app.warning('Import servers failed', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
|
_container.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ import 'package:server_box/data/res/store.dart';
|
|||||||
|
|
||||||
import 'package:server_box/view/widget/two_line_text.dart';
|
import 'package:server_box/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
class EditorPage extends StatefulWidget {
|
final class EditorPageRet {
|
||||||
|
/// If edit text, this includes the edited result
|
||||||
|
final String? result;
|
||||||
|
|
||||||
|
/// Indicates whether it's ok to edit existing file
|
||||||
|
final bool? editExistedOk;
|
||||||
|
|
||||||
|
const EditorPageRet({this.result, this.editExistedOk});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EditorPageArgs {
|
||||||
/// If path is not null, then it's a file editor
|
/// If path is not null, then it's a file editor
|
||||||
/// If path is null, then it's a text editor
|
/// If path is null, then it's a text editor
|
||||||
final String? path;
|
final String? path;
|
||||||
@@ -28,13 +38,23 @@ class EditorPage extends StatefulWidget {
|
|||||||
|
|
||||||
final String? title;
|
final String? title;
|
||||||
|
|
||||||
const EditorPage({
|
const EditorPageArgs({
|
||||||
super.key,
|
|
||||||
this.path,
|
this.path,
|
||||||
this.text,
|
this.text,
|
||||||
this.langCode,
|
this.langCode,
|
||||||
this.title,
|
this.title,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditorPage extends StatefulWidget {
|
||||||
|
final EditorPageArgs? args;
|
||||||
|
|
||||||
|
const EditorPage({super.key, this.args});
|
||||||
|
|
||||||
|
static const route = AppRoute<EditorPageRet, EditorPageArgs>(
|
||||||
|
page: EditorPage.new,
|
||||||
|
path: '/editor',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EditorPage> createState() => _EditorPageState();
|
State<EditorPage> createState() => _EditorPageState();
|
||||||
@@ -50,13 +70,21 @@ class _EditorPageState extends State<EditorPage> {
|
|||||||
|
|
||||||
String? _langCode;
|
String? _langCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_controller.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
/// Higher priority than [path]
|
/// Higher priority than [path]
|
||||||
if (Stores.setting.editorHighlight.fetch()) {
|
if (Stores.setting.editorHighlight.fetch()) {
|
||||||
_langCode = widget.langCode ?? Highlights.getCode(widget.path);
|
_langCode =
|
||||||
|
widget.args?.langCode ?? Highlights.getCode(widget.args?.path);
|
||||||
}
|
}
|
||||||
_controller = CodeController(
|
_controller = CodeController(
|
||||||
language: Highlights.all[_langCode],
|
language: Highlights.all[_langCode],
|
||||||
@@ -72,14 +100,15 @@ class _EditorPageState extends State<EditorPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setupCtrl() async {
|
Future<void> _setupCtrl() async {
|
||||||
if (widget.path != null) {
|
final path = widget.args?.path;
|
||||||
final code = await Computer.shared.start(
|
final text = widget.args?.text;
|
||||||
(path) async => await File(path).readAsString(),
|
if (path != null) {
|
||||||
widget.path!,
|
final code = await Computer.shared.startNoParam(
|
||||||
|
() => File(path).readAsString(),
|
||||||
);
|
);
|
||||||
_controller.text = code;
|
_controller.text = code;
|
||||||
} else if (widget.text != null) {
|
} else if (text != null) {
|
||||||
_controller.text = widget.text!;
|
_controller.text = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +126,6 @@ class _EditorPageState extends State<EditorPage> {
|
|||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
_focusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -117,7 +139,9 @@ class _EditorPageState extends State<EditorPage> {
|
|||||||
return CustomAppBar(
|
return CustomAppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: TwoLineText(
|
title: TwoLineText(
|
||||||
up: widget.title ?? widget.path?.getFileName() ?? l10n.unknown,
|
up: widget.args?.title ??
|
||||||
|
widget.args?.path?.getFileName() ??
|
||||||
|
l10n.unknown,
|
||||||
down: l10n.editor,
|
down: l10n.editor,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -144,20 +168,21 @@ class _EditorPageState extends State<EditorPage> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// If path is not null, then it's a file editor
|
// If path is not null, then it's a file editor
|
||||||
// save the text and return true to pop the page
|
// save the text and return true to pop the page
|
||||||
if (widget.path != null) {
|
final path = widget.args?.path;
|
||||||
|
if (path != null) {
|
||||||
final (res, _) = await context.showLoadingDialog(
|
final (res, _) = await context.showLoadingDialog(
|
||||||
fn: () => File(widget.path!).writeAsString(_controller.text),
|
fn: () => File(path).writeAsString(_controller.text),
|
||||||
);
|
);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
context.showSnackBar(libL10n.fail);
|
context.showSnackBar(libL10n.fail);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.pop(true);
|
context.pop(const EditorPageRet(editExistedOk: true));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// else it's a text editor
|
// else it's a text editor
|
||||||
// return the text to the previous page
|
// return the text to the previous page
|
||||||
context.pop(_controller.text);
|
context.pop(EditorPageRet(result: _controller.text));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,39 +1,26 @@
|
|||||||
part of 'home.dart';
|
part of 'home.dart';
|
||||||
|
|
||||||
final class _AppBar extends CustomAppBar {
|
final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final ValueNotifier<int> selectIndex;
|
final double paddingTop;
|
||||||
final ValueNotifier<bool> landscape;
|
|
||||||
|
|
||||||
const _AppBar({
|
const _AppBar(this.paddingTop);
|
||||||
required this.selectIndex,
|
|
||||||
required this.landscape,
|
|
||||||
super.title,
|
|
||||||
super.actions,
|
|
||||||
super.centerTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final placeholder = SizedBox(
|
return SizedBox(
|
||||||
height: CustomAppBar.barHeight ?? 0 + MediaQuery.of(context).padding.top,
|
height: preferredSize.height,
|
||||||
);
|
child: isIOS
|
||||||
return selectIndex.listenVal(
|
? const Center(child: Text(BuildData.name, style: UIs.text15Bold))
|
||||||
(idx) {
|
: null,
|
||||||
if (idx == AppTab.ssh.index) {
|
|
||||||
return placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDesktop) return super.build(context);
|
|
||||||
|
|
||||||
return ValBuilder(
|
|
||||||
listenable: landscape,
|
|
||||||
builder: (ls) {
|
|
||||||
if (ls) return placeholder;
|
|
||||||
|
|
||||||
return super.build(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize {
|
||||||
|
final height = switch (Pfs.type) {
|
||||||
|
Pfs.macos => paddingTop + (CustomAppBar.sysStatusBarHeight ?? 0),
|
||||||
|
_ => paddingTop,
|
||||||
|
};
|
||||||
|
return Size.fromHeight(height);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
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:icons_plus/icons_plus.dart';
|
|
||||||
import 'package:server_box/core/channel/home_widget.dart';
|
import 'package:server_box/core/channel/home_widget.dart';
|
||||||
import 'package:server_box/core/extension/build.dart';
|
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
|
||||||
import 'package:server_box/core/route.dart';
|
|
||||||
import 'package:server_box/data/model/app/tab.dart';
|
import 'package:server_box/data/model/app/tab.dart';
|
||||||
import 'package:server_box/data/provider/app.dart';
|
import 'package:server_box/data/provider/app.dart';
|
||||||
import 'package:server_box/data/provider/server.dart';
|
import 'package:server_box/data/provider/server.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/github_id.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/data/res/url.dart';
|
import 'package:server_box/data/res/url.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -39,6 +31,18 @@ class _HomePageState extends State<HomePage>
|
|||||||
bool _switchingPage = false;
|
bool _switchingPage = false;
|
||||||
bool _shouldAuth = false;
|
bool _shouldAuth = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
ServerProvider.closeServer();
|
||||||
|
_pageController.dispose();
|
||||||
|
WakelockPlus.disable();
|
||||||
|
|
||||||
|
_selectIndex.dispose();
|
||||||
|
_isLandscape.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -61,15 +65,6 @@ class _HomePageState extends State<HomePage>
|
|||||||
MediaQuery.of(context).orientation == Orientation.landscape;
|
MediaQuery.of(context).orientation == Orientation.landscape;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
ServerProvider.closeServer();
|
|
||||||
_pageController.dispose();
|
|
||||||
WakelockPlus.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
@@ -105,39 +100,10 @@ class _HomePageState extends State<HomePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
AppProvider.ctx = context;
|
AppProvider.ctx = context;
|
||||||
|
final sysPadding = MediaQuery.of(context).padding;
|
||||||
|
|
||||||
final appBar = _AppBar(
|
|
||||||
selectIndex: _selectIndex,
|
|
||||||
landscape: _isLandscape,
|
|
||||||
centerTitle: false,
|
|
||||||
title: const Text(BuildData.name),
|
|
||||||
actions: <Widget>[
|
|
||||||
ValBuilder(
|
|
||||||
listenable: Stores.setting.serverStatusUpdateInterval.listenable(),
|
|
||||||
builder: (interval) {
|
|
||||||
if (interval != 0) return UIs.placeholder;
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
tooltip: 'Refresh',
|
|
||||||
onPressed: () async {
|
|
||||||
await ServerProvider.refresh();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.developer_mode, size: 21),
|
|
||||||
tooltip: 'Debug',
|
|
||||||
onPressed: () => DebugPage.route.go(
|
|
||||||
context,
|
|
||||||
args: const DebugPageArgs(title: 'Debug(${BuildData.build})'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: _buildDrawer(),
|
appBar: _AppBar(sysPadding.top),
|
||||||
appBar: appBar,
|
|
||||||
body: PageView.builder(
|
body: PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemCount: AppTab.values.length,
|
itemCount: AppTab.values.length,
|
||||||
@@ -185,133 +151,7 @@ class _HomePageState extends State<HomePage>
|
|||||||
labelBehavior: ls
|
labelBehavior: ls
|
||||||
? NavigationDestinationLabelBehavior.alwaysHide
|
? NavigationDestinationLabelBehavior.alwaysHide
|
||||||
: NavigationDestinationLabelBehavior.onlyShowSelected,
|
: NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||||
destinations: [
|
destinations: AppTab.navDestinations,
|
||||||
NavigationDestination(
|
|
||||||
icon: const Icon(BoxIcons.bx_server),
|
|
||||||
label: l10n.server,
|
|
||||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.terminal_outlined),
|
|
||||||
label: 'SSH',
|
|
||||||
selectedIcon: Icon(Icons.terminal),
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
icon: const Icon(MingCute.file_code_line),
|
|
||||||
label: l10n.snippet,
|
|
||||||
selectedIcon: const Icon(MingCute.file_code_fill),
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(MingCute.planet_line),
|
|
||||||
label: 'Ping',
|
|
||||||
selectedIcon: Icon(MingCute.planet_fill),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDrawer() {
|
|
||||||
return Drawer(
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildIcon(),
|
|
||||||
const Text(
|
|
||||||
'${BuildData.name}\n${BuildDataX.versionStr}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: UIs.text15,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 37),
|
|
||||||
_buildTiles(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTiles() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.settings),
|
|
||||||
title: Text(libL10n.setting),
|
|
||||||
onTap: () => AppRoutes.settings().go(context),
|
|
||||||
onLongPress: _onLongPressSetting,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.vpn_key),
|
|
||||||
title: Text(l10n.privateKey),
|
|
||||||
onTap: () => AppRoutes.keyList().go(context),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(BoxIcons.bxs_file_blank),
|
|
||||||
title: Text(libL10n.file),
|
|
||||||
onTap: () => AppRoutes.localStorage().go(context),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MingCute.file_import_fill),
|
|
||||||
title: Text(libL10n.backup),
|
|
||||||
onTap: () => AppRoutes.backup().go(context),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(OctIcons.feed_discussion),
|
|
||||||
title: Text('${libL10n.about} & ${libL10n.feedback}'),
|
|
||||||
onTap: _showAboutDialog,
|
|
||||||
)
|
|
||||||
].map((e) => CardX(child: e)).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showAboutDialog() {
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.about,
|
|
||||||
child: _buildAboutContent(),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Urls.appWiki.launch(),
|
|
||||||
child: const Text('Wiki'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Urls.appHelp.launch(),
|
|
||||||
child: Text(libL10n.feedback),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => showLicensePage(context: context),
|
|
||||||
child: Text(l10n.license),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAboutContent() {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
|
||||||
child: SimpleMarkdown(
|
|
||||||
data: '''
|
|
||||||
${l10n.madeWithLove('[lollipopkit](${Urls.myGithub})')}
|
|
||||||
|
|
||||||
#### Contributors
|
|
||||||
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}
|
|
||||||
|
|
||||||
#### Participants
|
|
||||||
${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
|
||||||
|
|
||||||
#### My other apps
|
|
||||||
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box)
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIcon() {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 57, maxWidth: 57),
|
|
||||||
child: UIs.appIcon,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,35 +207,4 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLongPressSetting() async {
|
|
||||||
final map = Stores.setting.box.toJson(includeInternal: false);
|
|
||||||
final keys = map.keys;
|
|
||||||
|
|
||||||
/// Encode [map] to String with indent `\t`
|
|
||||||
final text = Miscs.jsonEncoder.convert(map);
|
|
||||||
final result = await AppRoutes.editor(
|
|
||||||
text: text,
|
|
||||||
langCode: 'json',
|
|
||||||
title: libL10n.setting,
|
|
||||||
).go<String>(context);
|
|
||||||
if (result == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final newSettings = json.decode(result) as Map<String, dynamic>;
|
|
||||||
Stores.setting.box.putAll(newSettings);
|
|
||||||
final newKeys = newSettings.keys;
|
|
||||||
final removedKeys = keys.where((e) => !newKeys.contains(e));
|
|
||||||
for (final key in removedKeys) {
|
|
||||||
Stores.setting.box.delete(key);
|
|
||||||
}
|
|
||||||
} catch (e, trace) {
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.error,
|
|
||||||
child: Text('${l10n.save}:\n$e'),
|
|
||||||
);
|
|
||||||
Loggers.app.warning('Update json settings failed', e, trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ class _IPerfPageState extends State<IPerfPage> {
|
|||||||
final _hostCtrl = TextEditingController();
|
final _hostCtrl = TextEditingController();
|
||||||
final _portCtrl = TextEditingController();
|
final _portCtrl = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_hostCtrl.dispose();
|
||||||
|
_portCtrl.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ class _PingPageState extends State<PingPage>
|
|||||||
final _results = ValueNotifier(<PingResult>[]);
|
final _results = ValueNotifier(<PingResult>[]);
|
||||||
bool get isInit => _results.value.isEmpty;
|
bool get isInit => _results.value.isEmpty;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_textEditingController = TextEditingController(text: '');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -36,6 +30,12 @@ class _PingPageState extends State<PingPage>
|
|||||||
_results.dispose();
|
_results.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_textEditingController = TextEditingController(text: '');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
|||||||
@@ -34,6 +34,18 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
|
|
||||||
final _loading = ValueNotifier<Widget?>(null);
|
final _loading = ValueNotifier<Widget?>(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_keyController.dispose();
|
||||||
|
_pwdController.dispose();
|
||||||
|
_nameNode.dispose();
|
||||||
|
_keyNode.dispose();
|
||||||
|
_pwdNode.dispose();
|
||||||
|
_loading.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -51,17 +63,6 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_nameController.dispose();
|
|
||||||
_keyController.dispose();
|
|
||||||
_pwdController.dispose();
|
|
||||||
_nameNode.dispose();
|
|
||||||
_keyNode.dispose();
|
|
||||||
_pwdNode.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
@@ -199,7 +200,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
_loading.value = SizedLoading.centerMedium;
|
_loading.value = SizedLoading.medium;
|
||||||
try {
|
try {
|
||||||
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
|
final decrypted = await Computer.shared.start(decyptPem, [key, pwd]);
|
||||||
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
final pki = PrivateKeyInfo(id: name, key: decrypted);
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
|
||||||
title: Text(l10n.privateKey),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
ProcSortMode _procSortMode = ProcSortMode.cpu;
|
ProcSortMode _procSortMode = ProcSortMode.cpu;
|
||||||
List<ProcSortMode> _sortModes = List.from(ProcSortMode.values);
|
List<ProcSortMode> _sortModes = List.from(ProcSortMode.values);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -75,12 +81,6 @@ class _ProcessPageState extends State<ProcessPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_timer.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
late MediaQueryData _media;
|
late MediaQueryData _media;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_timer?.cancel();
|
||||||
|
_pve.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
@@ -42,13 +49,6 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
_afterInit();
|
_afterInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_timer?.cancel();
|
|
||||||
_pve.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:server_box/data/model/server/sensors.dart';
|
|||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/system.dart';
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/view/page/server/edit.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|
||||||
import 'package:server_box/core/route.dart';
|
import 'package:server_box/core/route.dart';
|
||||||
@@ -61,6 +62,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
late final _collapse = _settings.collapseUIDefault.fetch();
|
late final _collapse = _settings.collapseUIDefault.fetch();
|
||||||
late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());
|
late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_netSortType.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
@@ -117,15 +124,15 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
return CustomAppBar(
|
return CustomAppBar(
|
||||||
title: Text(si.spi.name),
|
title: Text(si.spi.name),
|
||||||
actions: [
|
actions: [
|
||||||
ShareBtn(
|
QrShareBtn(
|
||||||
data: si.spi.toJsonString(),
|
data: si.spi.toJsonString(),
|
||||||
tip: si.spi.name,
|
tip: si.spi.name,
|
||||||
tip2: '${libL10n.share} ${l10n.server} ~ ServerBox',
|
tip2: '${l10n.server} ~ ServerBox',
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final delete = await AppRoutes.serverEdit(spi: si.spi).go(context);
|
final delete = await ServerEditPage.route.go(context, args: si.spi);
|
||||||
if (delete == true) {
|
if (delete == true) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,24 @@ import 'package:server_box/data/provider/server.dart';
|
|||||||
import 'package:server_box/core/route.dart';
|
import 'package:server_box/core/route.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/private_key.dart';
|
import 'package:server_box/data/provider/private_key.dart';
|
||||||
|
import 'package:server_box/data/store/server.dart';
|
||||||
|
|
||||||
class ServerEditPage extends StatefulWidget {
|
class ServerEditPage extends StatefulWidget {
|
||||||
const ServerEditPage({super.key, this.spi});
|
final Spi? args;
|
||||||
|
|
||||||
final Spi? spi;
|
const ServerEditPage({super.key, this.args});
|
||||||
|
|
||||||
|
static const route = AppRoute<bool, Spi>(
|
||||||
|
page: ServerEditPage.new,
|
||||||
|
path: '/server_edit',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ServerEditPage> createState() => _ServerEditPageState();
|
State<ServerEditPage> createState() => _ServerEditPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||||
|
late final spi = widget.args;
|
||||||
final _nameController = TextEditingController();
|
final _nameController = TextEditingController();
|
||||||
final _ipController = TextEditingController();
|
final _ipController = TextEditingController();
|
||||||
final _altUrlController = TextEditingController();
|
final _altUrlController = TextEditingController();
|
||||||
@@ -47,6 +54,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
|
|
||||||
late FocusScopeNode _focusScope;
|
late FocusScopeNode _focusScope;
|
||||||
|
|
||||||
|
/// -1: non selected, null: password, others: index of private key
|
||||||
final _keyIdx = ValueNotifier<int?>(null);
|
final _keyIdx = ValueNotifier<int?>(null);
|
||||||
final _autoConnect = ValueNotifier(true);
|
final _autoConnect = ValueNotifier(true);
|
||||||
final _jumpServer = nvn<String?>();
|
final _jumpServer = nvn<String?>();
|
||||||
@@ -64,12 +72,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
_portController.dispose();
|
_portController.dispose();
|
||||||
_usernameController.dispose();
|
_usernameController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_nameFocus.dispose();
|
|
||||||
_ipFocus.dispose();
|
|
||||||
_alterUrlFocus.dispose();
|
|
||||||
_portFocus.dispose();
|
|
||||||
_usernameFocus.dispose();
|
|
||||||
_pveAddrCtrl.dispose();
|
|
||||||
_preferTempDevCtrl.dispose();
|
_preferTempDevCtrl.dispose();
|
||||||
_logoUrlCtrl.dispose();
|
_logoUrlCtrl.dispose();
|
||||||
_wolMacCtrl.dispose();
|
_wolMacCtrl.dispose();
|
||||||
@@ -77,6 +79,21 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
_wolPwdCtrl.dispose();
|
_wolPwdCtrl.dispose();
|
||||||
_netDevCtrl.dispose();
|
_netDevCtrl.dispose();
|
||||||
_scriptDirCtrl.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,7 +105,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final actions = <Widget>[];
|
final actions = <Widget>[];
|
||||||
if (widget.spi != null) actions.add(_buildDelBtn());
|
if (spi != null) actions.add(_buildDelBtn());
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _focusScope.unfocus(),
|
onTap: () => _focusScope.unfocus(),
|
||||||
@@ -277,7 +294,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
final res = await KvEditor.route.go(
|
final res = await KvEditor.route.go(
|
||||||
context,
|
context,
|
||||||
args: KvEditorArgs(data: widget.spi?.envs ?? {}),
|
KvEditorArgs(data: spi?.envs ?? {}),
|
||||||
);
|
);
|
||||||
if (res == null) return;
|
if (res == null) return;
|
||||||
_env.value = res;
|
_env.value = res;
|
||||||
@@ -409,7 +426,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
final res = await KvEditor.route.go(
|
final res = await KvEditor.route.go(
|
||||||
context,
|
context,
|
||||||
args: KvEditorArgs(data: _customCmds.value),
|
KvEditorArgs(data: _customCmds.value),
|
||||||
);
|
);
|
||||||
if (res == null) return;
|
if (res == null) return;
|
||||||
_customCmds.value = res;
|
_customCmds.value = res;
|
||||||
@@ -477,7 +494,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
final srvs = ServerProvider.servers.values
|
final srvs = ServerProvider.servers.values
|
||||||
.map((e) => e.value)
|
.map((e) => e.value)
|
||||||
.where((e) => e.spi.jumpId == null)
|
.where((e) => e.spi.jumpId == null)
|
||||||
.where((e) => e.spi.id != widget.spi?.id)
|
.where((e) => e.spi.id != spi?.id)
|
||||||
.toList();
|
.toList();
|
||||||
final choice = _jumpServer.listenVal(
|
final choice = _jumpServer.listenVal(
|
||||||
(val) {
|
(val) {
|
||||||
@@ -602,10 +619,15 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
envs: _env.value.isEmpty ? null : _env.value,
|
envs: _env.value.isEmpty ? null : _env.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.spi == null) {
|
if (this.spi == null) {
|
||||||
|
final existsIds = ServerStore.instance.box.keys;
|
||||||
|
if (existsIds.contains(spi.id)) {
|
||||||
|
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
ServerProvider.addServer(spi);
|
ServerProvider.addServer(spi);
|
||||||
} else {
|
} else {
|
||||||
ServerProvider.updateServer(widget.spi!, spi);
|
ServerProvider.updateServer(this.spi!, spi);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.pop();
|
context.pop();
|
||||||
@@ -613,9 +635,8 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void afterFirstLayout(BuildContext context) {
|
void afterFirstLayout(BuildContext context) {
|
||||||
final spi = widget.spi;
|
|
||||||
if (spi != null) {
|
if (spi != null) {
|
||||||
_initWithSpi(spi);
|
_initWithSpi(spi!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,7 +649,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
_passwordController.text = spi.pwd ?? '';
|
_passwordController.text = spi.pwd ?? '';
|
||||||
} else {
|
} else {
|
||||||
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
|
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
|
||||||
(e) => e.id == widget.spi!.keyId,
|
(e) => e.id == spi.keyId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,11 +703,11 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
text: libL10n.import,
|
text: libL10n.import,
|
||||||
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final codes = await BarcodeScannerPage.route.go(
|
final ret = await BarcodeScannerPage.route.go(
|
||||||
context,
|
context,
|
||||||
args: const BarcodeScannerPageArgs(),
|
args: const BarcodeScannerPageArgs(),
|
||||||
);
|
);
|
||||||
final code = codes?.firstOrNull?.rawValue;
|
final code = ret?.text;
|
||||||
if (code == null) return;
|
if (code == null) return;
|
||||||
try {
|
try {
|
||||||
final spi = Spi.fromJson(json.decode(code));
|
final spi = Spi.fromJson(json.decode(code));
|
||||||
@@ -705,15 +726,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.showRoundDialog(
|
context.showRoundDialog(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
child: StatefulBuilder(builder: (ctx, setState) {
|
child: Text(libL10n.askContinue(
|
||||||
return Text(libL10n.askContinue(
|
'${libL10n.delete} ${l10n.server}(${spi!.name})',
|
||||||
'${libL10n.delete} ${l10n.server}(${widget.spi!.name})',
|
)),
|
||||||
));
|
|
||||||
}),
|
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.pop();
|
context.pop();
|
||||||
ServerProvider.delServer(widget.spi!.id);
|
ServerProvider.delServer(spi!.id);
|
||||||
context.pop(true);
|
context.pop(true);
|
||||||
},
|
},
|
||||||
red: true,
|
red: true,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import 'package:server_box/core/extension/context/locale.dart';
|
|||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
import 'package:server_box/core/extension/ssh_client.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.dart';
|
import 'package:server_box/data/model/app/shell_func.dart';
|
||||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
import 'package:server_box/data/model/server/try_limiter.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/edit.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/core/route.dart';
|
import 'package:server_box/core/route.dart';
|
||||||
@@ -18,6 +21,8 @@ import 'package:server_box/data/model/server/server_private_info.dart';
|
|||||||
import 'package:server_box/data/provider/server.dart';
|
import 'package:server_box/data/provider/server.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|
||||||
|
part 'top_bar.dart';
|
||||||
|
|
||||||
class ServerPage extends StatefulWidget {
|
class ServerPage extends StatefulWidget {
|
||||||
const ServerPage({super.key});
|
const ServerPage({super.key});
|
||||||
|
|
||||||
@@ -46,6 +51,15 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
final _autoHideKey = GlobalKey<AutoHideState>();
|
final _autoHideKey = GlobalKey<AutoHideState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_timer?.cancel();
|
||||||
|
_scrollController.dispose();
|
||||||
|
_autoHideKey.currentState?.dispose();
|
||||||
|
_tag.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -84,7 +98,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
|
|
||||||
Widget _buildPortrait() {
|
Widget _buildPortrait() {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: TagSwitcher(
|
appBar: _TopBar(
|
||||||
tags: ServerProvider.tags,
|
tags: ServerProvider.tags,
|
||||||
onTagChanged: (p0) => _tag.value = p0,
|
onTagChanged: (p0) => _tag.value = p0,
|
||||||
initTag: _tag.value,
|
initTag: _tag.value,
|
||||||
@@ -107,7 +121,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
heroTag: 'addServer',
|
heroTag: 'addServer',
|
||||||
onPressed: () => AppRoutes.serverEdit().go(context),
|
onPressed: () => ServerEditPage.route.go(context),
|
||||||
tooltip: libL10n.add,
|
tooltip: libL10n.add,
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
@@ -129,7 +143,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () => AppRoutes.settings().go(context),
|
onPressed: () => SettingsPage.route.go(context),
|
||||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -259,7 +273,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
if (srv.canViewDetails) {
|
if (srv.canViewDetails) {
|
||||||
AppRoutes.serverDetail(spi: srv.spi).go(context);
|
AppRoutes.serverDetail(spi: srv.spi).go(context);
|
||||||
} else {
|
} else {
|
||||||
AppRoutes.serverEdit(spi: srv.spi).go(context);
|
ServerEditPage.route.go(context, args: srv.spi);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
@@ -270,7 +284,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
flip: !cardStatus.value.flip,
|
flip: !cardStatus.value.flip,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
AppRoutes.serverEdit(spi: srv.spi).go(context);
|
ServerEditPage.route.go(context, args: srv.spi);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -314,15 +328,24 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final height = _calcCardHeight(srv.conn, cardStatus.value.flip);
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 377),
|
duration: const Duration(milliseconds: 377),
|
||||||
curve: Curves.fastEaseInToSlowEaseOut,
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
height: _calcCardHeight(srv.conn, cardStatus.value.flip),
|
height: height,
|
||||||
child: Column(
|
// Use [OverflowBox] to dismiss the warning of [Column] overflow.
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: OverflowBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
// If `height == _kCardHeightMin`, the `maxHeight` will be ignored.
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
//
|
||||||
children: children,
|
// You can comment the `maxHeight` then connect&disconnect the server
|
||||||
|
// to see the difference.
|
||||||
|
maxHeight: height != _kCardHeightMin ? height : null,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -384,7 +407,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
),
|
),
|
||||||
Btn.column(
|
Btn.column(
|
||||||
onTap: () => AppRoutes.serverEdit(spi: srv.spi).go(context),
|
onTap: () => ServerEditPage.route.go(context, args: srv.spi),
|
||||||
icon: const Icon(Icons.edit, color: Colors.grey),
|
icon: const Icon(Icons.edit, color: Colors.grey),
|
||||||
text: libL10n.edit,
|
text: libL10n.edit,
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
@@ -472,30 +495,18 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
ServerConn.failed => (
|
ServerConn.failed => (
|
||||||
const Icon(
|
const Icon(Icons.refresh, size: 21, color: Colors.grey),
|
||||||
Icons.refresh,
|
|
||||||
size: 21,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
() {
|
() {
|
||||||
TryLimiter.reset(s.spi.id);
|
TryLimiter.reset(s.spi.id);
|
||||||
ServerProvider.refresh(spi: s.spi);
|
ServerProvider.refresh(spi: s.spi);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ServerConn.disconnected => (
|
ServerConn.disconnected => (
|
||||||
const Icon(
|
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
|
||||||
MingCute.link_3_line,
|
|
||||||
size: 19,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
() => ServerProvider.refresh(spi: s.spi)
|
() => ServerProvider.refresh(spi: s.spi)
|
||||||
),
|
),
|
||||||
ServerConn.finished => (
|
ServerConn.finished => (
|
||||||
const Icon(
|
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
|
||||||
MingCute.unlink_2_line,
|
|
||||||
size: 17,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
() => ServerProvider.closeServer(id: s.spi.id),
|
() => ServerProvider.closeServer(id: s.spi.id),
|
||||||
),
|
),
|
||||||
_ when Stores.setting.serverTabUseOldUI.fetch() => (
|
_ when Stores.setting.serverTabUseOldUI.fetch() => (
|
||||||
@@ -507,11 +518,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
// Or the loading icon will be rescaled.
|
// Or the loading icon will be rescaled.
|
||||||
final wrapped = child is SizedBox
|
final wrapped = child is SizedBox
|
||||||
? child
|
? child
|
||||||
: SizedBox(
|
: SizedBox(height: _kCardHeightMin, width: 27, child: child);
|
||||||
height: _kCardHeightMin,
|
|
||||||
width: 27,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(7),
|
||||||
@@ -644,7 +651,7 @@ ${ss.err?.message ?? 'null'}
|
|||||||
|
|
||||||
List<String> _filterServers(List<String> order) {
|
List<String> _filterServers(List<String> order) {
|
||||||
final tag = _tag.value;
|
final tag = _tag.value;
|
||||||
if (tag == kDefaultTag) return order;
|
if (tag == TagSwitcher.kDefaultTag) return order;
|
||||||
return order.where((e) {
|
return order.where((e) {
|
||||||
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
||||||
if (tags == null) return false;
|
if (tags == null) return false;
|
||||||
|
|||||||
62
lib/view/page/server/top_bar.dart
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
part of 'tab.dart';
|
||||||
|
|
||||||
|
final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
final ValueNotifier<Set<String>> tags;
|
||||||
|
final void Function(String) onTagChanged;
|
||||||
|
final String initTag;
|
||||||
|
|
||||||
|
const _TopBar({
|
||||||
|
required this.initTag,
|
||||||
|
required this.onTagChanged,
|
||||||
|
required this.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(13),
|
||||||
|
onTap: () => DebugPage.route.go(
|
||||||
|
context,
|
||||||
|
args: const DebugPageArgs(title: 'Logs(${BuildData.build})'),
|
||||||
|
),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 7),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
BuildData.name,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.keyboard_arrow_right,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: 17,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 30),
|
||||||
|
TagSwitcher(
|
||||||
|
tags: tags,
|
||||||
|
onTagChanged: onTagChanged,
|
||||||
|
initTag: initTag,
|
||||||
|
singleLine: true,
|
||||||
|
reversed: true,
|
||||||
|
).expanded(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(TagSwitcher.kTagBtnHeight);
|
||||||
|
}
|
||||||