mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 12:44:59 +01:00
Compare commits
43 Commits
v1.0.1262
...
lollipopki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0928e2f6 | ||
|
|
61f161d8a6 | ||
|
|
52c80795f4 | ||
|
|
09f1ab2cf2 | ||
|
|
2eeb55c1d8 | ||
|
|
6738ac94f8 | ||
|
|
827d40b8b5 | ||
|
|
928f2becf1 | ||
|
|
7d30af44d6 | ||
|
|
35349a90eb | ||
|
|
8be9b9b10b | ||
|
|
c51cf62015 | ||
|
|
8589b3b4d7 | ||
|
|
7693e30cbf | ||
|
|
874d28be12 | ||
|
|
06070c29b9 | ||
|
|
bb0ada12e6 | ||
|
|
9ceeaf7cc4 | ||
|
|
29a57ad742 | ||
|
|
2c495a44c3 | ||
|
|
cc300c141a | ||
|
|
26efb8e185 | ||
|
|
06ed38ff45 | ||
|
|
7c35abe30e | ||
|
|
78ef181d4a | ||
|
|
3f15caeaf2 | ||
|
|
6458e736fa | ||
|
|
99fda8b747 | ||
|
|
c5cbb12ac3 | ||
|
|
038f0d4d77 | ||
|
|
141519d952 | ||
|
|
75d1a59e77 | ||
|
|
ca4e65d7a5 | ||
|
|
ffda27d057 | ||
|
|
c548b4ef48 | ||
|
|
70040c5840 | ||
|
|
5272324be6 | ||
|
|
8cbb48ed67 | ||
|
|
03720fa322 | ||
|
|
0b51719070 | ||
|
|
a84231393d | ||
|
|
d6c2cafce7 | ||
|
|
729b76177e |
16
.github/workflows/analysis.yml
vendored
16
.github/workflows/analysis.yml
vendored
@@ -16,27 +16,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
cache: true
|
|
||||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
|
||||||
|
|
||||||
- name: Cache pub dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ env.PUB_CACHE }}
|
|
||||||
~/.pub-cache
|
|
||||||
.dart_tool/package_config.json
|
|
||||||
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-${{ hashFiles('**/pubspec.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}-
|
|
||||||
${{ runner.os }}-pub-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -17,15 +17,15 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
releaseAndroid:
|
releaseAndroid:
|
||||||
name: Release android
|
name: Release android
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.35.3"
|
flutter-version: "3.38.0"
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
@@ -53,10 +53,10 @@ jobs:
|
|||||||
|
|
||||||
releaseLinux:
|
releaseLinux:
|
||||||
name: Release linux
|
name: Release linux
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
# runs-on: macos-latest
|
# runs-on: macos-latest
|
||||||
# steps:
|
# steps:
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v6
|
||||||
# - name: Install Flutter
|
# - name: Install Flutter
|
||||||
# uses: subosito/flutter-action@v2
|
# uses: subosito/flutter-action@v2
|
||||||
# - name: Build
|
# - name: Build
|
||||||
|
|||||||
143
LICENSE
143
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
@@ -5,7 +5,7 @@ English | [简体中文](README_zh.md)
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
|
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
|
||||||
<img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
|
<img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
|
||||||
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
|
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-yellow">
|
||||||
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,4 +85,4 @@ If I forgot to add your name to the contributors list, please add a comment in t
|
|||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
`GPL v3 lollipopkit`
|
`AGPL v3 lollipopkit & all contributors`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
|
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
|
||||||
<img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
|
<img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
|
||||||
<img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
|
<img alt="license" src="https://img.shields.io/badge/证书-AGPLv3-yellow">
|
||||||
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,4 +86,4 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
|||||||
|
|
||||||
## 📝 协议
|
## 📝 协议
|
||||||
|
|
||||||
`GPL v3 lollipopkit`
|
`AGPL v3 lollipopkit & 所有贡献者`
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.6.0' apply false
|
id "com.android.application" version '8.9.1' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
fastlane/metadata/android/ru/full_description.txt
Normal file
7
fastlane/metadata/android/ru/full_description.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Проект на базе Flutter, предоставляющий диаграммы состояний серверов под Linux, Unix и Windows и инструменты для управления ими.
|
||||||
|
|
||||||
|
Особая благодарность dartssh2 и xterm.dart.
|
||||||
|
|
||||||
|
* Диаграмма состояния (ЦП, датчики, видеокарта…), SSH Term, SFTP, Docker, пакеты, процессы…
|
||||||
|
* Платформозависимые: биометрическая аутентификация, push-уведомления, виджет, приложение для watchOS…
|
||||||
|
* Многоязычная поддержка: English, 简体中文; Deutsch, 繁體中文, Indonesian, Français, Dutch; Español, Русский язык, Português, 日本語
|
||||||
1
fastlane/metadata/android/ru/short_description.txt
Normal file
1
fastlane/metadata/android/ru/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Приложение для мониторинга серверов и набор инструментов управления ими
|
||||||
@@ -88,19 +88,19 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||||
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
|
||||||
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
||||||
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
|
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
||||||
|
|
||||||
|
|||||||
@@ -748,7 +748,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -758,7 +758,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -884,7 +884,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -894,7 +894,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -912,7 +912,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -922,7 +922,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -956,7 +956,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
@@ -982,7 +982,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1018,7 +1018,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1031,7 +1031,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1054,7 +1054,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1066,7 +1066,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
@@ -1095,7 +1095,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1107,7 +1107,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||||
PRODUCT_NAME = ServerBox;
|
PRODUCT_NAME = ServerBox;
|
||||||
@@ -1133,7 +1133,7 @@
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1262;
|
CURRENT_PROJECT_VERSION = 1291;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1145,7 +1145,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1262;
|
MARKETING_VERSION = 1.0.1291;
|
||||||
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;
|
||||||
|
|||||||
36
lib/app.dart
36
lib/app.dart
@@ -3,6 +3,7 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:fl_lib/generated/l10n/lib_l10n.dart';
|
import 'package:fl_lib/generated/l10n/lib_l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/core/app_navigator.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/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
@@ -11,12 +12,20 @@ import 'package:server_box/view/page/home.dart';
|
|||||||
|
|
||||||
part 'intro.dart';
|
part 'intro.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatefulWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
late final Future<List<IntroPageBuilder>> _introFuture = _IntroPage.builders;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_setup(context);
|
_setup(context);
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: RNodes.app,
|
listenable: RNodes.app,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
@@ -31,6 +40,7 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _build(BuildContext context) {
|
Widget _build(BuildContext context) {
|
||||||
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
||||||
|
|
||||||
UIs.colorSeed = colorSeed;
|
UIs.colorSeed = colorSeed;
|
||||||
UIs.primaryColor = colorSeed;
|
UIs.primaryColor = colorSeed;
|
||||||
|
|
||||||
@@ -53,14 +63,31 @@ class MyApp extends StatelessWidget {
|
|||||||
Widget _buildDynamicColor(BuildContext context) {
|
Widget _buildDynamicColor(BuildContext context) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (light, dark) {
|
builder: (light, dark) {
|
||||||
final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
|
final lightSeed = light?.primary;
|
||||||
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
|
final darkSeed = dark?.primary;
|
||||||
|
|
||||||
|
final lightTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorSchemeSeed: lightSeed,
|
||||||
|
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||||
|
);
|
||||||
|
final darkTheme = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorSchemeSeed: darkSeed,
|
||||||
|
appBarTheme: AppBarTheme(scrolledUnderElevation: 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
if (context.isDark && dark != null) {
|
if (context.isDark && dark != null) {
|
||||||
UIs.primaryColor = dark.primary;
|
UIs.primaryColor = dark.primary;
|
||||||
UIs.colorSeed = dark.primary;
|
UIs.colorSeed = dark.primary;
|
||||||
} else if (!context.isDark && light != null) {
|
} else if (!context.isDark && light != null) {
|
||||||
UIs.primaryColor = light.primary;
|
UIs.primaryColor = light.primary;
|
||||||
UIs.colorSeed = light.primary;
|
UIs.colorSeed = light.primary;
|
||||||
|
} else {
|
||||||
|
final fallbackColor = Color(Stores.setting.colorSeed.fetch());
|
||||||
|
UIs.primaryColor = fallbackColor;
|
||||||
|
UIs.colorSeed = fallbackColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildApp(context, light: lightTheme, dark: darkTheme);
|
return _buildApp(context, light: lightTheme, dark: darkTheme);
|
||||||
@@ -80,6 +107,7 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
key: ValueKey(locale),
|
key: ValueKey(locale),
|
||||||
|
navigatorKey: AppNavigator.key,
|
||||||
builder: ResponsivePoints.builder,
|
builder: ResponsivePoints.builder,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
|
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
|
||||||
@@ -91,7 +119,7 @@ class MyApp extends StatelessWidget {
|
|||||||
theme: light.fixWindowsFont,
|
theme: light.fixWindowsFont,
|
||||||
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
||||||
home: FutureBuilder<List<IntroPageBuilder>>(
|
home: FutureBuilder<List<IntroPageBuilder>>(
|
||||||
future: _IntroPage.builders,
|
future: _introFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
context.setLibL10n();
|
context.setLibL10n();
|
||||||
final appL10n = AppLocalizations.of(context);
|
final appL10n = AppLocalizations.of(context);
|
||||||
|
|||||||
8
lib/core/app_navigator.dart
Normal file
8
lib/core/app_navigator.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Global navigator access used for cross-cutting flows (e.g. dialogs).
|
||||||
|
abstract final class AppNavigator {
|
||||||
|
static final key = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
static BuildContext? get context => key.currentContext;
|
||||||
|
}
|
||||||
@@ -35,8 +35,8 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Updating Android sessions: $payload');
|
Loggers.app.info('Updating Android sessions: $payload');
|
||||||
await _channel.invokeMethod('updateSessions', payload);
|
await _channel.invokeMethod('updateSessions', payload);
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
// ignore
|
Loggers.app.warning('Failed to update Android sessions', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,8 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
final res = await _channel.invokeMethod('isServiceRunning');
|
final res = await _channel.invokeMethod('isServiceRunning');
|
||||||
return res == true;
|
return res == true;
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to check if Android service is running', e, s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,9 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Starting iOS Live Activity: $payload');
|
Loggers.app.info('Starting iOS Live Activity: $payload');
|
||||||
await _channel.invokeMethod('startLiveActivity', payload);
|
await _channel.invokeMethod('startLiveActivity', payload);
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to start iOS Live Activity', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> updateLiveActivity(String payload) async {
|
static Future<void> updateLiveActivity(String payload) async {
|
||||||
@@ -65,7 +68,9 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Updating iOS Live Activity: $payload');
|
Loggers.app.info('Updating iOS Live Activity: $payload');
|
||||||
await _channel.invokeMethod('updateLiveActivity', payload);
|
await _channel.invokeMethod('updateLiveActivity', payload);
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to update iOS Live Activity', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> stopLiveActivity() async {
|
static Future<void> stopLiveActivity() async {
|
||||||
@@ -73,7 +78,9 @@ abstract final class MethodChans {
|
|||||||
try {
|
try {
|
||||||
Loggers.app.info('Stopping iOS Live Activity');
|
Loggers.app.info('Stopping iOS Live Activity');
|
||||||
await _channel.invokeMethod('stopLiveActivity');
|
await _channel.invokeMethod('stopLiveActivity');
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to stop iOS Live Activity', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a handler for native -> Flutter callbacks.
|
/// Register a handler for native -> Flutter callbacks.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
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:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:server_box/data/helper/ssh_decoder.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/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
@@ -170,4 +171,98 @@ extension SSHClientX on SSHClient {
|
|||||||
);
|
);
|
||||||
return ret.$2;
|
return ret.$2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a command and decodes output safely with encoding fallback
|
||||||
|
///
|
||||||
|
/// [systemType] - The system type (affects encoding choice)
|
||||||
|
/// Runs a command and safely decodes the result
|
||||||
|
Future<String> runSafe(
|
||||||
|
String command, {
|
||||||
|
SystemType? systemType,
|
||||||
|
String? context,
|
||||||
|
}) async {
|
||||||
|
// Let SSH errors propagate with their original type (e.g., SSHError subclasses)
|
||||||
|
final result = await run(command);
|
||||||
|
|
||||||
|
// Only catch decoding failures and add context
|
||||||
|
try {
|
||||||
|
return SSHDecoder.decode(
|
||||||
|
result,
|
||||||
|
isWindows: systemType == SystemType.windows,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to decode command output${context != null ? " [$context]" : ""}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes a command with stdin and safely decodes stdout/stderr
|
||||||
|
Future<(String stdout, String stderr)> execSafe(
|
||||||
|
void Function(SSHSession session) callback, {
|
||||||
|
required String entry,
|
||||||
|
SystemType? systemType,
|
||||||
|
String? context,
|
||||||
|
}) async {
|
||||||
|
final stdoutBuilder = BytesBuilder(copy: false);
|
||||||
|
final stderrBuilder = BytesBuilder(copy: false);
|
||||||
|
final stdoutDone = Completer<void>();
|
||||||
|
final stderrDone = Completer<void>();
|
||||||
|
|
||||||
|
final session = await execute(entry);
|
||||||
|
|
||||||
|
session.stdout.listen(
|
||||||
|
(e) {
|
||||||
|
stdoutBuilder.add(e);
|
||||||
|
},
|
||||||
|
onDone: stdoutDone.complete,
|
||||||
|
onError: stdoutDone.completeError,
|
||||||
|
);
|
||||||
|
|
||||||
|
session.stderr.listen(
|
||||||
|
(e) {
|
||||||
|
stderrBuilder.add(e);
|
||||||
|
},
|
||||||
|
onDone: stderrDone.complete,
|
||||||
|
onError: stderrDone.completeError,
|
||||||
|
);
|
||||||
|
|
||||||
|
callback(session);
|
||||||
|
|
||||||
|
await stdoutDone.future;
|
||||||
|
await stderrDone.future;
|
||||||
|
|
||||||
|
final stdoutBytes = stdoutBuilder.takeBytes();
|
||||||
|
final stderrBytes = stderrBuilder.takeBytes();
|
||||||
|
|
||||||
|
// Only catch decoding failures, let other errors propagate
|
||||||
|
String stdout;
|
||||||
|
try {
|
||||||
|
stdout = SSHDecoder.decode(
|
||||||
|
stdoutBytes,
|
||||||
|
isWindows: systemType == SystemType.windows,
|
||||||
|
context: context != null ? '$context (stdout)' : 'stdout',
|
||||||
|
);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to decode stdout${context != null ? " [$context]" : ""}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String stderr;
|
||||||
|
try {
|
||||||
|
stderr = SSHDecoder.decode(
|
||||||
|
stderrBytes,
|
||||||
|
isWindows: systemType == SystemType.windows,
|
||||||
|
context: context != null ? '$context (stderr)' : 'stderr',
|
||||||
|
);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to decode stderr${context != null ? " [$context]" : ""}: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (stdout, stderr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ class SshDiscoveryService {
|
|||||||
// Some tools return non-zero but still have useful output
|
// Some tools return non-zero but still have useful output
|
||||||
if (out.trim().isNotEmpty) return out;
|
if (out.trim().isNotEmpty) return out;
|
||||||
return null;
|
return null;
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to run command: $exe ${args.join(' ')}', e, s);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ class SshDiscoveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchCount == 0) {
|
if (matchCount == 0) {
|
||||||
lprint(
|
Loggers.app.warning(
|
||||||
'[ssh_discovery] Warning: No ARP entries parsed on macOS. Output may be unexpected or localized. Output sample: ${s.length > 100 ? '${s.substring(0, 100)}...' : s}',
|
'[ssh_discovery] Warning: No ARP entries parsed on macOS. Output may be unexpected or localized. Output sample: ${s.length > 100 ? '${s.substring(0, 100)}...' : s}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,8 +177,7 @@ class SshDiscoveryService {
|
|||||||
r'inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+0x([0-9a-fA-F]+)(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))?',
|
r'inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+0x([0-9a-fA-F]+)(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))?',
|
||||||
).firstMatch(line);
|
).firstMatch(line);
|
||||||
if (ipm == null) {
|
if (ipm == null) {
|
||||||
// Log unexpected format but continue processing other lines
|
Loggers.app.warning('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');
|
||||||
lprint('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final ip = InternetAddress(ipm.group(1)!);
|
final ip = InternetAddress(ipm.group(1)!);
|
||||||
@@ -190,7 +190,7 @@ class SshDiscoveryService {
|
|||||||
final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);
|
final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);
|
||||||
res.add(_Cidr(ip, prefix, mask, net, brd));
|
res.add(_Cidr(ip, prefix, mask, net, brd));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lprint('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');
|
Loggers.app.warning('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,9 @@ class SshDiscoveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to discover mDNS SSH candidates on macOS', e, s);
|
||||||
|
}
|
||||||
} else if (_isLinux) {
|
} else if (_isLinux) {
|
||||||
final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);
|
final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);
|
||||||
if (s != null) {
|
if (s != null) {
|
||||||
@@ -335,7 +337,8 @@ class _Scanner {
|
|||||||
);
|
);
|
||||||
final banner = await c.future.timeout(timeout, onTimeout: () => null);
|
final banner = await c.future.timeout(timeout, onTimeout: () => null);
|
||||||
return _ScanResult(ip, banner);
|
return _ScanResult(ip, banner);
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to probe SSH at ${ip.address}', e, s);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
sub?.cancel();
|
sub?.cancel();
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ final class BakSyncer extends SyncIface {
|
|||||||
return MergeableUtils.fromJsonString(content, pwd).$1;
|
return MergeableUtils.fromJsonString(content, pwd).$1;
|
||||||
}
|
}
|
||||||
return MergeableUtils.fromJsonString(content).$1;
|
return MergeableUtils.fromJsonString(content).$1;
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to parse backup file with password, trying without password', e, s);
|
||||||
// Fallback: try without password if detection failed
|
// Fallback: try without password if detection failed
|
||||||
return MergeableUtils.fromJsonString(content).$1;
|
return MergeableUtils.fromJsonString(content).$1;
|
||||||
}
|
}
|
||||||
|
|||||||
26
lib/core/utils/host_key_helper.dart
Normal file
26
lib/core/utils/host_key_helper.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:server_box/core/utils/server.dart';
|
||||||
|
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
|
Future<bool> ensureHostKeyAcceptedForSftp(BuildContext context, Spi spi) async {
|
||||||
|
final known = Stores.setting.sshKnownHostFingerprints.get();
|
||||||
|
final hostId = spi.id.isNotEmpty ? spi.id : spi.oldId;
|
||||||
|
final prefix = '$hostId::';
|
||||||
|
if (known.keys.any((key) => key.startsWith(prefix))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final (result, error) = await context.showLoadingDialog<bool>(
|
||||||
|
fn: () async {
|
||||||
|
await ensureKnownHostKey(
|
||||||
|
spi,
|
||||||
|
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi, ctx: context),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return error == null && result == true;
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
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:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:server_box/core/app_navigator.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.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';
|
||||||
@@ -29,11 +33,82 @@ enum GenSSHClientStatus { socket, key, pwd }
|
|||||||
String getPrivateKey(String id) {
|
String getPrivateKey(String id) {
|
||||||
final pki = Stores.key.fetchOne(id);
|
final pki = Stores.key.fetchOne(id);
|
||||||
if (pki == null) {
|
if (pki == null) {
|
||||||
throw SSHErr(type: SSHErrType.noPrivateKey, message: 'key [$id] not found');
|
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(id));
|
||||||
}
|
}
|
||||||
return pki.key;
|
return pki.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Spi> resolveMergedJumpChain(
|
||||||
|
Spi target, {
|
||||||
|
List<Spi>? jumpChain,
|
||||||
|
}) {
|
||||||
|
final injectedSpiMap = <String, Spi>{};
|
||||||
|
if (jumpChain != null) {
|
||||||
|
for (final s in jumpChain) {
|
||||||
|
injectedSpiMap[s.id] = s;
|
||||||
|
if (s.oldId.isNotEmpty) {
|
||||||
|
injectedSpiMap[s.oldId] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spi resolveSpi(String id) {
|
||||||
|
final injected = injectedSpiMap[id];
|
||||||
|
if (injected != null) return injected;
|
||||||
|
if (jumpChain != null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
|
||||||
|
}
|
||||||
|
final fromStore = Stores.server.box.get(id);
|
||||||
|
if (fromStore == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
|
||||||
|
}
|
||||||
|
return fromStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _resolveMergedJumpChainInternal(target, resolveSpi: resolveSpi);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Spi> _resolveMergedJumpChainInternal(
|
||||||
|
Spi target, {
|
||||||
|
required Spi Function(String id) resolveSpi,
|
||||||
|
}) {
|
||||||
|
final roots = target.jumpChainIds ?? (target.jumpId == null ? const <String>[] : [target.jumpId!]);
|
||||||
|
if (roots.isEmpty) return const <Spi>[];
|
||||||
|
|
||||||
|
final seen = <String>{};
|
||||||
|
final stack = <String>{};
|
||||||
|
final out = <Spi>[];
|
||||||
|
|
||||||
|
String normId(Spi spi) => spi.id.isNotEmpty ? spi.id : spi.oldId;
|
||||||
|
|
||||||
|
void dfs(String id) {
|
||||||
|
final hop = resolveSpi(id);
|
||||||
|
final norm = normId(hop);
|
||||||
|
|
||||||
|
if (stack.contains(norm)) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at $norm');
|
||||||
|
}
|
||||||
|
if (seen.contains(norm)) return;
|
||||||
|
|
||||||
|
stack.add(norm);
|
||||||
|
final deps = hop.jumpChainIds ?? (hop.jumpId == null ? const <String>[] : [hop.jumpId!]);
|
||||||
|
for (final dep in deps) {
|
||||||
|
dfs(dep);
|
||||||
|
}
|
||||||
|
stack.remove(norm);
|
||||||
|
|
||||||
|
if (seen.add(norm)) {
|
||||||
|
out.add(hop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final r in roots) {
|
||||||
|
dfs(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Future<SSHClient> genClient(
|
Future<SSHClient> genClient(
|
||||||
Spi spi, {
|
Spi spi, {
|
||||||
void Function(GenSSHClientStatus)? onStatus,
|
void Function(GenSSHClientStatus)? onStatus,
|
||||||
@@ -41,46 +116,187 @@ Future<SSHClient> genClient(
|
|||||||
/// Only pass this param if using multi-threading and key login
|
/// Only pass this param if using multi-threading and key login
|
||||||
String? privateKey,
|
String? privateKey,
|
||||||
|
|
||||||
/// Only pass this param if using multi-threading and key login
|
/// Pre-resolved jump chain (in `spi.jumpId` order: immediate -> farthest).
|
||||||
String? jumpPrivateKey,
|
|
||||||
Duration timeout = const Duration(seconds: 5),
|
|
||||||
|
|
||||||
/// [Spi] of the jump server
|
|
||||||
///
|
///
|
||||||
/// Must pass this param if using multi-threading and key login
|
/// This is mainly used when `Stores` is unavailable (e.g. in an isolate).
|
||||||
Spi? jumpSpi,
|
List<Spi>? jumpChain,
|
||||||
|
|
||||||
|
/// Private keys for [jumpChain], aligned by index.
|
||||||
|
///
|
||||||
|
/// If a jump server uses key auth (`keyId != null`), you must provide the
|
||||||
|
/// decrypted key pem here (or `genClient` will try to read from `Stores`).
|
||||||
|
List<String?>? jumpPrivateKeys,
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
|
||||||
/// Handle keyboard-interactive authentication
|
/// Handle keyboard-interactive authentication
|
||||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
|
Map<String, String>? knownHostFingerprints,
|
||||||
|
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||||
|
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||||
}) async {
|
}) async {
|
||||||
|
return _genClientInternal(
|
||||||
|
spi,
|
||||||
|
onStatus: onStatus,
|
||||||
|
privateKey: privateKey,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: knownHostFingerprints,
|
||||||
|
onHostKeyAccepted: onHostKeyAccepted,
|
||||||
|
onHostKeyPrompt: onHostKeyPrompt,
|
||||||
|
visited: <String>{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SSHClient> _genClientInternal(
|
||||||
|
Spi spi, {
|
||||||
|
void Function(GenSSHClientStatus)? onStatus,
|
||||||
|
String? privateKey,
|
||||||
|
List<Spi>? jumpChain,
|
||||||
|
List<String?>? jumpPrivateKeys,
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
|
Map<String, String>? knownHostFingerprints,
|
||||||
|
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||||
|
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||||
|
required Set<String> visited,
|
||||||
|
SSHSocket? socketOverride,
|
||||||
|
bool followJumpConfig = true,
|
||||||
|
}) async {
|
||||||
|
final identifier = _hostIdentifier(spi);
|
||||||
|
if (!visited.add(identifier)) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at ${spi.name} ($identifier)');
|
||||||
|
}
|
||||||
|
|
||||||
onStatus?.call(GenSSHClientStatus.socket);
|
onStatus?.call(GenSSHClientStatus.socket);
|
||||||
|
|
||||||
|
final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints());
|
||||||
|
final hostKeyPersist = onHostKeyAccepted ?? _persistHostKeyFingerprint;
|
||||||
|
final hostKeyPrompt = onHostKeyPrompt ?? _defaultHostKeyPrompt;
|
||||||
|
|
||||||
String? alterUser;
|
String? alterUser;
|
||||||
|
|
||||||
final socket = await () async {
|
final (socket, hopClients) = await () async {
|
||||||
// Proxy
|
if (socketOverride != null) return (socketOverride, <SSHClient>[]);
|
||||||
final jumpSpi_ = () {
|
|
||||||
// Multi-thread or key login
|
|
||||||
if (jumpSpi != null) return jumpSpi;
|
|
||||||
// Main thread
|
|
||||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
|
||||||
}();
|
|
||||||
if (jumpSpi_ != null) {
|
|
||||||
final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout);
|
|
||||||
|
|
||||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
if (followJumpConfig) {
|
||||||
|
final injectedSpiMap = <String, Spi>{};
|
||||||
|
final injectedKeyMap = <String, String?>{};
|
||||||
|
|
||||||
|
if (jumpChain != null) {
|
||||||
|
for (var i = 0; i < jumpChain.length; i++) {
|
||||||
|
final s = jumpChain[i];
|
||||||
|
injectedSpiMap[s.id] = s;
|
||||||
|
if (s.oldId.isNotEmpty) injectedSpiMap[s.oldId] = s;
|
||||||
|
if (jumpPrivateKeys != null && i < jumpPrivateKeys.length) {
|
||||||
|
injectedKeyMap[s.id] = jumpPrivateKeys[i];
|
||||||
|
if (s.oldId.isNotEmpty) injectedKeyMap[s.oldId] = jumpPrivateKeys[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spi resolveSpi(String id) {
|
||||||
|
final injected = injectedSpiMap[id];
|
||||||
|
if (injected != null) return injected;
|
||||||
|
if (jumpChain != null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
|
||||||
|
}
|
||||||
|
final fromStore = Stores.server.box.get(id);
|
||||||
|
if (fromStore == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
|
||||||
|
}
|
||||||
|
return fromStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? resolveHopPrivateKey(Spi hop) {
|
||||||
|
final keyId = hop.keyId;
|
||||||
|
if (keyId == null) return null;
|
||||||
|
final injected = injectedKeyMap[hop.id] ?? injectedKeyMap[hop.oldId];
|
||||||
|
return injected ?? getPrivateKey(keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hops = _resolveMergedJumpChainInternal(spi, resolveSpi: resolveSpi);
|
||||||
|
if (hops.isNotEmpty) {
|
||||||
|
// Build multi-hop forward chain with dedup/merge.
|
||||||
|
final createdClients = <SSHClient>[];
|
||||||
|
SSHClient? currentClient;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final firstHop = hops.first;
|
||||||
|
final firstKey = resolveHopPrivateKey(firstHop);
|
||||||
|
if (firstHop.keyId != null && firstKey == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(firstHop.keyId ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
currentClient = await _genClientInternal(
|
||||||
|
firstHop,
|
||||||
|
privateKey: firstKey,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: hostKeyCache,
|
||||||
|
onHostKeyAccepted: hostKeyPersist,
|
||||||
|
onHostKeyPrompt: hostKeyPrompt,
|
||||||
|
visited: visited,
|
||||||
|
followJumpConfig: false,
|
||||||
|
);
|
||||||
|
createdClients.add(currentClient);
|
||||||
|
|
||||||
|
for (var i = 1; i < hops.length; i++) {
|
||||||
|
final hop = hops[i];
|
||||||
|
final forwarded = await currentClient!.forwardLocal(hop.ip, hop.port);
|
||||||
|
final hopKey = resolveHopPrivateKey(hop);
|
||||||
|
if (hop.keyId != null && hopKey == null) {
|
||||||
|
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(hop.keyId ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
currentClient = await _genClientInternal(
|
||||||
|
hop,
|
||||||
|
privateKey: hopKey,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: hostKeyCache,
|
||||||
|
onHostKeyAccepted: hostKeyPersist,
|
||||||
|
onHostKeyPrompt: hostKeyPrompt,
|
||||||
|
visited: visited,
|
||||||
|
socketOverride: forwarded,
|
||||||
|
followJumpConfig: false,
|
||||||
|
);
|
||||||
|
createdClients.add(currentClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
final forwardedSocket = await currentClient!.forwardLocal(spi.ip, spi.port);
|
||||||
|
return (forwardedSocket, createdClients);
|
||||||
|
} catch (e) {
|
||||||
|
// Close all created clients on error to avoid leaks
|
||||||
|
for (final client in createdClients) {
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore close errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// Note: On success, all intermediate clients must remain open
|
||||||
|
// because the returned socket tunnels through them.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct
|
// Direct
|
||||||
try {
|
try {
|
||||||
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
return (await SSHSocket.connect(spi.ip, spi.port, timeout: timeout), <SSHClient>[]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('genClient', e);
|
Loggers.app.warning('genClient', e);
|
||||||
if (spi.alterUrl == null) rethrow;
|
if (spi.alterUrl == null) rethrow;
|
||||||
try {
|
try {
|
||||||
final res = spi.parseAlterUrl();
|
final res = spi.parseAlterUrl();
|
||||||
alterUser = res.$2;
|
alterUser = res.$2;
|
||||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
return (await SSHSocket.connect(res.$1, res.$3, timeout: timeout), <SSHClient>[]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Loggers.app.warning('genClient alterUrl', e);
|
Loggers.app.warning('genClient alterUrl', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -88,28 +304,307 @@ Future<SSHClient> genClient(
|
|||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
final keyId = spi.keyId;
|
final hostKeyVerifier = _HostKeyVerifier(
|
||||||
if (keyId == null) {
|
spi: spi,
|
||||||
onStatus?.call(GenSSHClientStatus.pwd);
|
cache: hostKeyCache,
|
||||||
|
persistCallback: hostKeyPersist,
|
||||||
|
prompt: hostKeyPrompt,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<SSHClient> buildClient(SSHSocket socket) async {
|
||||||
|
final keyId = spi.keyId;
|
||||||
|
if (keyId == null) {
|
||||||
|
onStatus?.call(GenSSHClientStatus.pwd);
|
||||||
|
return SSHClient(
|
||||||
|
socket,
|
||||||
|
username: alterUser ?? spi.user,
|
||||||
|
onPasswordRequest: () => spi.pwd,
|
||||||
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
|
// printDebug: debugPrint,
|
||||||
|
// printTrace: debugPrint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
privateKey ??= getPrivateKey(keyId);
|
||||||
|
|
||||||
|
onStatus?.call(GenSSHClientStatus.key);
|
||||||
return SSHClient(
|
return SSHClient(
|
||||||
socket,
|
socket,
|
||||||
username: alterUser ?? spi.user,
|
username: spi.user,
|
||||||
onPasswordRequest: () => spi.pwd,
|
// Must use [compute] here, instead of [Computer.shared.start]
|
||||||
|
identities: await compute(loadIndentity, privateKey!),
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
// printDebug: debugPrint,
|
// printDebug: debugPrint,
|
||||||
// printTrace: debugPrint,
|
// printTrace: debugPrint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
privateKey ??= getPrivateKey(keyId);
|
|
||||||
|
|
||||||
onStatus?.call(GenSSHClientStatus.key);
|
final client = await buildClient(socket);
|
||||||
return SSHClient(
|
|
||||||
socket,
|
// Tie hop clients' lifetime to the final client: close all hop clients
|
||||||
username: spi.user,
|
// when the target client disconnects to avoid leaking SSH connections.
|
||||||
// Must use [compute] here, instead of [Computer.shared.start]
|
if (hopClients.isNotEmpty) {
|
||||||
identities: await compute(loadIndentity, privateKey),
|
client.done.whenComplete(() {
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
for (final hopClient in hopClients) {
|
||||||
// printDebug: debugPrint,
|
try {
|
||||||
// printTrace: debugPrint,
|
hopClient.close();
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore close errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef _HostKeyPersistCallback = void Function(String storageKey, String fingerprintHex);
|
||||||
|
|
||||||
|
class HostKeyPromptInfo {
|
||||||
|
HostKeyPromptInfo({
|
||||||
|
required this.spi,
|
||||||
|
required this.keyType,
|
||||||
|
required this.fingerprintHex,
|
||||||
|
required this.fingerprintBase64,
|
||||||
|
required this.isMismatch,
|
||||||
|
this.previousFingerprintHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Spi spi;
|
||||||
|
final String keyType;
|
||||||
|
final String fingerprintHex;
|
||||||
|
final String fingerprintBase64;
|
||||||
|
final bool isMismatch;
|
||||||
|
final String? previousFingerprintHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HostKeyVerifier {
|
||||||
|
_HostKeyVerifier({
|
||||||
|
required this.spi,
|
||||||
|
required Map<String, String> cache,
|
||||||
|
required this.prompt,
|
||||||
|
this.persistCallback,
|
||||||
|
}) : _cache = cache;
|
||||||
|
|
||||||
|
final Spi spi;
|
||||||
|
final Map<String, String> _cache;
|
||||||
|
final _HostKeyPersistCallback? persistCallback;
|
||||||
|
final Future<bool> Function(HostKeyPromptInfo info) prompt;
|
||||||
|
|
||||||
|
Future<bool> call(String keyType, Uint8List fingerprintBytes) async {
|
||||||
|
final storageKey = _hostKeyStorageKey(spi, keyType);
|
||||||
|
final fingerprintHex = _fingerprintToHex(fingerprintBytes);
|
||||||
|
final fingerprintBase64 = _fingerprintToBase64(fingerprintBytes);
|
||||||
|
final existing = _cache[storageKey];
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
final accepted = await prompt(
|
||||||
|
HostKeyPromptInfo(
|
||||||
|
spi: spi,
|
||||||
|
keyType: keyType,
|
||||||
|
fingerprintHex: fingerprintHex,
|
||||||
|
fingerprintBase64: fingerprintBase64,
|
||||||
|
isMismatch: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!accepted) {
|
||||||
|
Loggers.app.warning('User rejected new SSH host key for ${spi.name} ($keyType).');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_cache[storageKey] = fingerprintHex;
|
||||||
|
persistCallback?.call(storageKey, fingerprintHex);
|
||||||
|
Loggers.app.info('Trusted SSH host key for ${spi.name} ($keyType).');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing == fingerprintHex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final accepted = await prompt(
|
||||||
|
HostKeyPromptInfo(
|
||||||
|
spi: spi,
|
||||||
|
keyType: keyType,
|
||||||
|
fingerprintHex: fingerprintHex,
|
||||||
|
fingerprintBase64: fingerprintBase64,
|
||||||
|
isMismatch: true,
|
||||||
|
previousFingerprintHex: existing,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!accepted) {
|
||||||
|
Loggers.app.warning(
|
||||||
|
'SSH host key mismatch for ${spi.name}',
|
||||||
|
'expected $existing but received $fingerprintHex ($keyType)',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache[storageKey] = fingerprintHex;
|
||||||
|
persistCallback?.call(storageKey, fingerprintHex);
|
||||||
|
Loggers.app.warning('Updated stored SSH host key for ${spi.name} ($keyType) after user confirmation.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> _loadKnownHostFingerprints() {
|
||||||
|
try {
|
||||||
|
final prop = Stores.setting.sshKnownHostFingerprints;
|
||||||
|
return Map<String, String>.from(prop.get());
|
||||||
|
} catch (e, stack) {
|
||||||
|
Loggers.app.warning('Load SSH host key fingerprints failed', e, stack);
|
||||||
|
return <String, String>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _persistHostKeyFingerprint(String storageKey, String fingerprintHex) {
|
||||||
|
try {
|
||||||
|
final prop = Stores.setting.sshKnownHostFingerprints;
|
||||||
|
final updated = Map<String, String>.from(prop.get());
|
||||||
|
if (updated[storageKey] == fingerprintHex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updated[storageKey] = fingerprintHex;
|
||||||
|
prop.put(updated);
|
||||||
|
Loggers.app.info('Stored SSH host key fingerprint for $storageKey');
|
||||||
|
} catch (e, stack) {
|
||||||
|
Loggers.app.warning('Persist SSH host key fingerprint failed', e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _defaultHostKeyPrompt(HostKeyPromptInfo info) async {
|
||||||
|
final ctx = AppNavigator.context;
|
||||||
|
if (ctx == null) {
|
||||||
|
Loggers.app.warning('Host key prompt skipped: navigator context unavailable.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hostLine = '${info.spi.user}@${info.spi.ip}:${info.spi.port}';
|
||||||
|
final description = info.isMismatch
|
||||||
|
? l10n.sshHostKeyChangedDesc(info.spi.name)
|
||||||
|
: l10n.sshHostKeyNewDesc(info.spi.name);
|
||||||
|
|
||||||
|
final result = await ctx.showRoundDialog<bool>(
|
||||||
|
title: libL10n.attention,
|
||||||
|
barrierDismiss: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(description),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SelectableText('${l10n.server}: ${info.spi.name}'),
|
||||||
|
SelectableText('${libL10n.addr}: $hostLine'),
|
||||||
|
SelectableText('${l10n.sshHostKeyType}: ${info.keyType}'),
|
||||||
|
SelectableText(l10n.sshHostKeyFingerprintMd5Hex(info.fingerprintHex)),
|
||||||
|
SelectableText(l10n.sshHostKeyFingerprintMd5Base64(info.fingerprintBase64)),
|
||||||
|
if (info.previousFingerprintHex != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SelectableText(l10n.sshHostKeyStoredFingerprint(info.previousFingerprintHex!)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => ctx.pop(false), child: Text(libL10n.cancel)),
|
||||||
|
TextButton(onPressed: () => ctx.pop(true), child: Text(libL10n.ok)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureKnownHostKey(
|
||||||
|
Spi spi, {
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
|
}) async {
|
||||||
|
var cache = _loadKnownHostFingerprints();
|
||||||
|
|
||||||
|
final hops = resolveMergedJumpChain(spi);
|
||||||
|
|
||||||
|
// Check each hop's host key, routing through preceding hops
|
||||||
|
for (var i = 0; i < hops.length; i++) {
|
||||||
|
final hop = hops[i];
|
||||||
|
// Preceding hops needed to reach this hop
|
||||||
|
final precedingHops = i > 0 ? hops.sublist(0, i) : null;
|
||||||
|
final precedingKeys = precedingHops?.map((h) =>
|
||||||
|
h.keyId != null ? getPrivateKey(h.keyId!) : null
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
cache = await _ensureKnownHostKeyForSingle(
|
||||||
|
hop,
|
||||||
|
cache: cache,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
jumpChain: precedingHops,
|
||||||
|
jumpPrivateKeys: precedingKeys,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the target's host key, routing through all hops
|
||||||
|
final allKeys = hops.isNotEmpty
|
||||||
|
? hops.map((h) => h.keyId != null ? getPrivateKey(h.keyId!) : null).toList()
|
||||||
|
: null;
|
||||||
|
await _ensureKnownHostKeyForSingle(
|
||||||
|
spi,
|
||||||
|
cache: cache,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
jumpChain: hops.isNotEmpty ? hops : null,
|
||||||
|
jumpPrivateKeys: allKeys,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> _ensureKnownHostKeyForSingle(
|
||||||
|
Spi spi, {
|
||||||
|
required Map<String, String> cache,
|
||||||
|
Duration timeout = const Duration(seconds: 5),
|
||||||
|
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||||
|
List<Spi>? jumpChain,
|
||||||
|
List<String?>? jumpPrivateKeys,
|
||||||
|
}) async {
|
||||||
|
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
final client = await genClient(
|
||||||
|
spi,
|
||||||
|
timeout: timeout,
|
||||||
|
onKeyboardInteractive: onKeyboardInteractive,
|
||||||
|
knownHostFingerprints: cache,
|
||||||
|
jumpChain: jumpChain,
|
||||||
|
jumpPrivateKeys: jumpPrivateKeys,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.authenticated;
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.addAll(_loadKnownHostFingerprints());
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) {
|
||||||
|
final prefix = '${_hostIdentifier(spi)}::';
|
||||||
|
return cache.keys.any((key) => key.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
String _hostKeyStorageKey(Spi spi, String keyType) {
|
||||||
|
final base = _hostIdentifier(spi);
|
||||||
|
return '$base::$keyType';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _hostIdentifier(Spi spi) => spi.id.isNotEmpty ? spi.id : spi.oldId;
|
||||||
|
|
||||||
|
String _fingerprintToHex(Uint8List fingerprint) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (var i = 0; i < fingerprint.length; i++) {
|
||||||
|
if (i > 0) buffer.write(':');
|
||||||
|
buffer.write(fingerprint[i].toRadixString(16).padLeft(2, '0'));
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fingerprintToBase64(Uint8List fingerprint) => base64.encode(fingerprint);
|
||||||
|
|||||||
@@ -149,10 +149,12 @@ abstract final class SSHConfig {
|
|||||||
|
|
||||||
/// Extract jump host from ProxyJump or ProxyCommand
|
/// Extract jump host from ProxyJump or ProxyCommand
|
||||||
static String? _extractJumpHost(String value) {
|
static String? _extractJumpHost(String value) {
|
||||||
|
if (value.isEmpty) return null;
|
||||||
// For ProxyJump, the format is usually: user@host:port
|
// For ProxyJump, the format is usually: user@host:port
|
||||||
// For ProxyCommand, it's more complex and might need custom parsing
|
// For ProxyCommand, it's more complex and might need custom parsing
|
||||||
if (value.contains('@')) {
|
if (value.contains('@')) {
|
||||||
return value.split(' ').first;
|
final parts = value.split(' ');
|
||||||
|
return parts.isNotEmpty ? parts[0] : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
66
lib/data/helper/ssh_decoder.dart
Normal file
66
lib/data/helper/ssh_decoder.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart';
|
||||||
|
|
||||||
|
/// Utility class for decoding SSH command output with encoding fallback
|
||||||
|
class SSHDecoder {
|
||||||
|
/// Decodes bytes to string with multiple encoding fallback strategies
|
||||||
|
///
|
||||||
|
/// Tries in order:
|
||||||
|
/// 1. UTF-8 (with allowMalformed for lenient parsing)
|
||||||
|
/// - Windows PowerShell scripts now set UTF-8 output encoding by default
|
||||||
|
/// 2. GBK (for Windows Chinese systems)
|
||||||
|
/// - In some cases, Windows will still revert to GBK.
|
||||||
|
/// - Only attempted if UTF-8 produces replacement characters (<28>)
|
||||||
|
static String decode(
|
||||||
|
List<int> bytes, {
|
||||||
|
bool isWindows = false,
|
||||||
|
String? context,
|
||||||
|
}) {
|
||||||
|
if (bytes.isEmpty) return '';
|
||||||
|
|
||||||
|
// Try UTF-8 first with allowMalformed
|
||||||
|
try {
|
||||||
|
final result = utf8.decode(bytes, allowMalformed: true);
|
||||||
|
// Check if there are replacement characters indicating decode failure
|
||||||
|
// For non-Windows systems, always use UTF-8 result
|
||||||
|
if (!result.contains('<EFBFBD>') || !isWindows) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// For Windows with replacement chars, log and try GBK fallback
|
||||||
|
if (isWindows && result.contains('<EFBFBD>')) {
|
||||||
|
final contextInfo = context != null ? ' [$context]' : '';
|
||||||
|
Loggers.app.info('UTF-8 decode has replacement chars$contextInfo, trying GBK fallback');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
final contextInfo = context != null ? ' [$context]' : '';
|
||||||
|
Loggers.app.warning('UTF-8 decode failed$contextInfo: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Windows or when UTF-8 has replacement chars, try GBK
|
||||||
|
try {
|
||||||
|
return gbk.decode(bytes);
|
||||||
|
} catch (e) {
|
||||||
|
final contextInfo = context != null ? ' [$context]' : '';
|
||||||
|
Loggers.app.warning('GBK decode failed$contextInfo: $e');
|
||||||
|
// Return empty string if all decoding attempts fail
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes string to bytes for SSH command input
|
||||||
|
///
|
||||||
|
/// Uses GBK for Windows, UTF-8 for others
|
||||||
|
static List<int> encode(String text, {bool isWindows = false}) {
|
||||||
|
if (isWindows) {
|
||||||
|
try {
|
||||||
|
return gbk.encode(text);
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('GBK encode failed: $e, falling back to UTF-8');
|
||||||
|
return utf8.encode(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return utf8.encode(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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/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';
|
||||||
|
|
||||||
@@ -23,7 +24,10 @@ class SystemDetector {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)
|
// Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)
|
||||||
final unixResult = await client.run('uname -a 2>/dev/null').string;
|
final unixResult = await client.runSafe(
|
||||||
|
'uname -a 2>/dev/null',
|
||||||
|
context: 'uname detection for ${spi.oldId}',
|
||||||
|
);
|
||||||
if (unixResult.contains('Linux')) {
|
if (unixResult.contains('Linux')) {
|
||||||
detectedSystemType = SystemType.linux;
|
detectedSystemType = SystemType.linux;
|
||||||
dprint('Detected Linux system type for ${spi.oldId}');
|
dprint('Detected Linux system type for ${spi.oldId}');
|
||||||
@@ -35,15 +39,19 @@ class SystemDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If uname fails, try to detect Windows systems
|
// If uname fails, try to detect Windows systems
|
||||||
final powershellResult = await client.run('ver 2>nul').string;
|
final powershellResult = await client.runSafe(
|
||||||
|
'ver 2>nul',
|
||||||
|
systemType: SystemType.windows,
|
||||||
|
context: 'ver detection for ${spi.oldId}',
|
||||||
|
);
|
||||||
if (powershellResult.isNotEmpty &&
|
if (powershellResult.isNotEmpty &&
|
||||||
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
|
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
|
||||||
detectedSystemType = SystemType.windows;
|
detectedSystemType = SystemType.windows;
|
||||||
dprint('Detected Windows system type for ${spi.oldId}');
|
dprint('Detected Windows system type for ${spi.oldId}');
|
||||||
return detectedSystemType;
|
return detectedSystemType;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
|
Loggers.app.warning('System detection failed for ${spi.oldId}: $e\n$stackTrace');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback
|
// Default fallback
|
||||||
|
|||||||
74
lib/data/model/ai/ask_ai_models.dart
Normal file
74
lib/data/model/ai/ask_ai_models.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Chat message exchanged with the Ask AI service.
|
||||||
|
enum AskAiMessageRole { user, assistant }
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class AskAiMessage {
|
||||||
|
const AskAiMessage({
|
||||||
|
required this.role,
|
||||||
|
required this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AskAiMessageRole role;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
String get apiRole {
|
||||||
|
switch (role) {
|
||||||
|
case AskAiMessageRole.user:
|
||||||
|
return 'user';
|
||||||
|
case AskAiMessageRole.assistant:
|
||||||
|
return 'assistant';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recommended command returned by the AI tool call.
|
||||||
|
@immutable
|
||||||
|
class AskAiCommand {
|
||||||
|
const AskAiCommand({
|
||||||
|
required this.command,
|
||||||
|
this.description = '',
|
||||||
|
this.toolName,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String command;
|
||||||
|
final String description;
|
||||||
|
final String? toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
sealed class AskAiEvent {
|
||||||
|
const AskAiEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Incremental text delta emitted while streaming the AI response.
|
||||||
|
class AskAiContentDelta extends AskAiEvent {
|
||||||
|
const AskAiContentDelta(this.delta);
|
||||||
|
final String delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits when a tool call returns a runnable command suggestion.
|
||||||
|
class AskAiToolSuggestion extends AskAiEvent {
|
||||||
|
const AskAiToolSuggestion(this.command);
|
||||||
|
final AskAiCommand command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals that the stream finished successfully.
|
||||||
|
class AskAiCompleted extends AskAiEvent {
|
||||||
|
const AskAiCompleted({
|
||||||
|
required this.fullText,
|
||||||
|
required this.commands,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String fullText;
|
||||||
|
final List<AskAiCommand> commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals that the stream terminated with an error before completion.
|
||||||
|
class AskAiStreamError extends AskAiEvent {
|
||||||
|
const AskAiStreamError(this.error, this.stackTrace);
|
||||||
|
|
||||||
|
final Object error;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
}
|
||||||
@@ -74,8 +74,8 @@ class BackupService {
|
|||||||
await _confirmAndRestore(context, backup);
|
await _confirmAndRestore(context, backup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
// Saved password failed, will prompt for manual input
|
Loggers.app.warning('Failed to restore with saved password, will prompt for manual input', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
lib/data/model/app/menu/platform.dart
Normal file
119
lib/data/model/app/menu/platform.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/data/model/app/tab.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/res/url.dart';
|
||||||
|
import 'package:server_box/generated/l10n/l10n.dart';
|
||||||
|
import 'package:server_box/view/page/setting/entry.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
/// macOS Menu Bar
|
||||||
|
class MacOSMenuBarManager {
|
||||||
|
static List<PlatformMenu> buildMenuBar(BuildContext context, Function(int) onTabChanged) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final homeTabs = Stores.setting.homeTabs.fetch();
|
||||||
|
return [
|
||||||
|
PlatformMenu(
|
||||||
|
label: 'Server Box',
|
||||||
|
menus: [
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: libL10n.about,
|
||||||
|
onSelected: () => _showAboutDialog(context),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuSettings,
|
||||||
|
shortcut: const SingleActivator(LogicalKeyboardKey.comma, meta: true),
|
||||||
|
onSelected: () => _openSettings(context),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuQuit,
|
||||||
|
shortcut: const SingleActivator(LogicalKeyboardKey.keyQ, meta: true),
|
||||||
|
onSelected: () => SystemNavigator.pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
PlatformMenu(
|
||||||
|
label: l10n.menuNavigate,
|
||||||
|
menus: _buildNavigateMenuItems(l10n, homeTabs, onTabChanged),
|
||||||
|
),
|
||||||
|
PlatformMenu(
|
||||||
|
label: l10n.menuInfo,
|
||||||
|
menus: [
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuGitHubRepository,
|
||||||
|
onSelected: () => _openURL(Urls.thisRepo),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuWiki,
|
||||||
|
onSelected: () => _openURL(Urls.appWiki),
|
||||||
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
label: l10n.menuHelp,
|
||||||
|
onSelected: () => _openURL(Urls.appHelp),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PlatformMenuItem> _buildNavigateMenuItems(
|
||||||
|
AppLocalizations l10n,
|
||||||
|
List<AppTab> homeTabs,
|
||||||
|
Function(int) onTabChanged,
|
||||||
|
) {
|
||||||
|
final menuItems = <PlatformMenuItem>[];
|
||||||
|
final tabLabels = {
|
||||||
|
AppTab.server: l10n.server,
|
||||||
|
AppTab.ssh: 'SSH',
|
||||||
|
AppTab.file: libL10n.file,
|
||||||
|
AppTab.snippet: l10n.snippet,
|
||||||
|
};
|
||||||
|
for (var i = 0; i < homeTabs.length; i++) {
|
||||||
|
final tab = homeTabs[i];
|
||||||
|
final label = tabLabels[tab];
|
||||||
|
if (label == null) continue;
|
||||||
|
final shortcutKey = _getShortcutKeyForIndex(i);
|
||||||
|
menuItems.add(PlatformMenuItem(
|
||||||
|
label: label,
|
||||||
|
shortcut: shortcutKey != null
|
||||||
|
? SingleActivator(shortcutKey, meta: true)
|
||||||
|
: null,
|
||||||
|
onSelected: () => onTabChanged(i),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LogicalKeyboardKey? _getShortcutKeyForIndex(int index) {
|
||||||
|
const keys = [
|
||||||
|
LogicalKeyboardKey.digit1,
|
||||||
|
LogicalKeyboardKey.digit2,
|
||||||
|
LogicalKeyboardKey.digit3,
|
||||||
|
LogicalKeyboardKey.digit4,
|
||||||
|
LogicalKeyboardKey.digit5,
|
||||||
|
LogicalKeyboardKey.digit6,
|
||||||
|
LogicalKeyboardKey.digit7,
|
||||||
|
LogicalKeyboardKey.digit8,
|
||||||
|
LogicalKeyboardKey.digit9,
|
||||||
|
];
|
||||||
|
return index < keys.length ? keys[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _showAboutDialog(BuildContext context) async {
|
||||||
|
const channel = MethodChannel('about');
|
||||||
|
await channel.invokeMethod('showAboutPanel');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _openSettings(BuildContext context) {
|
||||||
|
SettingsPage.route.go(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _openURL(String url) async {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,25 +166,34 @@ enum WindowsStatusCmdType implements ShellCmdType {
|
|||||||
echo('echo ${SystemType.windowsSign}'),
|
echo('echo ${SystemType.windowsSign}'),
|
||||||
time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
|
time('[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()'),
|
||||||
|
|
||||||
/// Get network interface statistics using Windows Performance Counters
|
/// Get network interface statistics using WMI
|
||||||
///
|
///
|
||||||
/// Uses Get-Counter to collect network I/O metrics from all network interfaces:
|
/// Uses WMI Win32_PerfRawData_Tcpip_NetworkInterface for cross-language compatibility:
|
||||||
/// - Collects bytes received and sent per second for all network interfaces
|
|
||||||
/// - Takes 2 samples with 1 second interval to calculate rates
|
/// - Takes 2 samples with 1 second interval to calculate rates
|
||||||
/// - Outputs results in JSON format for easy parsing
|
|
||||||
/// - Counter paths use double backslashes to escape PowerShell string literals
|
|
||||||
net(
|
net(
|
||||||
r'Get-Counter -Counter '
|
r'$s1 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '
|
||||||
r'"\\NetworkInterface(*)\\Bytes Received/sec", '
|
r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '
|
||||||
r'"\\NetworkInterface(*)\\Bytes Sent/sec" '
|
r'Start-Sleep -Seconds 1; '
|
||||||
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
|
r'$s2 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '
|
||||||
|
r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '
|
||||||
|
r'@($s1, $s2) | ConvertTo-Json -Depth 5',
|
||||||
),
|
),
|
||||||
sys('(Get-ComputerInfo).OsName'),
|
sys('(Get-ComputerInfo).OsName'),
|
||||||
cpu(
|
cpu(
|
||||||
'Get-WmiObject -Class Win32_Processor | '
|
'Get-WmiObject -Class Win32_Processor | '
|
||||||
'Select-Object Name, LoadPercentage | ConvertTo-Json',
|
'Select-Object Name, LoadPercentage, NumberOfCores, NumberOfLogicalProcessors | ConvertTo-Json',
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Get system uptime by calculating time since last boot
|
||||||
|
///
|
||||||
|
/// Calculates uptime directly in PowerShell to avoid date format parsing issues:
|
||||||
|
/// - Gets LastBootUpTime from Win32_OperatingSystem
|
||||||
|
/// - Calculates difference from current time
|
||||||
|
/// - Returns pre-formatted string: "X days, H:MM" or "H:MM" (if less than 1 day)
|
||||||
|
/// - Uses ToString('00') for zero-padding to avoid quote escaping issues
|
||||||
|
uptime(
|
||||||
|
r"""$up = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime; if ($up.Days -gt 0) { "$($up.Days) days, $($up.Hours):$($up.Minutes.ToString('00'))" } else { "$($up.Hours):$($up.Minutes.ToString('00'))" }""",
|
||||||
),
|
),
|
||||||
uptime('(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime'),
|
|
||||||
conn('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
|
conn('(netstat -an | findstr ESTABLISHED | Measure-Object -Line).Count'),
|
||||||
disk(
|
disk(
|
||||||
'Get-WmiObject -Class Win32_LogicalDisk | '
|
'Get-WmiObject -Class Win32_LogicalDisk | '
|
||||||
@@ -213,19 +222,19 @@ enum WindowsStatusCmdType implements ShellCmdType {
|
|||||||
),
|
),
|
||||||
host(r'Write-Output $env:COMPUTERNAME'),
|
host(r'Write-Output $env:COMPUTERNAME'),
|
||||||
|
|
||||||
/// Get disk I/O statistics using Windows Performance Counters
|
/// Get disk I/O statistics using WMI
|
||||||
///
|
///
|
||||||
/// Uses Get-Counter to collect disk I/O metrics from all physical disks:
|
/// Uses WMI Win32_PerfRawData_PerfDisk_PhysicalDisk:
|
||||||
/// - Monitors read and write bytes per second for all physical disks
|
/// - Monitors read and write bytes per second for all physical disks
|
||||||
/// - Takes 2 samples with 1 second interval to calculate I/O rates
|
/// - Takes 2 samples with 1 second interval to calculate rates
|
||||||
/// - Physical disk counters provide hardware-level I/O statistics
|
/// - DiskReadBytesPersec and DiskWriteBytesPersec are cumulative counters
|
||||||
/// - Outputs results in JSON format for parsing
|
|
||||||
/// - Counter names use wildcard (*) to capture all disk instances
|
|
||||||
diskio(
|
diskio(
|
||||||
r'Get-Counter -Counter '
|
r'$s1 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '
|
||||||
r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
|
r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '
|
||||||
r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
|
r'Start-Sleep -Seconds 1; '
|
||||||
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
|
r'$s2 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '
|
||||||
|
r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '
|
||||||
|
r'@($s1, $s2) | ConvertTo-Json -Depth 5',
|
||||||
),
|
),
|
||||||
battery(
|
battery(
|
||||||
'Get-WmiObject -Class Win32_Battery | '
|
'Get-WmiObject -Class Win32_Battery | '
|
||||||
@@ -287,7 +296,7 @@ enum WindowsStatusCmdType implements ShellCmdType {
|
|||||||
String get separator => ScriptConstants.getCmdSeparator(name);
|
String get separator => ScriptConstants.getCmdSeparator(name);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get divider => ScriptConstants.getCmdDivider(name);
|
String get divider => ScriptConstants.getWindowsCmdDivider(name);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CmdTypeSys get sysType => CmdTypeSys.windows;
|
CmdTypeSys get sysType => CmdTypeSys.windows;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ class ScriptConstants {
|
|||||||
/// Generate command-specific divider
|
/// Generate command-specific divider
|
||||||
static String getCmdDivider(String cmdName) => '\necho ${getCmdSeparator(cmdName)}\n\t';
|
static String getCmdDivider(String cmdName) => '\necho ${getCmdSeparator(cmdName)}\n\t';
|
||||||
|
|
||||||
|
/// Generate command-specific divider for Windows PowerShell
|
||||||
|
static String getWindowsCmdDivider(String cmdName) => '\n Write-Host "${getCmdSeparator(cmdName)}"\n ';
|
||||||
|
|
||||||
/// Parse script output into command-specific map
|
/// Parse script output into command-specific map
|
||||||
static Map<String, String> parseScriptOutput(String raw) {
|
static Map<String, String> parseScriptOutput(String raw) {
|
||||||
final result = <String, String>{};
|
final result = <String, String>{};
|
||||||
@@ -102,6 +105,7 @@ exec 2>/dev/null
|
|||||||
# DO NOT delete this file while app is running
|
# DO NOT delete this file while app is running
|
||||||
|
|
||||||
\$ErrorActionPreference = "SilentlyContinue"
|
\$ErrorActionPreference = "SilentlyContinue"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ final class PodmanImg implements ContainerImg {
|
|||||||
String toRawJson() => json.encode(toJson());
|
String toRawJson() => json.encode(toJson());
|
||||||
|
|
||||||
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
||||||
repository: json['repository'],
|
repository: _asString(json['repository']),
|
||||||
tag: json['tag'],
|
tag: _asString(json['tag']),
|
||||||
id: json['Id'],
|
id: _asString(json['Id']),
|
||||||
created: json['Created'],
|
created: _asInt(json['Created']),
|
||||||
size: json['Size'],
|
size: _asInt(json['Size']),
|
||||||
containers: json['Containers'],
|
containers: _asInt(json['Containers']),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@@ -119,3 +119,16 @@ final class DockerImg implements ContainerImg {
|
|||||||
'Tag': tag,
|
'Tag': tag,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _asString(dynamic val) {
|
||||||
|
if (val == null) return null;
|
||||||
|
if (val is String) return val;
|
||||||
|
return val.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _asInt(dynamic val) {
|
||||||
|
if (val == null) return null;
|
||||||
|
if (val is int) return val;
|
||||||
|
if (val is double) return val.toInt();
|
||||||
|
return int.tryParse(val.toString());
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ sealed class ContainerPs {
|
|||||||
|
|
||||||
factory ContainerPs.fromRaw(String s, ContainerType typ) => typ.ps(s);
|
factory ContainerPs.fromRaw(String s, ContainerType typ) => typ.ps(s);
|
||||||
|
|
||||||
void parseStats(String s);
|
void parseStats(String s, [String? version]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PodmanPs implements ContainerPs {
|
final class PodmanPs implements ContainerPs {
|
||||||
@@ -55,7 +55,7 @@ final class PodmanPs implements ContainerPs {
|
|||||||
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void parseStats(String s) {
|
void parseStats(String s, [String? version]) {
|
||||||
final stats = json.decode(s);
|
final stats = json.decode(s);
|
||||||
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
|
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
|
||||||
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
|
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
|
||||||
@@ -63,12 +63,32 @@ final class PodmanPs implements ContainerPs {
|
|||||||
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
|
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
|
||||||
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
|
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
|
||||||
mem = '$memUsage / $memLimit';
|
mem = '$memUsage / $memLimit';
|
||||||
final netIn = (stats['NetInput'] as int? ?? 0).bytes2Str;
|
|
||||||
final netOut = (stats['NetOutput'] as int? ?? 0).bytes2Str;
|
int netIn = 0;
|
||||||
net = '↓ $netIn / ↑ $netOut';
|
int netOut = 0;
|
||||||
|
final majorVersion = version?.split('.').firstOrNull;
|
||||||
|
final majorVersionNum = majorVersion != null ? int.tryParse(majorVersion) : null;
|
||||||
|
|
||||||
|
// Podman 4.x and earlier use top-level NetInput/NetOutput fields.
|
||||||
|
// Podman 5.x changed network backend (Netavark) and uses nested
|
||||||
|
// Network.{iface}.RxBytes/TxBytes structure instead.
|
||||||
|
if (majorVersionNum == null || majorVersionNum <= 4) {
|
||||||
|
netIn = stats['NetInput'] as int? ?? 0;
|
||||||
|
netOut = stats['NetOutput'] as int? ?? 0;
|
||||||
|
} else if (majorVersionNum >= 5) {
|
||||||
|
final network = stats['Network'] as Map<String, dynamic>?;
|
||||||
|
if (network != null) {
|
||||||
|
for (final interface in network.values) {
|
||||||
|
netIn += interface['RxBytes'] as int? ?? 0;
|
||||||
|
netOut += interface['TxBytes'] as int? ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
net = '↓ ${netIn.bytes2Str} / ↑ ${netOut.bytes2Str}';
|
||||||
|
|
||||||
final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;
|
final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;
|
||||||
final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;
|
final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;
|
||||||
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
disk = '${l10n.read} $diskIn / ${l10n.write} $diskOut';
|
||||||
}
|
}
|
||||||
|
|
||||||
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
factory PodmanPs.fromRawJson(String str) => PodmanPs.fromJson(json.decode(str));
|
||||||
@@ -125,12 +145,18 @@ final class DockerPs implements ContainerPs {
|
|||||||
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void parseStats(String s) {
|
void parseStats(String s, [String? version]) {
|
||||||
final stats = json.decode(s);
|
final stats = json.decode(s);
|
||||||
cpu = stats['CPUPerc'];
|
cpu = stats['CPUPerc'];
|
||||||
mem = stats['MemUsage'];
|
mem = stats['MemUsage'];
|
||||||
net = stats['NetIO'];
|
|
||||||
disk = stats['BlockIO'];
|
final netIO = stats['NetIO'] as String? ?? '0B / 0B';
|
||||||
|
final netParts = netIO.split(' / ');
|
||||||
|
net = '↓ ${netParts.firstOrNull ?? '0B'} / ↑ ${netParts.length > 1 ? netParts[1] : '0B'}';
|
||||||
|
|
||||||
|
final blockIO = stats['BlockIO'] as String? ?? '0B / 0B';
|
||||||
|
final blockParts = blockIO.split(' / ');
|
||||||
|
disk = '${l10n.read} ${blockParts.firstOrNull ?? '0B'} / ${l10n.write} ${blockParts.length > 1 ? blockParts[1] : '0B'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CONTAINER ID NAMES IMAGE STATUS
|
/// CONTAINER ID NAMES IMAGE STATUS
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class UpgradePkgInfo {
|
|||||||
|
|
||||||
void _parsePacman(String raw) {
|
void _parsePacman(String raw) {
|
||||||
final parts = raw.split(' ');
|
final parts = raw.split(' ');
|
||||||
|
if (parts.length < 4) throw Exception('Invalid pacman output format');
|
||||||
package = parts[0];
|
package = parts[0];
|
||||||
nowVersion = parts[1];
|
nowVersion = parts[1];
|
||||||
newVersion = parts[3];
|
newVersion = parts[3];
|
||||||
@@ -70,6 +71,7 @@ class UpgradePkgInfo {
|
|||||||
|
|
||||||
void _parseOpkg(String raw) {
|
void _parseOpkg(String raw) {
|
||||||
final parts = raw.split(' - ');
|
final parts = raw.split(' - ');
|
||||||
|
if (parts.length < 3) throw Exception('Invalid opkg output format');
|
||||||
package = parts[0];
|
package = parts[0];
|
||||||
nowVersion = parts[1];
|
nowVersion = parts[1];
|
||||||
newVersion = parts[2];
|
newVersion = parts[2];
|
||||||
@@ -80,6 +82,7 @@ class UpgradePkgInfo {
|
|||||||
void _parseApk(String raw) {
|
void _parseApk(String raw) {
|
||||||
final parts = raw.split(' ');
|
final parts = raw.split(' ');
|
||||||
final len = parts.length;
|
final len = parts.length;
|
||||||
|
if (len < 2) throw Exception('Invalid apk output format');
|
||||||
newVersion = parts[len - 1];
|
newVersion = parts[len - 1];
|
||||||
nowVersion = parts[0];
|
nowVersion = parts[0];
|
||||||
newVersion = newVersion.substring(0, newVersion.length - 1);
|
newVersion = newVersion.substring(0, newVersion.length - 1);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:server_box/data/res/status.dart';
|
|||||||
/// Capacity of the FIFO queue
|
/// Capacity of the FIFO queue
|
||||||
const _kCap = 30;
|
const _kCap = 30;
|
||||||
|
|
||||||
class Cpus extends TimeSeq<List<SingleCpuCore>> {
|
class Cpus extends TimeSeq<SingleCpuCore> {
|
||||||
Cpus(super.init1, super.init2);
|
Cpus(super.init1, super.init2);
|
||||||
|
|
||||||
final Map<String, int> brand = {};
|
final Map<String, int> brand = {};
|
||||||
@@ -14,21 +14,30 @@ class Cpus extends TimeSeq<List<SingleCpuCore>> {
|
|||||||
@override
|
@override
|
||||||
void onUpdate() {
|
void onUpdate() {
|
||||||
_coresCount = now.length;
|
_coresCount = now.length;
|
||||||
|
if (pre.isEmpty || now.isEmpty || pre.length != now.length) {
|
||||||
|
_totalDelta = 0;
|
||||||
|
_user = 0;
|
||||||
|
_sys = 0;
|
||||||
|
_iowait = 0;
|
||||||
|
_idle = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
_totalDelta = now[0].total - pre[0].total;
|
_totalDelta = now[0].total - pre[0].total;
|
||||||
_user = _getUser();
|
_user = _getUser();
|
||||||
_sys = _getSys();
|
_sys = _getSys();
|
||||||
_iowait = _getIowait();
|
_iowait = _getIowait();
|
||||||
_idle = _getIdle();
|
_idle = _getIdle();
|
||||||
_updateSpots();
|
_updateSpots();
|
||||||
//_updateRange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double usedPercent({int coreIdx = 0}) {
|
double usedPercent({int coreIdx = 0}) {
|
||||||
if (now.length != pre.length) return 0;
|
if (now.length != pre.length) return 0;
|
||||||
if (now.isEmpty) return 0;
|
if (now.isEmpty) return 0;
|
||||||
|
if (coreIdx >= now.length) return 0;
|
||||||
try {
|
try {
|
||||||
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
|
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
|
||||||
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
|
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
|
||||||
|
if (totalDelta == 0) return 0;
|
||||||
final used = idleDelta / totalDelta;
|
final used = idleDelta / totalDelta;
|
||||||
return used.isNaN ? 0 : 100 - used * 100;
|
return used.isNaN ? 0 : 100 - used * 100;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -157,6 +166,7 @@ class SingleCpuCore extends TimeSeqIface<SingleCpuCore> {
|
|||||||
final id = item.split(' ').firstOrNull;
|
final id = item.split(' ').firstOrNull;
|
||||||
if (id == null) continue;
|
if (id == null) continue;
|
||||||
final matches = item.replaceFirst(id, '').trim().split(' ');
|
final matches = item.replaceFirst(id, '').trim().split(' ');
|
||||||
|
if (matches.length < 7) continue;
|
||||||
cpus.add(
|
cpus.add(
|
||||||
SingleCpuCore(
|
SingleCpuCore(
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ class Disk with EquatableMixin {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
class DiskIO extends TimeSeq<DiskIOPiece> {
|
||||||
DiskIO(super.init1, super.init2);
|
DiskIO(super.init1, super.init2);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
|
|||||||
|
|
||||||
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
|
typedef CachedNetVals = ({String sizeIn, String sizeOut, String speedIn, String speedOut});
|
||||||
|
|
||||||
class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
class NetSpeed extends TimeSeq<NetSpeedPart> {
|
||||||
NetSpeed(super.init1, super.init2);
|
NetSpeed(super.init1, super.init2);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -164,7 +164,8 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
|||||||
final bytesIn = BigInt.parse(bytes.first);
|
final bytesIn = BigInt.parse(bytes.first);
|
||||||
final bytesOut = BigInt.parse(bytes[8]);
|
final bytesOut = BigInt.parse(bytes[8]);
|
||||||
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to parse net speed data: $item', e, s);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ class Proc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get binary {
|
String get binary {
|
||||||
final parts = command.split(' ');
|
final parts = command.trim().split(' ').where((e) => e.isNotEmpty).toList();
|
||||||
return parts[0];
|
return parts.isNotEmpty ? parts[0] : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.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/server/custom.dart';
|
import 'package:server_box/data/model/server/custom.dart';
|
||||||
@@ -35,8 +36,15 @@ abstract class Spi with _$Spi {
|
|||||||
String? alterUrl,
|
String? alterUrl,
|
||||||
@Default(true) bool autoConnect,
|
@Default(true) bool autoConnect,
|
||||||
|
|
||||||
/// [id] of the jump server
|
/// [id] of the jump server (legacy, single hop)
|
||||||
|
///
|
||||||
|
/// Migrated to [jumpChainIds].
|
||||||
String? jumpId,
|
String? jumpId,
|
||||||
|
|
||||||
|
/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
@JsonKey(includeIfNull: false) List<String>? jumpChainIds,
|
||||||
ServerCustom? custom,
|
ServerCustom? custom,
|
||||||
WakeOnLanCfg? wolCfg,
|
WakeOnLanCfg? wolCfg,
|
||||||
|
|
||||||
@@ -79,7 +87,10 @@ extension Spix on Spi {
|
|||||||
String? migrateId() {
|
String? migrateId() {
|
||||||
if (id.isNotEmpty) return null;
|
if (id.isNotEmpty) return null;
|
||||||
ServerStore.instance.delete(oldId);
|
ServerStore.instance.delete(oldId);
|
||||||
final newSpi = copyWith(id: ShortId.generate());
|
final newSpi = copyWith(
|
||||||
|
id: ShortId.generate(),
|
||||||
|
jumpChainIds: jumpChainIds ?? (jumpId == null ? null : [jumpId!]),
|
||||||
|
);
|
||||||
newSpi.save();
|
newSpi.save();
|
||||||
return newSpi.id;
|
return newSpi.id;
|
||||||
}
|
}
|
||||||
@@ -94,7 +105,8 @@ extension Spix on Spi {
|
|||||||
port == other.port &&
|
port == other.port &&
|
||||||
pwd == other.pwd &&
|
pwd == other.pwd &&
|
||||||
keyId == other.keyId &&
|
keyId == other.keyId &&
|
||||||
jumpId == other.jumpId;
|
jumpId == other.jumpId &&
|
||||||
|
listEquals(jumpChainIds, other.jumpChainIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the connection should be re-established.
|
/// Returns true if the connection should be re-established.
|
||||||
@@ -137,7 +149,7 @@ extension Spix on Spi {
|
|||||||
tags: ['tag1', 'tag2'],
|
tags: ['tag1', 'tag2'],
|
||||||
alterUrl: 'user@ip:port',
|
alterUrl: 'user@ip:port',
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
jumpId: 'jump_server_id',
|
jumpChainIds: ['jump_server_id'],
|
||||||
custom: ServerCustom(
|
custom: ServerCustom(
|
||||||
pveAddr: 'http://localhost:8006',
|
pveAddr: 'http://localhost:8006',
|
||||||
pveIgnoreCert: false,
|
pveIgnoreCert: false,
|
||||||
|
|||||||
@@ -16,8 +16,13 @@ T _$identity<T>(T value) => value;
|
|||||||
mixin _$Spi {
|
mixin _$Spi {
|
||||||
|
|
||||||
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
|
||||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server (legacy, single hop)
|
||||||
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
///
|
||||||
|
/// Migrated to [jumpChainIds].
|
||||||
|
String? get jumpId;/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
@JsonKey(includeIfNull: false) List<String>? get jumpChainIds; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||||
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
||||||
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
||||||
@@ -33,12 +38,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other.jumpChainIds, jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -49,7 +54,7 @@ abstract mixin class $SpiCopyWith<$Res> {
|
|||||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +71,7 @@ class _$SpiCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -78,7 +83,8 @@ as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to
|
|||||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
as String?,jumpChainIds: freezed == jumpChainIds ? _self.jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||||
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
|
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -169,10 +175,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi() when $default != null:
|
case _Spi() when $default != null:
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -190,10 +196,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi():
|
case _Spi():
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -210,10 +216,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi() when $default != null:
|
case _Spi() when $default != null:
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -225,7 +231,7 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
|
|
||||||
@JsonSerializable(includeIfNull: false)
|
@JsonSerializable(includeIfNull: false)
|
||||||
class _Spi extends Spi {
|
class _Spi extends Spi {
|
||||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, @JsonKey(includeIfNull: false) final List<String>? jumpChainIds, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_jumpChainIds = jumpChainIds,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
||||||
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||||
|
|
||||||
@override final String name;
|
@override final String name;
|
||||||
@@ -246,8 +252,25 @@ class _Spi extends Spi {
|
|||||||
|
|
||||||
@override final String? alterUrl;
|
@override final String? alterUrl;
|
||||||
@override@JsonKey() final bool autoConnect;
|
@override@JsonKey() final bool autoConnect;
|
||||||
/// [id] of the jump server
|
/// [id] of the jump server (legacy, single hop)
|
||||||
|
///
|
||||||
|
/// Migrated to [jumpChainIds].
|
||||||
@override final String? jumpId;
|
@override final String? jumpId;
|
||||||
|
/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
final List<String>? _jumpChainIds;
|
||||||
|
/// Jump chain hop ids (nearest -> farthest)
|
||||||
|
///
|
||||||
|
/// Preferred over [jumpId].
|
||||||
|
@override@JsonKey(includeIfNull: false) List<String>? get jumpChainIds {
|
||||||
|
final value = _jumpChainIds;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_jumpChainIds is EqualUnmodifiableListView) return _jumpChainIds;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
@override final ServerCustom? custom;
|
@override final ServerCustom? custom;
|
||||||
@override final WakeOnLanCfg? wolCfg;
|
@override final WakeOnLanCfg? wolCfg;
|
||||||
/// It only applies to SSH terminal.
|
/// It only applies to SSH terminal.
|
||||||
@@ -289,12 +312,12 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other._jumpChainIds, _jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(_jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -305,7 +328,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
|||||||
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -322,7 +345,7 @@ class __$SpiCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
||||||
return _then(_Spi(
|
return _then(_Spi(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -334,7 +357,8 @@ as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_t
|
|||||||
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
as String?,jumpChainIds: freezed == jumpChainIds ? _self._jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
|
||||||
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
|
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
|
|||||||
alterUrl: json['alterUrl'] as String?,
|
alterUrl: json['alterUrl'] as String?,
|
||||||
autoConnect: json['autoConnect'] as bool? ?? true,
|
autoConnect: json['autoConnect'] as bool? ?? true,
|
||||||
jumpId: json['jumpId'] as String?,
|
jumpId: json['jumpId'] as String?,
|
||||||
|
jumpChainIds: (json['jumpChainIds'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
custom: json['custom'] == null
|
custom: json['custom'] == null
|
||||||
? null
|
? null
|
||||||
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
|
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
|
||||||
@@ -47,6 +50,7 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
|||||||
'alterUrl': ?instance.alterUrl,
|
'alterUrl': ?instance.alterUrl,
|
||||||
'autoConnect': instance.autoConnect,
|
'autoConnect': instance.autoConnect,
|
||||||
'jumpId': ?instance.jumpId,
|
'jumpId': ?instance.jumpId,
|
||||||
|
'jumpChainIds': ?instance.jumpChainIds,
|
||||||
'custom': ?instance.custom,
|
'custom': ?instance.custom,
|
||||||
'wolCfg': ?instance.wolCfg,
|
'wolCfg': ?instance.wolCfg,
|
||||||
'envs': ?instance.envs,
|
'envs': ?instance.envs,
|
||||||
|
|||||||
@@ -378,18 +378,27 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedO
|
|||||||
// Windows CPU parsing - JSON format from PowerShell
|
// Windows CPU parsing - JSON format from PowerShell
|
||||||
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
|
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
|
||||||
if (cpuRaw.isNotEmpty && cpuRaw != 'null' && !cpuRaw.contains('error') && !cpuRaw.contains('Exception')) {
|
if (cpuRaw.isNotEmpty && cpuRaw != 'null' && !cpuRaw.contains('error') && !cpuRaw.contains('Exception')) {
|
||||||
final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
|
final cpuResult = WindowsParser.parseCpu(cpuRaw, req.ss);
|
||||||
if (cpus.isNotEmpty) {
|
if (cpuResult.cores.isNotEmpty) {
|
||||||
req.ss.cpu.update(cpus);
|
req.ss.cpu.update(cpuResult.cores);
|
||||||
|
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
|
||||||
|
if (brandRaw.isNotEmpty && brandRaw != 'null') {
|
||||||
|
req.ss.cpu.brand.clear();
|
||||||
|
final brandLines = brandRaw.trim().split('\n');
|
||||||
|
final uniqueBrands = <String>{};
|
||||||
|
for (final line in brandLines) {
|
||||||
|
final trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.isNotEmpty) {
|
||||||
|
uniqueBrands.add(trimmedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uniqueBrands.isNotEmpty) {
|
||||||
|
final brandName = uniqueBrands.first;
|
||||||
|
req.ss.cpu.brand[brandName] = cpuResult.coreCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows CPU brand parsing
|
|
||||||
final brandRaw = WindowsStatusCmdType.cpuBrand.findInMap(parsedOutput);
|
|
||||||
if (brandRaw.isNotEmpty && brandRaw != 'null') {
|
|
||||||
req.ss.cpu.brand.clear();
|
|
||||||
req.ss.cpu.brand[brandRaw.trim()] = 1;
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Loggers.app.warning('Windows CPU parsing failed: $e', s);
|
Loggers.app.warning('Windows CPU parsing failed: $e', s);
|
||||||
}
|
}
|
||||||
@@ -427,8 +436,11 @@ void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsed
|
|||||||
/// Parse Windows uptime data
|
/// Parse Windows uptime data
|
||||||
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
|
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
|
||||||
try {
|
try {
|
||||||
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
|
final uptimeRaw = WindowsStatusCmdType.uptime.findInMap(parsedOutput);
|
||||||
if (uptime != null) {
|
if (uptimeRaw.isNotEmpty && uptimeRaw != 'null') {
|
||||||
|
// PowerShell now returns pre-formatted uptime string (e.g., "28 days, 5:00" or "5:00")
|
||||||
|
// No parsing needed - use it directly
|
||||||
|
final uptime = uptimeRaw.trim();
|
||||||
req.ss.more[StatusCmdType.uptime] = uptime;
|
req.ss.more[StatusCmdType.uptime] = uptime;
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -541,38 +553,36 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
|
|||||||
final dynamic jsonData = json.decode(raw);
|
final dynamic jsonData = json.decode(raw);
|
||||||
final List<NetSpeedPart> netParts = [];
|
final List<NetSpeedPart> netParts = [];
|
||||||
|
|
||||||
// PowerShell Get-Counter returns a structure with CounterSamples
|
if (jsonData is List && jsonData.length >= 2) {
|
||||||
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
|
var sample1 = jsonData[jsonData.length - 2];
|
||||||
final samples = jsonData['CounterSamples'] as List?;
|
var sample2 = jsonData[jsonData.length - 1];
|
||||||
if (samples != null && samples.length >= 2) {
|
if (sample1 is Map && sample1.containsKey('value')) {
|
||||||
// We need 2 samples to calculate speed (interval between them)
|
sample1 = sample1['value'];
|
||||||
final Map<String, double> interfaceRx = {};
|
}
|
||||||
final Map<String, double> interfaceTx = {};
|
if (sample2 is Map && sample2.containsKey('value')) {
|
||||||
|
sample2 = sample2['value'];
|
||||||
for (final sample in samples) {
|
}
|
||||||
final path = sample['Path']?.toString() ?? '';
|
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
|
||||||
final cookedValue = sample['CookedValue'] as num? ?? 0;
|
for (int i = 0; i < sample1.length; i++) {
|
||||||
|
final s1 = sample1[i];
|
||||||
if (path.contains('Bytes Received/sec')) {
|
final s2 = sample2[i];
|
||||||
final interfaceName = _extractInterfaceName(path);
|
final name = s1['Name']?.toString() ?? '';
|
||||||
if (interfaceName.isNotEmpty) {
|
if (name.isEmpty || name == '_Total') continue;
|
||||||
interfaceRx[interfaceName] = cookedValue.toDouble();
|
final rx1 = (s1['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
|
||||||
}
|
final rx2 = (s2['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
|
||||||
} else if (path.contains('Bytes Sent/sec')) {
|
final tx1 = (s1['BytesSentPersec'] as num?)?.toDouble() ?? 0;
|
||||||
final interfaceName = _extractInterfaceName(path);
|
final tx2 = (s2['BytesSentPersec'] as num?)?.toDouble() ?? 0;
|
||||||
if (interfaceName.isNotEmpty) {
|
final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
|
||||||
interfaceTx[interfaceName] = cookedValue.toDouble();
|
final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
|
||||||
}
|
final timeDelta = (time2 - time1) / 10000000;
|
||||||
}
|
if (timeDelta <= 0) continue;
|
||||||
}
|
final rxDelta = rx2 - rx1;
|
||||||
|
final txDelta = tx2 - tx1;
|
||||||
// Create NetSpeedPart for each interface
|
if (rxDelta < 0 || txDelta < 0) continue;
|
||||||
for (final interfaceName in interfaceRx.keys) {
|
final rxSpeed = rxDelta / timeDelta;
|
||||||
final rx = interfaceRx[interfaceName] ?? 0;
|
final txSpeed = txDelta / timeDelta;
|
||||||
final tx = interfaceTx[interfaceName] ?? 0;
|
|
||||||
|
|
||||||
netParts.add(
|
netParts.add(
|
||||||
NetSpeedPart(interfaceName, BigInt.from(rx.toInt()), BigInt.from(tx.toInt()), currentTime),
|
NetSpeedPart(name, BigInt.from(rxSpeed.toInt()), BigInt.from(txSpeed.toInt()), currentTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,53 +594,45 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _extractInterfaceName(String path) {
|
|
||||||
// Extract interface name from path like
|
|
||||||
// "\\Computer\\NetworkInterface(Interface Name)\\..."
|
|
||||||
final match = RegExp(r'\\NetworkInterface\(([^)]+)\)\\').firstMatch(path);
|
|
||||||
return match?.group(1) ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
|
List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
|
||||||
try {
|
try {
|
||||||
final dynamic jsonData = json.decode(raw);
|
final dynamic jsonData = json.decode(raw);
|
||||||
final List<DiskIOPiece> diskParts = [];
|
final List<DiskIOPiece> diskParts = [];
|
||||||
|
|
||||||
// PowerShell Get-Counter returns a structure with CounterSamples
|
if (jsonData is List && jsonData.length >= 2) {
|
||||||
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
|
var sample1 = jsonData[jsonData.length - 2];
|
||||||
final samples = jsonData['CounterSamples'] as List?;
|
var sample2 = jsonData[jsonData.length - 1];
|
||||||
if (samples != null) {
|
if (sample1 is Map && sample1.containsKey('value')) {
|
||||||
final Map<String, double> diskReads = {};
|
sample1 = sample1['value'];
|
||||||
final Map<String, double> diskWrites = {};
|
}
|
||||||
|
if (sample2 is Map && sample2.containsKey('value')) {
|
||||||
for (final sample in samples) {
|
sample2 = sample2['value'];
|
||||||
final path = sample['Path']?.toString() ?? '';
|
}
|
||||||
final cookedValue = sample['CookedValue'] as num? ?? 0;
|
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
|
||||||
|
for (int i = 0; i < sample1.length; i++) {
|
||||||
if (path.contains('Disk Read Bytes/sec')) {
|
final s1 = sample1[i];
|
||||||
final diskName = _extractDiskName(path);
|
final s2 = sample2[i];
|
||||||
if (diskName.isNotEmpty) {
|
final name = s1['Name']?.toString() ?? '';
|
||||||
diskReads[diskName] = cookedValue.toDouble();
|
if (name.isEmpty || name == '_Total') continue;
|
||||||
}
|
final read1 = (s1['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||||
} else if (path.contains('Disk Write Bytes/sec')) {
|
final read2 = (s2['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||||
final diskName = _extractDiskName(path);
|
final write1 = (s1['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||||
if (diskName.isNotEmpty) {
|
final write2 = (s2['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||||
diskWrites[diskName] = cookedValue.toDouble();
|
final time1 = (s1['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
|
||||||
}
|
final time2 = (s2['Timestamp_Sys100NS'] as num?)?.toDouble() ?? 0;
|
||||||
}
|
final timeDelta = (time2 - time1) / 10000000;
|
||||||
}
|
if (timeDelta <= 0) continue;
|
||||||
|
final readDelta = read2 - read1;
|
||||||
// Create DiskIOPiece for each disk - convert bytes to sectors
|
final writeDelta = write2 - write1;
|
||||||
// (assuming 512 bytes per sector)
|
if (readDelta < 0 || writeDelta < 0) continue;
|
||||||
for (final diskName in diskReads.keys) {
|
final readSpeed = readDelta / timeDelta;
|
||||||
final readBytes = diskReads[diskName] ?? 0;
|
final writeSpeed = writeDelta / timeDelta;
|
||||||
final writeBytes = diskWrites[diskName] ?? 0;
|
final sectorsRead = (readSpeed / 512).round();
|
||||||
final sectorsRead = (readBytes / 512).round();
|
final sectorsWrite = (writeSpeed / 512).round();
|
||||||
final sectorsWrite = (writeBytes / 512).round();
|
|
||||||
|
|
||||||
diskParts.add(
|
diskParts.add(
|
||||||
DiskIOPiece(
|
DiskIOPiece(
|
||||||
dev: diskName,
|
dev: name,
|
||||||
sectorsRead: sectorsRead,
|
sectorsRead: sectorsRead,
|
||||||
sectorsWrite: sectorsWrite,
|
sectorsWrite: sectorsWrite,
|
||||||
time: currentTime,
|
time: currentTime,
|
||||||
@@ -646,13 +648,6 @@ List<DiskIOPiece> _parseWindowsDiskIO(String raw, int currentTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _extractDiskName(String path) {
|
|
||||||
// Extract disk name from path like
|
|
||||||
// "\\Computer\\PhysicalDisk(Disk Name)\\..."
|
|
||||||
final match = RegExp(r'\\PhysicalDisk\(([^)]+)\)\\').firstMatch(path);
|
|
||||||
return match?.group(1) ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
void _parseWindowsTemperatures(Temperatures temps, String raw) {
|
void _parseWindowsTemperatures(Temperatures temps, String raw) {
|
||||||
try {
|
try {
|
||||||
// Handle error output
|
// Handle error output
|
||||||
@@ -684,7 +679,7 @@ void _parseWindowsTemperatures(Temperatures temps, String raw) {
|
|||||||
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
|
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
|
||||||
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
|
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
// If JSON parsing fails, ignore temperature data
|
Loggers.app.warning('Failed to parse Windows temperature data', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,27 +37,39 @@ class Fifo<T> extends ListBase<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class TimeSeq<T extends List<TimeSeqIface>> extends Fifo<T> {
|
abstract class TimeSeq<T extends TimeSeqIface<T>> extends Fifo<List<T>> {
|
||||||
/// Due to the design, at least two elements are required, otherwise [pre] /
|
/// Due to the design, at least two elements are required, otherwise [pre] /
|
||||||
/// [now] will throw.
|
/// [now] will throw.
|
||||||
TimeSeq(T init1, T init2, {super.capacity}) : super(list: [init1, init2]);
|
TimeSeq(List<T> init1, List<T> init2, {super.capacity}) : super(list: [init1, init2]);
|
||||||
|
|
||||||
T get pre {
|
List<T> get pre {
|
||||||
return _list[length - 2];
|
return _list[length - 2];
|
||||||
}
|
}
|
||||||
|
|
||||||
T get now {
|
List<T> get now {
|
||||||
return _list[length - 1];
|
return _list[length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUpdate();
|
void onUpdate();
|
||||||
|
|
||||||
void update(T new_) {
|
void update(List<T> new_) {
|
||||||
add(new_);
|
add(new_);
|
||||||
|
|
||||||
if (pre.length != now.length) {
|
if (pre.length != now.length) {
|
||||||
pre.removeWhere((e) => now.any((el) => e.same(el)));
|
final previous = pre.toList(growable: false);
|
||||||
pre.addAll(now.where((e) => pre.every((el) => !e.same(el))));
|
final remaining = previous.toList(growable: true);
|
||||||
|
final aligned = <T>[];
|
||||||
|
|
||||||
|
for (final current in now) {
|
||||||
|
final matchIndex = remaining.indexWhere((item) => item.same(current));
|
||||||
|
if (matchIndex >= 0) {
|
||||||
|
aligned.add(remaining.removeAt(matchIndex));
|
||||||
|
} else {
|
||||||
|
aligned.add(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_list[length - 2] = aligned;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import 'package:server_box/data/model/server/disk.dart';
|
|||||||
import 'package:server_box/data/model/server/memory.dart';
|
import 'package:server_box/data/model/server/memory.dart';
|
||||||
import 'package:server_box/data/model/server/server.dart';
|
import 'package:server_box/data/model/server/server.dart';
|
||||||
|
|
||||||
|
/// Windows CPU parse result
|
||||||
|
class WindowsCpuResult {
|
||||||
|
final List<SingleCpuCore> cores;
|
||||||
|
final int coreCount;
|
||||||
|
const WindowsCpuResult(this.cores, this.coreCount);
|
||||||
|
}
|
||||||
|
|
||||||
/// Windows-specific status parsing utilities
|
/// Windows-specific status parsing utilities
|
||||||
///
|
///
|
||||||
/// This module handles parsing of Windows PowerShell command outputs
|
/// This module handles parsing of Windows PowerShell command outputs
|
||||||
@@ -94,30 +101,75 @@ class WindowsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse Windows CPU information from PowerShell output
|
/// Parse Windows CPU information from PowerShell output
|
||||||
static List<SingleCpuCore> parseCpu(String raw, ServerStatus serverStatus) {
|
/// Returns WindowsCpuResult containing CPU cores and total core count
|
||||||
|
static WindowsCpuResult parseCpu(String raw, ServerStatus serverStatus) {
|
||||||
try {
|
try {
|
||||||
final dynamic jsonData = json.decode(raw);
|
final dynamic jsonData = json.decode(raw);
|
||||||
final List<SingleCpuCore> cpus = [];
|
final List<SingleCpuCore> cpus = [];
|
||||||
|
int totalCoreCount = 1;
|
||||||
|
|
||||||
if (jsonData is List) {
|
if (jsonData is List) {
|
||||||
for (int i = 0; i < jsonData.length; i++) {
|
// Multiple physical processors
|
||||||
final cpu = jsonData[i];
|
totalCoreCount = 0; // Reset to sum up
|
||||||
final loadPercentage = cpu['LoadPercentage'] ?? 0;
|
var logicalProcessorOffset = 0;
|
||||||
final usage = loadPercentage as int;
|
final prevCpus = serverStatus.cpu.now;
|
||||||
|
for (int procIdx = 0; procIdx < jsonData.length; procIdx++) {
|
||||||
|
final processor = jsonData[procIdx];
|
||||||
|
final loadPercentage = (processor['LoadPercentage'] as num?) ?? 0;
|
||||||
|
final numberOfCores = (processor['NumberOfCores'] as int?) ?? 1;
|
||||||
|
final numberOfLogicalProcessors = (processor['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;
|
||||||
|
totalCoreCount += numberOfCores;
|
||||||
|
final usage = loadPercentage.toInt();
|
||||||
final idle = 100 - usage;
|
final idle = 100 - usage;
|
||||||
|
|
||||||
// Get previous CPU data to calculate cumulative values
|
// Create a SingleCpuCore entry for each logical processor
|
||||||
final prevCpus = serverStatus.cpu.now;
|
// Windows only reports overall CPU load, so we distribute it evenly
|
||||||
final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
|
for (int i = 0; i < numberOfLogicalProcessors; i++) {
|
||||||
|
final coreId = logicalProcessorOffset + i;
|
||||||
|
// Skip summary entry at index 0 when looking up previous samples
|
||||||
|
final prevIndex = coreId + 1;
|
||||||
|
final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;
|
||||||
|
|
||||||
// LIMITATION: Windows CPU counters approach
|
// LIMITATION: Windows CPU counters approach
|
||||||
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
|
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
|
||||||
// We simulate cumulative counters by adding current percentages to previous totals.
|
// We simulate cumulative counters by adding current percentages to previous totals.
|
||||||
// This approach has limitations:
|
// Additionally, Windows only provides overall CPU load, not per-core load.
|
||||||
// 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
|
// We distribute the load evenly across all logical processors.
|
||||||
// 2. May drift over time with variable polling intervals
|
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||||
// 3. Results depend on consistent polling frequency
|
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||||
// However, this allows compatibility with existing delta-based CPU calculation logic.
|
|
||||||
|
cpus.add(
|
||||||
|
SingleCpuCore(
|
||||||
|
'cpu$coreId',
|
||||||
|
newUser, // cumulative user time
|
||||||
|
0, // sys (not available)
|
||||||
|
0, // nice (not available)
|
||||||
|
newIdle, // cumulative idle time
|
||||||
|
0, // iowait (not available)
|
||||||
|
0, // irq (not available)
|
||||||
|
0, // softirq (not available)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logicalProcessorOffset += numberOfLogicalProcessors;
|
||||||
|
}
|
||||||
|
} else if (jsonData is Map) {
|
||||||
|
// Single physical processor
|
||||||
|
final loadPercentage = (jsonData['LoadPercentage'] as num?) ?? 0;
|
||||||
|
final numberOfCores = (jsonData['NumberOfCores'] as int?) ?? 1;
|
||||||
|
final numberOfLogicalProcessors = (jsonData['NumberOfLogicalProcessors'] as int?) ?? numberOfCores;
|
||||||
|
totalCoreCount = numberOfCores;
|
||||||
|
final usage = loadPercentage.toInt();
|
||||||
|
final idle = 100 - usage;
|
||||||
|
|
||||||
|
// Create a SingleCpuCore entry for each logical processor
|
||||||
|
final prevCpus = serverStatus.cpu.now;
|
||||||
|
for (int i = 0; i < numberOfLogicalProcessors; i++) {
|
||||||
|
// Skip summary entry at index 0 when looking up previous samples
|
||||||
|
final prevIndex = i + 1;
|
||||||
|
final prevCpu = prevIndex < prevCpus.length ? prevCpus[prevIndex] : null;
|
||||||
|
|
||||||
|
// LIMITATION: See comment above for Windows CPU counter limitations
|
||||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||||
|
|
||||||
@@ -125,46 +177,43 @@ class WindowsParser {
|
|||||||
SingleCpuCore(
|
SingleCpuCore(
|
||||||
'cpu$i',
|
'cpu$i',
|
||||||
newUser, // cumulative user time
|
newUser, // cumulative user time
|
||||||
0, // sys (not available)
|
0, // sys
|
||||||
0, // nice (not available)
|
0, // nice
|
||||||
newIdle, // cumulative idle time
|
newIdle, // cumulative idle time
|
||||||
0, // iowait (not available)
|
0, // iowait
|
||||||
0, // irq (not available)
|
0, // irq
|
||||||
0, // softirq (not available)
|
0, // softirq
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (jsonData is Map) {
|
|
||||||
// Single CPU core
|
|
||||||
final loadPercentage = jsonData['LoadPercentage'] ?? 0;
|
|
||||||
final usage = loadPercentage as int;
|
|
||||||
final idle = 100 - usage;
|
|
||||||
|
|
||||||
// Get previous CPU data to calculate cumulative values
|
|
||||||
final prevCpus = serverStatus.cpu.now;
|
|
||||||
final prevCpu = prevCpus.isNotEmpty ? prevCpus[0] : null;
|
|
||||||
|
|
||||||
// LIMITATION: See comment above for Windows CPU counter limitations
|
|
||||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
|
||||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
|
||||||
|
|
||||||
cpus.add(
|
|
||||||
SingleCpuCore(
|
|
||||||
'cpu0',
|
|
||||||
newUser, // cumulative user time
|
|
||||||
0, // sys
|
|
||||||
0, // nice
|
|
||||||
newIdle, // cumulative idle time
|
|
||||||
0, // iowait
|
|
||||||
0, // irq
|
|
||||||
0, // softirq
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cpus;
|
// Add a summary entry at the beginning (like Linux 'cpu' line)
|
||||||
} catch (e) {
|
// This is the aggregate of all logical processors
|
||||||
return [];
|
if (cpus.isNotEmpty) {
|
||||||
|
int totalUser = 0;
|
||||||
|
int totalIdle = 0;
|
||||||
|
for (final core in cpus) {
|
||||||
|
totalUser += core.user;
|
||||||
|
totalIdle += core.idle;
|
||||||
|
}
|
||||||
|
// Insert at the beginning with ID 'cpu' (matching Linux format)
|
||||||
|
cpus.insert(0, SingleCpuCore(
|
||||||
|
'cpu', // Summary entry, like Linux
|
||||||
|
totalUser,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
totalIdle,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return WindowsCpuResult(cpus, totalCoreCount);
|
||||||
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Windows CPU parsing failed: $e', s);
|
||||||
|
return WindowsCpuResult([], 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,32 @@ class SftpReq {
|
|||||||
final String localPath;
|
final String localPath;
|
||||||
final SftpReqType type;
|
final SftpReqType type;
|
||||||
String? privateKey;
|
String? privateKey;
|
||||||
Spi? jumpSpi;
|
List<Spi>? jumpChain;
|
||||||
String? jumpPrivateKey;
|
List<String?>? jumpPrivateKeys;
|
||||||
|
Map<String, String>? knownHostFingerprints;
|
||||||
|
|
||||||
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||||
final keyId = spi.keyId;
|
final keyId = spi.keyId;
|
||||||
if (keyId != null) {
|
if (keyId != null) {
|
||||||
privateKey = getPrivateKey(keyId);
|
privateKey = getPrivateKey(keyId);
|
||||||
}
|
}
|
||||||
if (spi.jumpId != null) {
|
if (spi.jumpChainIds != null || spi.jumpId != null) {
|
||||||
jumpSpi = Stores.server.box.get(spi.jumpId);
|
// Use resolveMergedJumpChain to recursively expand nested hop chains
|
||||||
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
|
final chain = resolveMergedJumpChain(spi);
|
||||||
|
final keys = <String?>[];
|
||||||
|
for (final hop in chain) {
|
||||||
|
keys.add(hop.keyId != null ? getPrivateKey(hop.keyId!) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always set when a jump is configured so the isolate won't fallback to Stores.
|
||||||
|
jumpChain = chain;
|
||||||
|
jumpPrivateKeys = keys;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get());
|
||||||
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to load SSH known host fingerprints', e, s);
|
||||||
|
knownHostFingerprints = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +45,7 @@ class SftpReqStatus {
|
|||||||
late SftpWorker worker;
|
late SftpWorker worker;
|
||||||
final Completer? completer;
|
final Completer? completer;
|
||||||
|
|
||||||
String get fileName => req.localPath.split('/').last;
|
String get fileName => req.localPath.split(Pfs.seperator).last;
|
||||||
|
|
||||||
// status of the download
|
// status of the download
|
||||||
double? progress;
|
double? progress;
|
||||||
@@ -83,4 +98,4 @@ class SftpReqStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SftpWorkerStatus { preparing, sshConnectted, loading, finished }
|
enum SftpWorkerStatus { preparing, sshConnected, loading, finished }
|
||||||
|
|||||||
@@ -63,13 +63,14 @@ Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sen
|
|||||||
final client = await genClient(
|
final client = await genClient(
|
||||||
req.spi,
|
req.spi,
|
||||||
privateKey: req.privateKey,
|
privateKey: req.privateKey,
|
||||||
jumpSpi: req.jumpSpi,
|
jumpChain: req.jumpChain,
|
||||||
jumpPrivateKey: req.jumpPrivateKey,
|
jumpPrivateKeys: req.jumpPrivateKeys,
|
||||||
|
knownHostFingerprints: req.knownHostFingerprints,
|
||||||
);
|
);
|
||||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
mainSendPort.send(SftpWorkerStatus.sshConnected);
|
||||||
|
|
||||||
/// Create the directory if not exists
|
/// Create the directory if not exists
|
||||||
final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf('/'));
|
final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf(Pfs.seperator));
|
||||||
await Directory(dirPath).create(recursive: true);
|
await Directory(dirPath).create(recursive: true);
|
||||||
|
|
||||||
/// Use [FileMode.write] to overwrite the file
|
/// Use [FileMode.write] to overwrite the file
|
||||||
@@ -119,10 +120,11 @@ Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendE
|
|||||||
final client = await genClient(
|
final client = await genClient(
|
||||||
req.spi,
|
req.spi,
|
||||||
privateKey: req.privateKey,
|
privateKey: req.privateKey,
|
||||||
jumpSpi: req.jumpSpi,
|
jumpChain: req.jumpChain,
|
||||||
jumpPrivateKey: req.jumpPrivateKey,
|
jumpPrivateKeys: req.jumpPrivateKeys,
|
||||||
|
knownHostFingerprints: req.knownHostFingerprints,
|
||||||
);
|
);
|
||||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
mainSendPort.send(SftpWorkerStatus.sshConnected);
|
||||||
|
|
||||||
final local = File(req.localPath);
|
final local = File(req.localPath);
|
||||||
if (!await local.exists()) {
|
if (!await local.exists()) {
|
||||||
|
|||||||
343
lib/data/provider/ai/ask_ai.dart
Normal file
343
lib/data/provider/ai/ask_ai.dart
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:server_box/data/model/ai/ask_ai_models.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/store/setting.dart';
|
||||||
|
|
||||||
|
final askAiRepositoryProvider = Provider<AskAiRepository>((ref) {
|
||||||
|
return AskAiRepository();
|
||||||
|
});
|
||||||
|
|
||||||
|
class AskAiRepository {
|
||||||
|
AskAiRepository({Dio? dio}) : _dio = dio ?? Dio();
|
||||||
|
|
||||||
|
final Dio _dio;
|
||||||
|
|
||||||
|
SettingStore get _settings => Stores.setting;
|
||||||
|
|
||||||
|
/// Streams the AI response using the configured endpoint.
|
||||||
|
Stream<AskAiEvent> ask({
|
||||||
|
required String selection,
|
||||||
|
String? localeHint,
|
||||||
|
List<AskAiMessage> conversation = const [],
|
||||||
|
}) async* {
|
||||||
|
final baseUrl = _settings.askAiBaseUrl.fetch().trim();
|
||||||
|
final apiKey = _settings.askAiApiKey.fetch().trim();
|
||||||
|
final model = _settings.askAiModel.fetch().trim();
|
||||||
|
|
||||||
|
final missing = <AskAiConfigField>[];
|
||||||
|
if (baseUrl.isEmpty) missing.add(AskAiConfigField.baseUrl);
|
||||||
|
if (apiKey.isEmpty) missing.add(AskAiConfigField.apiKey);
|
||||||
|
if (model.isEmpty) missing.add(AskAiConfigField.model);
|
||||||
|
if (missing.isNotEmpty) {
|
||||||
|
throw AskAiConfigException(missingFields: missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedBaseUri = Uri.tryParse(baseUrl);
|
||||||
|
final hasScheme = parsedBaseUri?.hasScheme ?? false;
|
||||||
|
final hasHost = (parsedBaseUri?.host ?? '').isNotEmpty;
|
||||||
|
if (!hasScheme || !hasHost) {
|
||||||
|
throw AskAiConfigException(invalidBaseUrl: baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
final uri = _composeUri(baseUrl, '/v1/chat/completions');
|
||||||
|
final authHeader = apiKey.startsWith('Bearer ') ? apiKey : 'Bearer $apiKey';
|
||||||
|
final headers = <String, String>{
|
||||||
|
Headers.acceptHeader: 'text/event-stream',
|
||||||
|
Headers.contentTypeHeader: Headers.jsonContentType,
|
||||||
|
'Authorization': authHeader,
|
||||||
|
};
|
||||||
|
|
||||||
|
final requestBody = _buildRequestBody(
|
||||||
|
model: model,
|
||||||
|
selection: selection,
|
||||||
|
localeHint: localeHint,
|
||||||
|
conversation: conversation,
|
||||||
|
);
|
||||||
|
|
||||||
|
Response<ResponseBody> response;
|
||||||
|
try {
|
||||||
|
response = await _dio.postUri<ResponseBody>(
|
||||||
|
uri,
|
||||||
|
data: jsonEncode(requestBody),
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
headers: headers,
|
||||||
|
sendTimeout: const Duration(seconds: 20),
|
||||||
|
receiveTimeout: const Duration(minutes: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw AskAiNetworkException(message: e.message ?? 'Request failed', cause: e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = response.data;
|
||||||
|
if (body == null) {
|
||||||
|
throw AskAiNetworkException(message: 'Empty response body');
|
||||||
|
}
|
||||||
|
|
||||||
|
final contentBuffer = StringBuffer();
|
||||||
|
final commands = <AskAiCommand>[];
|
||||||
|
final toolBuilders = <int, _ToolCallBuilder>{};
|
||||||
|
final utf8Stream = body.stream.cast<List<int>>().transform(utf8.decoder);
|
||||||
|
final carry = StringBuffer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await for (final chunk in utf8Stream) {
|
||||||
|
carry.write(chunk);
|
||||||
|
final segments = carry.toString().split('\n\n');
|
||||||
|
carry
|
||||||
|
..clear()
|
||||||
|
..write(segments.removeLast());
|
||||||
|
|
||||||
|
for (final segment in segments) {
|
||||||
|
final lines = segment.split('\n');
|
||||||
|
for (final rawLine in lines) {
|
||||||
|
final line = rawLine.trim();
|
||||||
|
if (line.isEmpty || !line.startsWith('data:')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final payload = line.substring(5).trim();
|
||||||
|
if (payload.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (payload == '[DONE]') {
|
||||||
|
yield AskAiCompleted(
|
||||||
|
fullText: contentBuffer.toString(),
|
||||||
|
commands: List.unmodifiable(commands),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> json;
|
||||||
|
try {
|
||||||
|
json = jsonDecode(payload) as Map<String, dynamic>;
|
||||||
|
} catch (e, s) {
|
||||||
|
yield AskAiStreamError(e, s);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final choices = json['choices'];
|
||||||
|
if (choices is! List || choices.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final choice in choices) {
|
||||||
|
if (choice is! Map<String, dynamic>) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final delta = choice['delta'];
|
||||||
|
if (delta is Map<String, dynamic>) {
|
||||||
|
final content = delta['content'];
|
||||||
|
if (content is String && content.isNotEmpty) {
|
||||||
|
contentBuffer.write(content);
|
||||||
|
yield AskAiContentDelta(content);
|
||||||
|
} else if (content is List) {
|
||||||
|
for (final item in content) {
|
||||||
|
if (item is Map<String, dynamic>) {
|
||||||
|
final text = item['text'] as String?;
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
contentBuffer.write(text);
|
||||||
|
yield AskAiContentDelta(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final toolCalls = delta['tool_calls'];
|
||||||
|
if (toolCalls is List) {
|
||||||
|
for (final toolCall in toolCalls) {
|
||||||
|
if (toolCall is! Map<String, dynamic>) continue;
|
||||||
|
final index = toolCall['index'] as int? ?? 0;
|
||||||
|
final builder = toolBuilders.putIfAbsent(index, _ToolCallBuilder.new);
|
||||||
|
final function = toolCall['function'];
|
||||||
|
if (function is Map<String, dynamic>) {
|
||||||
|
builder.name ??= function['name'] as String?;
|
||||||
|
final args = function['arguments'] as String?;
|
||||||
|
if (args != null && args.isNotEmpty) {
|
||||||
|
builder.arguments.write(args);
|
||||||
|
final command = builder.tryBuild();
|
||||||
|
if (command != null) {
|
||||||
|
commands.add(command);
|
||||||
|
yield AskAiToolSuggestion(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final finishReason = choice['finish_reason'];
|
||||||
|
if (finishReason == 'tool_calls') {
|
||||||
|
for (final builder in toolBuilders.values) {
|
||||||
|
final command = builder.tryBuild(force: true);
|
||||||
|
if (command != null) {
|
||||||
|
commands.add(command);
|
||||||
|
yield AskAiToolSuggestion(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toolBuilders.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush remaining buffer if [DONE] not received.
|
||||||
|
if (contentBuffer.isNotEmpty || commands.isNotEmpty) {
|
||||||
|
yield AskAiCompleted(
|
||||||
|
fullText: contentBuffer.toString(),
|
||||||
|
commands: List.unmodifiable(commands),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
yield AskAiStreamError(e, s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildRequestBody({
|
||||||
|
required String model,
|
||||||
|
required String selection,
|
||||||
|
required List<AskAiMessage> conversation,
|
||||||
|
String? localeHint,
|
||||||
|
}) {
|
||||||
|
final promptBuffer = StringBuffer()
|
||||||
|
..writeln('你是一个 SSH 终端助手。')
|
||||||
|
..writeln('用户会提供一段终端输出或命令,请结合上下文给出解释。')
|
||||||
|
..writeln('当需要给出可执行命令时,调用 `recommend_shell` 工具,并提供简短描述。')
|
||||||
|
..writeln('仅在非常确定命令安全时才给出建议。');
|
||||||
|
|
||||||
|
if (localeHint != null && localeHint.isNotEmpty) {
|
||||||
|
promptBuffer
|
||||||
|
.writeln('请优先使用用户的语言输出:$localeHint。');
|
||||||
|
}
|
||||||
|
|
||||||
|
final messages = <Map<String, String>>[
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content': promptBuffer.toString(),
|
||||||
|
},
|
||||||
|
...conversation.map((message) => {
|
||||||
|
'role': message.apiRole,
|
||||||
|
'content': message.content,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': '以下是终端选中的内容:\n$selection',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model': model,
|
||||||
|
'stream': true,
|
||||||
|
'messages': messages,
|
||||||
|
'tools': [
|
||||||
|
{
|
||||||
|
'type': 'function',
|
||||||
|
'function': {
|
||||||
|
'name': 'recommend_shell',
|
||||||
|
'description': '返回一个用户可以直接复制执行的终端命令。',
|
||||||
|
'parameters': {
|
||||||
|
'type': 'object',
|
||||||
|
'required': ['command'],
|
||||||
|
'properties': {
|
||||||
|
'command': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '完整的终端命令,确保可以被粘贴后直接执行。',
|
||||||
|
},
|
||||||
|
'description': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '简述该命令的作用或注意事项。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri _composeUri(String base, String path) {
|
||||||
|
final sanitizedBase = base.replaceAll(RegExp(r'/+$'), '');
|
||||||
|
final sanitizedPath = path.replaceFirst(RegExp(r'^/+'), '');
|
||||||
|
return Uri.parse('$sanitizedBase/$sanitizedPath');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToolCallBuilder {
|
||||||
|
_ToolCallBuilder();
|
||||||
|
|
||||||
|
final StringBuffer arguments = StringBuffer();
|
||||||
|
String? name;
|
||||||
|
bool _emitted = false;
|
||||||
|
|
||||||
|
AskAiCommand? tryBuild({bool force = false}) {
|
||||||
|
if (_emitted && !force) return null;
|
||||||
|
final raw = arguments.toString();
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
|
final command = decoded['command'] as String?;
|
||||||
|
if (command == null || command.trim().isEmpty) {
|
||||||
|
if (force) {
|
||||||
|
_emitted = true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final description = decoded['description'] as String? ?? decoded['explanation'] as String? ?? '';
|
||||||
|
_emitted = true;
|
||||||
|
return AskAiCommand(
|
||||||
|
command: command.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
toolName: name,
|
||||||
|
);
|
||||||
|
} on FormatException {
|
||||||
|
if (force) {
|
||||||
|
_emitted = true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
enum AskAiConfigField { baseUrl, apiKey, model }
|
||||||
|
|
||||||
|
class AskAiConfigException implements Exception {
|
||||||
|
const AskAiConfigException({this.missingFields = const [], this.invalidBaseUrl});
|
||||||
|
|
||||||
|
final List<AskAiConfigField> missingFields;
|
||||||
|
final String? invalidBaseUrl;
|
||||||
|
|
||||||
|
bool get hasInvalidBaseUrl => (invalidBaseUrl ?? '').isNotEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final parts = <String>[];
|
||||||
|
if (missingFields.isNotEmpty) {
|
||||||
|
parts.add('missing: ${missingFields.map((e) => e.name).join(', ')}');
|
||||||
|
}
|
||||||
|
if (hasInvalidBaseUrl) {
|
||||||
|
parts.add('invalidBaseUrl: $invalidBaseUrl');
|
||||||
|
}
|
||||||
|
if (parts.isEmpty) {
|
||||||
|
return 'AskAiConfigException()';
|
||||||
|
}
|
||||||
|
return 'AskAiConfigException(${parts.join('; ')})';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class AskAiNetworkException implements Exception {
|
||||||
|
const AskAiNetworkException({required this.message, this.cause});
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final Object? cause;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AskAiNetworkException(message: $message)';
|
||||||
|
}
|
||||||
@@ -40,22 +40,15 @@ class ContainerNotifier extends _$ContainerNotifier {
|
|||||||
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
|
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
|
||||||
final type = Stores.container.getType(hostId);
|
final type = Stores.container.getType(hostId);
|
||||||
final initialState = ContainerState(type: type);
|
final initialState = ContainerState(type: type);
|
||||||
|
|
||||||
// Async initialization
|
// Async initialization
|
||||||
Future.microtask(() => refresh());
|
Future.microtask(() => refresh());
|
||||||
|
|
||||||
return initialState;
|
return initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setType(ContainerType type) async {
|
Future<void> setType(ContainerType type) async {
|
||||||
state = state.copyWith(
|
state = state.copyWith(type: type, error: null, runLog: null, items: null, images: null, version: null);
|
||||||
type: type,
|
|
||||||
error: null,
|
|
||||||
runLog: null,
|
|
||||||
items: null,
|
|
||||||
images: null,
|
|
||||||
version: null,
|
|
||||||
);
|
|
||||||
Stores.container.setType(type, hostId);
|
Stores.container.setType(type, hostId);
|
||||||
sudoCompleter = Completer<bool>();
|
sudoCompleter = Completer<bool>();
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -180,16 +173,20 @@ class ContainerNotifier extends _$ContainerNotifier {
|
|||||||
try {
|
try {
|
||||||
final statsLines = statsRaw.split('\n');
|
final statsLines = statsRaw.split('\n');
|
||||||
statsLines.removeWhere((element) => element.isEmpty);
|
statsLines.removeWhere((element) => element.isEmpty);
|
||||||
for (var item in state.items!) {
|
final items = state.items;
|
||||||
|
if (items == null) return;
|
||||||
|
|
||||||
|
for (var item in items) {
|
||||||
final id = item.id;
|
final id = item.id;
|
||||||
if (id == null) continue;
|
if (id == null) continue;
|
||||||
|
if (id.length < 5) continue;
|
||||||
final statsLine = statsLines.firstWhereOrNull(
|
final statsLine = statsLines.firstWhereOrNull(
|
||||||
/// Use 5 characters to match the container id, possibility of mismatch
|
/// Use 5 characters to match the container id, possibility of mismatch
|
||||||
/// is very low.
|
/// is very low.
|
||||||
(element) => element.contains(id.substring(0, 5)),
|
(element) => element.contains(id.substring(0, 5)),
|
||||||
);
|
);
|
||||||
if (statsLine == null) continue;
|
if (statsLine == null) continue;
|
||||||
item.parseStats(statsLine);
|
item.parseStats(statsLine, state.version);
|
||||||
}
|
}
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -267,7 +264,6 @@ class ContainerNotifier extends _$ContainerNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const _jsonFmt = '--format "{{json .}}"';
|
const _jsonFmt = '--format "{{json .}}"';
|
||||||
|
|
||||||
enum ContainerCmdType {
|
enum ContainerCmdType {
|
||||||
@@ -284,7 +280,7 @@ enum ContainerCmdType {
|
|||||||
return switch (this) {
|
return switch (this) {
|
||||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||||
ContainerCmdType.ps => switch (type) {
|
ContainerCmdType.ps => switch (type) {
|
||||||
/// TODO: Rollback to json format when permformance recovers.
|
/// TODO: Rollback to json format when performance recovers.
|
||||||
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
|
||||||
ContainerType.docker =>
|
ContainerType.docker =>
|
||||||
'$prefix ps -a --format "table {{printf \\"'
|
'$prefix ps -a --format "table {{printf \\"'
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class ContainerNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
|
String _$containerNotifierHash() => r'85457ec75264199c284572ee45beeaccba2044a1';
|
||||||
|
|
||||||
final class ContainerNotifierFamily extends $Family
|
final class ContainerNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:dartssh2/dartssh2.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
@@ -108,7 +107,7 @@ class PveNotifier extends _$PveNotifier {
|
|||||||
final newUrl = Uri.parse(
|
final newUrl = Uri.parse(
|
||||||
addr,
|
addr,
|
||||||
).replace(host: 'localhost', port: _localPort).toString();
|
).replace(host: 'localhost', port: _localPort).toString();
|
||||||
debugPrint('Forwarding $newUrl to $addr');
|
dprint('Forwarding $newUrl to $addr');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +234,15 @@ class PveNotifier extends _$PveNotifier {
|
|||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
try {
|
try {
|
||||||
await _serverSocket.close();
|
await _serverSocket.close();
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to close server socket', e, s);
|
||||||
|
}
|
||||||
for (final forward in _forwards) {
|
for (final forward in _forwards) {
|
||||||
try {
|
try {
|
||||||
forward.close();
|
forward.close();
|
||||||
} catch (_) {}
|
} catch (e, s) {
|
||||||
|
Loggers.app.warning('Failed to close forward', e, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class PveNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$pveNotifierHash() => r'ba5f2d6cb47c33735f7cc09b771b4a86501b86c6';
|
String _$pveNotifierHash() => r'1e71faadee074b9c07bee731ef4ae6505e791967';
|
||||||
|
|
||||||
final class PveNotifierFamily extends $Family
|
final class PveNotifierFamily extends $Family
|
||||||
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
||||||
|
|||||||
@@ -103,37 +103,44 @@ class ServersNotifier extends _$ServersNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait(
|
final serversToRefresh = <MapEntry<String, Spi>>[];
|
||||||
state.servers.entries.map((entry) async {
|
final idsToResetLimiter = <String>[];
|
||||||
final serverId = entry.key;
|
|
||||||
final spi = entry.value;
|
|
||||||
|
|
||||||
if (onlyFailed) {
|
for (final entry in state.servers.entries) {
|
||||||
final serverState = ref.read(serverProvider(serverId));
|
final serverId = entry.key;
|
||||||
if (serverState.conn != ServerConn.failed) return;
|
final spi = entry.value;
|
||||||
TryLimiter.reset(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.manualDisconnectedIds.contains(serverId)) return;
|
if (state.manualDisconnectedIds.contains(serverId)) continue;
|
||||||
|
|
||||||
final serverState = ref.read(serverProvider(serverId));
|
final serverState = ref.read(serverProvider(serverId));
|
||||||
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverNotifier = ref.read(serverProvider(serverId).notifier);
|
if (onlyFailed) {
|
||||||
await serverNotifier.refresh();
|
if (serverState.conn != ServerConn.failed) continue;
|
||||||
}),
|
idsToResetLimiter.add(serverId);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) continue;
|
||||||
|
|
||||||
|
serversToRefresh.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final id in idsToResetLimiter) {
|
||||||
|
TryLimiter.reset(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in serversToRefresh) {
|
||||||
|
final serverNotifier = ref.read(serverProvider(entry.key).notifier);
|
||||||
|
serverNotifier.refresh().ignore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startAutoRefresh() async {
|
Future<void> startAutoRefresh() async {
|
||||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
if (duration == 0) return;
|
if (duration == 0) return;
|
||||||
if (duration < 0 || duration > 10 || duration == 1) {
|
if (duration <= 1 || duration > 10) {
|
||||||
duration = 3;
|
|
||||||
Loggers.app.warning('Invalid duration: $duration, use default 3');
|
Loggers.app.warning('Invalid duration: $duration, use default 3');
|
||||||
|
duration = 3;
|
||||||
}
|
}
|
||||||
final timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
final timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -145,8 +152,8 @@ class ServersNotifier extends _$ServersNotifier {
|
|||||||
final timer = state.autoRefreshTimer;
|
final timer = state.autoRefreshTimer;
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
state = state.copyWith(autoRefreshTimer: null);
|
|
||||||
}
|
}
|
||||||
|
state = state.copyWith(autoRefreshTimer: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isAutoRefreshOn => state.autoRefreshTimer != null;
|
bool get isAutoRefreshOn => state.autoRefreshTimer != null;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class ServersNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$serversNotifierHash() => r'3292bdce7d602ff64687b05ff81d120e71761ec2';
|
String _$serversNotifierHash() => r'277d1b219235f14bcc1b82a1e16260c2f28decdb';
|
||||||
|
|
||||||
abstract class _$ServersNotifier extends $Notifier<ServersState> {
|
abstract class _$ServersNotifier extends $Notifier<ServersState> {
|
||||||
ServersState build();
|
ServersState build();
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
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:flutter_gbk2utf8/flutter_gbk2utf8.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:server_box/core/extension/ssh_client.dart';
|
import 'package:server_box/core/extension/ssh_client.dart';
|
||||||
import 'package:server_box/core/utils/server.dart';
|
import 'package:server_box/core/utils/server.dart';
|
||||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||||
|
import 'package:server_box/data/helper/ssh_decoder.dart';
|
||||||
import 'package:server_box/data/helper/system_detector.dart';
|
import 'package:server_box/data/helper/system_detector.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||||
@@ -36,7 +35,6 @@ abstract class ServerState with _$ServerState {
|
|||||||
required ServerStatus status,
|
required ServerStatus status,
|
||||||
@Default(ServerConn.disconnected) ServerConn conn,
|
@Default(ServerConn.disconnected) ServerConn conn,
|
||||||
SSHClient? client,
|
SSHClient? client,
|
||||||
Future<void>? updateFuture,
|
|
||||||
}) = _ServerState;
|
}) = _ServerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,19 +80,16 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh server status
|
// Refresh server status
|
||||||
|
bool _isRefreshing = false;
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
if (state.updateFuture != null) {
|
if (_isRefreshing) return;
|
||||||
await state.updateFuture;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final updateFuture = _updateServer();
|
|
||||||
state = state.copyWith(updateFuture: updateFuture);
|
|
||||||
|
|
||||||
|
_isRefreshing = true;
|
||||||
try {
|
try {
|
||||||
await updateFuture;
|
await _updateServer();
|
||||||
} finally {
|
} finally {
|
||||||
state = state.copyWith(updateFuture: null);
|
_isRefreshing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +135,7 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
|
|
||||||
final time2 = DateTime.now();
|
final time2 = DateTime.now();
|
||||||
final spentTime = time2.difference(time1).inMilliseconds;
|
final spentTime = time2.difference(time1).inMilliseconds;
|
||||||
if (spi.jumpId == null) {
|
if ((spi.jumpChainIds?.isNotEmpty != true) && spi.jumpId == null) {
|
||||||
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
||||||
} else {
|
} else {
|
||||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||||
@@ -213,7 +208,9 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
final newStatus = state.status..system = detectedSystemType;
|
final newStatus = state.status..system = detectedSystemType;
|
||||||
updateStatus(newStatus);
|
updateStatus(newStatus);
|
||||||
|
|
||||||
final (_, writeScriptResult) = await state.client!.exec(
|
Loggers.app.info('Writing script for ${spi.name} (${detectedSystemType.name})');
|
||||||
|
|
||||||
|
final (stdoutResult, writeScriptResult) = await state.client!.execSafe(
|
||||||
(session) async {
|
(session) async {
|
||||||
final scriptRaw = ShellFuncManager.allScript(
|
final scriptRaw = ShellFuncManager.allScript(
|
||||||
spi.custom?.cmds,
|
spi.custom?.cmds,
|
||||||
@@ -228,10 +225,22 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
systemType: detectedSystemType,
|
systemType: detectedSystemType,
|
||||||
customDir: spi.custom?.scriptDir,
|
customDir: spi.custom?.scriptDir,
|
||||||
),
|
),
|
||||||
|
systemType: detectedSystemType,
|
||||||
|
context: 'WriteScript<${spi.name}>',
|
||||||
);
|
);
|
||||||
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
|
||||||
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
|
if (stdoutResult.isNotEmpty) {
|
||||||
throw writeScriptResult;
|
Loggers.app.info('Script write stdout for ${spi.name}: $stdoutResult');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeScriptResult.isNotEmpty) {
|
||||||
|
Loggers.app.warning('Script write stderr for ${spi.name}: $writeScriptResult');
|
||||||
|
if (detectedSystemType != SystemType.windows) {
|
||||||
|
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
|
||||||
|
throw writeScriptResult;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Loggers.app.info('Script written successfully for ${spi.name}');
|
||||||
}
|
}
|
||||||
} on SSHAuthAbortError catch (e) {
|
} on SSHAuthAbortError catch (e) {
|
||||||
TryLimiter.inc(sid);
|
TryLimiter.inc(sid);
|
||||||
@@ -278,43 +287,25 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
String? raw;
|
String? raw;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final execResult = await state.client?.run(
|
final statusCmd = ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir);
|
||||||
ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir),
|
// Loggers.app.info('Running status command for ${spi.name} (${state.status.system.name}): $statusCmd');
|
||||||
);
|
final execResult = await state.client?.run(statusCmd);
|
||||||
if (execResult != null) {
|
if (execResult != null) {
|
||||||
String? rawStr;
|
raw = SSHDecoder.decode(
|
||||||
bool needGbk = false;
|
execResult,
|
||||||
try {
|
isWindows: state.status.system == SystemType.windows,
|
||||||
rawStr = utf8.decode(execResult, allowMalformed: true);
|
context: 'GetStatus<${spi.name}>',
|
||||||
// If there are unparseable characters, try fallback to GBK decoding
|
);
|
||||||
if (rawStr.contains('<EFBFBD>')) {
|
// Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');
|
||||||
Loggers.app.warning('UTF8 decoding failed, use GBK decoding');
|
|
||||||
needGbk = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e);
|
|
||||||
needGbk = true;
|
|
||||||
}
|
|
||||||
if (needGbk) {
|
|
||||||
try {
|
|
||||||
rawStr = gbk.decode(execResult);
|
|
||||||
} catch (e2) {
|
|
||||||
Loggers.app.warning('GBK decoding failed', e2);
|
|
||||||
rawStr = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rawStr == null) {
|
|
||||||
Loggers.app.warning('Decoding failed, execResult: $execResult');
|
|
||||||
}
|
|
||||||
raw = rawStr;
|
|
||||||
} else {
|
} else {
|
||||||
raw = execResult.toString();
|
raw = '';
|
||||||
|
Loggers.app.warning('No status result from ${spi.name}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw == null || raw.isEmpty) {
|
if (raw.isEmpty) {
|
||||||
TryLimiter.inc(sid);
|
TryLimiter.inc(sid);
|
||||||
final newStatus = state.status
|
final newStatus = state.status
|
||||||
..err = SSHErr(type: SSHErrType.segements, message: 'decode or split failed, raw:\n$raw');
|
..err = SSHErr(type: SSHErrType.segements, message: 'Empty response from server');
|
||||||
updateStatus(newStatus);
|
updateStatus(newStatus);
|
||||||
updateConnection(ServerConn.failed);
|
updateConnection(ServerConn.failed);
|
||||||
|
|
||||||
@@ -324,7 +315,7 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
|
segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
|
||||||
if (raw.isEmpty || segments.isEmpty) {
|
if (segments.isEmpty) {
|
||||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||||
// Keep previous server status when error occurs
|
// Keep previous server status when error occurs
|
||||||
if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
|
if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
|
||||||
@@ -333,7 +324,7 @@ class ServerNotifier extends _$ServerNotifier {
|
|||||||
}
|
}
|
||||||
TryLimiter.inc(sid);
|
TryLimiter.inc(sid);
|
||||||
final newStatus = state.status
|
final newStatus = state.status
|
||||||
..err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
|
..err = SSHErr(type: SSHErrType.segements, message: 'Separate segments failed, raw:\n$raw');
|
||||||
updateStatus(newStatus);
|
updateStatus(newStatus);
|
||||||
updateConnection(ServerConn.failed);
|
updateConnection(ServerConn.failed);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$ServerState {
|
mixin _$ServerState {
|
||||||
|
|
||||||
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client; Future<void>? get updateFuture;
|
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client;
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -25,16 +25,16 @@ $ServerStateCopyWith<ServerState> get copyWith => _$ServerStateCopyWithImpl<Serv
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
|
int get hashCode => Object.hash(runtimeType,spi,status,conn,client);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
|
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ abstract mixin class $ServerStateCopyWith<$Res> {
|
|||||||
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
|
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
|
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -62,14 +62,13 @@ class _$ServerStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
||||||
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||||
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
|
as SSHClient?,
|
||||||
as Future<void>?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
@@ -163,10 +162,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _ServerState() when $default != null:
|
case _ServerState() when $default != null:
|
||||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -184,10 +183,10 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _ServerState():
|
case _ServerState():
|
||||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -204,10 +203,10 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _ServerState() when $default != null:
|
case _ServerState() when $default != null:
|
||||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -219,14 +218,13 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
|
|||||||
|
|
||||||
|
|
||||||
class _ServerState implements ServerState {
|
class _ServerState implements ServerState {
|
||||||
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client, this.updateFuture});
|
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client});
|
||||||
|
|
||||||
|
|
||||||
@override final Spi spi;
|
@override final Spi spi;
|
||||||
@override final ServerStatus status;
|
@override final ServerStatus status;
|
||||||
@override@JsonKey() final ServerConn conn;
|
@override@JsonKey() final ServerConn conn;
|
||||||
@override final SSHClient? client;
|
@override final SSHClient? client;
|
||||||
@override final Future<void>? updateFuture;
|
|
||||||
|
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -238,16 +236,16 @@ _$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
|
int get hashCode => Object.hash(runtimeType,spi,status,conn,client);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
|
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -258,7 +256,7 @@ abstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith
|
|||||||
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
|
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
|
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -275,14 +273,13 @@ class __$ServerStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of ServerState
|
/// Create a copy of ServerState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {
|
||||||
return _then(_ServerState(
|
return _then(_ServerState(
|
||||||
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
||||||
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||||
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
|
as SSHClient?,
|
||||||
as Future<void>?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class ServerNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$serverNotifierHash() => r'185c6b4546c3bc526f5b2ca79d16aed665818863';
|
String _$serverNotifierHash() => r'52e806bcc32a7818d1ec2b07a3c683b06885c9f8';
|
||||||
|
|
||||||
final class ServerNotifierFamily extends $Family
|
final class ServerNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
abstract class BuildData {
|
abstract class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 1262;
|
static const int build = 1291;
|
||||||
static const int script = 69;
|
static const int script = 70;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ abstract final class GithubIds {
|
|||||||
'MasedMSD',
|
'MasedMSD',
|
||||||
'GitGitro',
|
'GitGitro',
|
||||||
'Shin-suechtig',
|
'Shin-suechtig',
|
||||||
|
'GT-610'
|
||||||
};
|
};
|
||||||
|
|
||||||
static const participants = <GhId>{
|
static const participants = <GhId>{
|
||||||
|
|||||||
@@ -89,15 +89,12 @@ class ServerStore extends HiveStore {
|
|||||||
// Replace ids in jump server settings.
|
// Replace ids in jump server settings.
|
||||||
final spi = get<Spi>(newId);
|
final spi = get<Spi>(newId);
|
||||||
if (spi != null) {
|
if (spi != null) {
|
||||||
final jumpId = spi.jumpId; // This could be an oldId.
|
final jumpChainIds = spi.jumpChainIds ?? (spi.jumpId == null ? null : [spi.jumpId!]);
|
||||||
// Check if this jumpId corresponds to a server that was also migrated.
|
if (jumpChainIds == null || jumpChainIds.isEmpty) continue;
|
||||||
if (jumpId != null && idMap.containsKey(jumpId)) {
|
|
||||||
final newJumpId = idMap[jumpId];
|
final newChain = jumpChainIds.map((e) => idMap[e] ?? e).toList();
|
||||||
if (spi.jumpId != newJumpId) {
|
final newSpi = spi.copyWith(jumpId: null, jumpChainIds: newChain);
|
||||||
final newSpi = spi.copyWith(jumpId: newJumpId);
|
update(spi, newSpi);
|
||||||
update(spi, newSpi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace ids in [Snippet]
|
// Replace ids in [Snippet]
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ class SettingStore extends HiveStore {
|
|||||||
|
|
||||||
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
|
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
|
||||||
|
|
||||||
|
/// Trusted SSH host key fingerprints keyed by `serverId::keyType`.
|
||||||
|
late final sshKnownHostFingerprints = propertyDefault<Map<String, String>>(
|
||||||
|
'sshKnownHostFingerprints',
|
||||||
|
const {},
|
||||||
|
fromObj: (raw) {
|
||||||
|
if (raw is Map) {
|
||||||
|
return raw.map((key, value) => MapEntry(key.toString(), value.toString()));
|
||||||
|
}
|
||||||
|
return <String, String>{};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Editor theme
|
// Editor theme
|
||||||
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
|
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
|
||||||
|
|
||||||
@@ -142,6 +154,11 @@ class SettingStore extends HiveStore {
|
|||||||
/// Whether collapse UI items by default
|
/// Whether collapse UI items by default
|
||||||
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
||||||
|
|
||||||
|
/// Terminal AI helper configuration
|
||||||
|
late final askAiBaseUrl = propertyDefault('askAiBaseUrl', 'https://api.openai.com');
|
||||||
|
late final askAiApiKey = propertyDefault('askAiApiKey', '');
|
||||||
|
late final askAiModel = propertyDefault('askAiModel', 'gpt-4o-mini');
|
||||||
|
|
||||||
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
|
late final serverFuncBtns = listProperty('serverBtns', defaultValue: ServerFuncBtn.defaultIdxs);
|
||||||
|
|
||||||
/// Docker is more popular than podman, set to `false` to use docker
|
/// Docker is more popular than podman, set to `false` to use docker
|
||||||
|
|||||||
@@ -155,6 +155,102 @@ abstract class AppLocalizations {
|
|||||||
/// **'Already in last directory.'**
|
/// **'Already in last directory.'**
|
||||||
String get alreadyLastDir;
|
String get alreadyLastDir;
|
||||||
|
|
||||||
|
/// No description provided for @askAi.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask AI'**
|
||||||
|
String get askAi;
|
||||||
|
|
||||||
|
/// No description provided for @askAiApiKey.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'API Key'**
|
||||||
|
String get askAiApiKey;
|
||||||
|
|
||||||
|
/// No description provided for @askAiAwaitingResponse.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Waiting for AI response...'**
|
||||||
|
String get askAiAwaitingResponse;
|
||||||
|
|
||||||
|
/// No description provided for @askAiBaseUrl.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Base URL'**
|
||||||
|
String get askAiBaseUrl;
|
||||||
|
|
||||||
|
/// No description provided for @askAiCommandInserted.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Command inserted into terminal'**
|
||||||
|
String get askAiCommandInserted;
|
||||||
|
|
||||||
|
/// No description provided for @askAiConfigMissing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Please configure {fields} in Settings.'**
|
||||||
|
String askAiConfigMissing(Object fields);
|
||||||
|
|
||||||
|
/// No description provided for @askAiConfirmExecute.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Confirm before executing'**
|
||||||
|
String get askAiConfirmExecute;
|
||||||
|
|
||||||
|
/// No description provided for @askAiConversation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'AI conversation'**
|
||||||
|
String get askAiConversation;
|
||||||
|
|
||||||
|
/// No description provided for @askAiDisclaimer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'AI may be incorrect. Review carefully before applying.'**
|
||||||
|
String get askAiDisclaimer;
|
||||||
|
|
||||||
|
/// No description provided for @askAiFollowUpHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask a follow-up...'**
|
||||||
|
String get askAiFollowUpHint;
|
||||||
|
|
||||||
|
/// No description provided for @askAiInsertTerminal.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Insert into terminal'**
|
||||||
|
String get askAiInsertTerminal;
|
||||||
|
|
||||||
|
/// No description provided for @askAiModel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Model'**
|
||||||
|
String get askAiModel;
|
||||||
|
|
||||||
|
/// No description provided for @askAiNoResponse.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No response'**
|
||||||
|
String get askAiNoResponse;
|
||||||
|
|
||||||
|
/// No description provided for @askAiRecommendedCommand.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'AI suggested command'**
|
||||||
|
String get askAiRecommendedCommand;
|
||||||
|
|
||||||
|
/// No description provided for @askAiSelectedContent.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Selected content'**
|
||||||
|
String get askAiSelectedContent;
|
||||||
|
|
||||||
|
/// No description provided for @askAiUsageHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Used in SSH Terminal'**
|
||||||
|
String get askAiUsageHint;
|
||||||
|
|
||||||
/// No description provided for @atLeastOneTab.
|
/// No description provided for @atLeastOneTab.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -285,13 +381,13 @@ abstract class AppLocalizations {
|
|||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.'**
|
/// **'Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.'**
|
||||||
String clearServerStatsContent(String serverName);
|
String clearServerStatsContent(Object serverName);
|
||||||
|
|
||||||
/// No description provided for @clearServerStatsTitle.
|
/// No description provided for @clearServerStatsTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Clear {serverName} Statistics'**
|
/// **'Clear {serverName} Statistics'**
|
||||||
String clearServerStatsTitle(String serverName);
|
String clearServerStatsTitle(Object serverName);
|
||||||
|
|
||||||
/// No description provided for @clearThisServerStats.
|
/// No description provided for @clearThisServerStats.
|
||||||
///
|
///
|
||||||
@@ -1052,6 +1148,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Private Key'**
|
/// **'Private Key'**
|
||||||
String get privateKey;
|
String get privateKey;
|
||||||
|
|
||||||
|
/// No description provided for @privateKeyNotFoundFmt.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Private key [{keyId}] not found.'**
|
||||||
|
String privateKeyNotFoundFmt(Object keyId);
|
||||||
|
|
||||||
/// No description provided for @process.
|
/// No description provided for @process.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1376,6 +1478,42 @@ abstract class AppLocalizations {
|
|||||||
/// **'Imported {count} servers from SSH config'**
|
/// **'Imported {count} servers from SSH config'**
|
||||||
String sshConfigImported(Object count);
|
String sshConfigImported(Object count);
|
||||||
|
|
||||||
|
/// No description provided for @sshHostKeyChangedDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The SSH host key changed for {serverName}. Only continue if you trust this server.'**
|
||||||
|
String sshHostKeyChangedDesc(Object serverName);
|
||||||
|
|
||||||
|
/// No description provided for @sshHostKeyFingerprintMd5Base64.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fingerprint (MD5 base64): {fingerprint}'**
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint);
|
||||||
|
|
||||||
|
/// No description provided for @sshHostKeyFingerprintMd5Hex.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fingerprint (MD5 hex): {fingerprint}'**
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint);
|
||||||
|
|
||||||
|
/// Label for the SSH host key type displayed in the host key verification dialog.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'SSH host key type'**
|
||||||
|
String get sshHostKeyType;
|
||||||
|
|
||||||
|
/// No description provided for @sshHostKeyNewDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A new SSH host key was received from {serverName}. Review the fingerprint before trusting.'**
|
||||||
|
String sshHostKeyNewDesc(Object serverName);
|
||||||
|
|
||||||
|
/// No description provided for @sshHostKeyStoredFingerprint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Stored fingerprint: {fingerprint}'**
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint);
|
||||||
|
|
||||||
/// No description provided for @sshConfigManualSelect.
|
/// No description provided for @sshConfigManualSelect.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1747,6 +1885,54 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
/// **'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.'**
|
||||||
String get writeScriptTip;
|
String get writeScriptTip;
|
||||||
|
|
||||||
|
/// No description provided for @menuSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Setting'**
|
||||||
|
String get menuSettings;
|
||||||
|
|
||||||
|
/// No description provided for @menuQuit.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Quit'**
|
||||||
|
String get menuQuit;
|
||||||
|
|
||||||
|
/// No description provided for @menuNavigate.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Navigate'**
|
||||||
|
String get menuNavigate;
|
||||||
|
|
||||||
|
/// No description provided for @menuInfo.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Info'**
|
||||||
|
String get menuInfo;
|
||||||
|
|
||||||
|
/// No description provided for @menuGitHubRepository.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'GitHub Repository'**
|
||||||
|
String get menuGitHubRepository;
|
||||||
|
|
||||||
|
/// No description provided for @menuWiki.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Wiki'**
|
||||||
|
String get menuWiki;
|
||||||
|
|
||||||
|
/// No description provided for @menuHelp.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Help'**
|
||||||
|
String get menuHelp;
|
||||||
|
|
||||||
|
/// No description provided for @logs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Logs'**
|
||||||
|
String get logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -28,6 +28,57 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Bereits im letzten Verzeichnis.';
|
String get alreadyLastDir => 'Bereits im letzten Verzeichnis.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'KI fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API-Schlüssel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Warte auf KI-Antwort...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Basis-URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Befehl ins Terminal eingefügt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Bitte konfigurieren Sie $fields in den Einstellungen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Vor Ausführung bestätigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'KI-Unterhaltung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer =>
|
||||||
|
'KI kann Fehler machen. Bitte vorsichtig verwenden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Weitere Frage stellen...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'In Terminal einfügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modell';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Keine Antwort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'KI-empfohlener Befehl';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Ausgewählter Inhalt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Verwendet im SSH-Terminal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein';
|
String get atLeastOneTab => 'Mindestens ein Tab muss ausgewählt sein';
|
||||||
|
|
||||||
@@ -99,12 +150,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Alle Statistiken löschen';
|
String get clearAllStatsTitle => 'Alle Statistiken löschen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"$serverName\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
|
return 'Sind Sie sicher, dass Sie die Verbindungsstatistiken für Server \"$serverName\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return '$serverName Statistiken löschen';
|
return '$serverName Statistiken löschen';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +581,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Private Key';
|
String get privateKey => 'Private Key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Privater Schlüssel [$keyId] wurde nicht gefunden.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Prozess';
|
String get process => 'Prozess';
|
||||||
|
|
||||||
@@ -713,6 +769,34 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return '$count Server aus SSH-Konfiguration importiert';
|
return '$count Server aus SSH-Konfiguration importiert';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'Der SSH-Hostschlüssel für $serverName hat sich geändert. Fahren Sie nur fort, wenn Sie diesem Server vertrauen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Fingerabdruck (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Fingerabdruck (MD5 Hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'SSH-Hostschlüsseltyp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Ein neuer SSH-Hostschlüssel wurde von $serverName empfangen. Prüfen Sie den Fingerabdruck, bevor Sie vertrauen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Gespeicherter Fingerabdruck: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?';
|
'Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?';
|
||||||
@@ -923,4 +1007,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
'Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Protokolle';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,57 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Already in last directory.';
|
String get alreadyLastDir => 'Already in last directory.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Ask AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API Key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Waiting for AI response...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Base URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Command inserted into terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Please configure $fields in Settings.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirm before executing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI conversation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer =>
|
||||||
|
'AI may be incorrect. Review carefully before applying.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Ask a follow-up...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Insert into terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'No response';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI suggested command';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Selected content';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Used in SSH Terminal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'At least one tab must be selected';
|
String get atLeastOneTab => 'At least one tab must be selected';
|
||||||
|
|
||||||
@@ -98,12 +149,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Clear All Statistics';
|
String get clearAllStatsTitle => 'Clear All Statistics';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Are you sure you want to clear connection statistics for server \"$serverName\"? This action cannot be undone.';
|
return 'Are you sure you want to clear connection statistics for server \"$serverName\"? This action cannot be undone.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Clear $serverName Statistics';
|
return 'Clear $serverName Statistics';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +578,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Private Key';
|
String get privateKey => 'Private Key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Private key [$keyId] not found.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Process';
|
String get process => 'Process';
|
||||||
|
|
||||||
@@ -707,6 +763,34 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Imported $count servers from SSH config';
|
return 'Imported $count servers from SSH config';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'The SSH host key changed for $serverName. Only continue if you trust this server.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Fingerprint (MD5 base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Fingerprint (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'SSH host key type';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'A new SSH host key was received from $serverName. Review the fingerprint before trusting.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Stored fingerprint: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Would you like to select the SSH config file manually?';
|
'Would you like to select the SSH config file manually?';
|
||||||
@@ -914,4 +998,28 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
'After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Logs';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,57 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Ya estás en el directorio superior';
|
String get alreadyLastDir => 'Ya estás en el directorio superior';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Preguntar a la IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Clave API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Esperando la respuesta de la IA...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Comando insertado en el terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Configura $fields en Ajustes.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirmar antes de ejecutar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Conversación con la IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer =>
|
||||||
|
'La IA puede equivocarse. Úsala con precaución.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Haz una pregunta adicional...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Insertar en el terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Sin respuesta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Comando sugerido por la IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Contenido seleccionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Usado en el terminal SSH';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada';
|
String get atLeastOneTab => 'Al menos una pestaña debe estar seleccionada';
|
||||||
|
|
||||||
@@ -99,12 +150,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Limpiar todas las estadísticas';
|
String get clearAllStatsTitle => 'Limpiar todas las estadísticas';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return '¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"$serverName\"? Esta acción no se puede deshacer.';
|
return '¿Estás seguro de que quieres limpiar las estadísticas de conexión del servidor \"$serverName\"? Esta acción no se puede deshacer.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Limpiar estadísticas de $serverName';
|
return 'Limpiar estadísticas de $serverName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +583,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Llave privada';
|
String get privateKey => 'Llave privada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'No se encontró la clave privada [$keyId].';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Proceso';
|
String get process => 'Proceso';
|
||||||
|
|
||||||
@@ -716,6 +772,34 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
return 'Se importaron $count servidores desde la configuración SSH';
|
return 'Se importaron $count servidores desde la configuración SSH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'La clave de host SSH de $serverName ha cambiado. Continúa solo si confías en este servidor.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Huella (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Huella (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Tipo de clave de host SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Se recibió una nueva clave de host SSH de $serverName. Revisa la huella antes de confiar.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Huella almacenada: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'¿Te gustaría seleccionar manualmente el archivo de configuración SSH?';
|
'¿Te gustaría seleccionar manualmente el archivo de configuración SSH?';
|
||||||
@@ -925,4 +1009,28 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
'Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Registros';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,57 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Déjà dans le dernier répertoire.';
|
String get alreadyLastDir => 'Déjà dans le dernier répertoire.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Demander à l\'IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Clé API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'En attente de la réponse de l\'IA...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL de base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Commande insérée dans le terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Veuillez configurer $fields dans les paramètres.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirmer avant d\'exécuter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Conversation avec l\'IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer =>
|
||||||
|
'L\'IA peut se tromper. Utilisez-la avec prudence.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Poser une question supplémentaire...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Insérer dans le terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modèle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Aucune réponse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Commande suggérée par l\'IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Contenu sélectionné';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Utilisé dans le terminal SSH';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Au moins un onglet doit être sélectionné';
|
String get atLeastOneTab => 'Au moins un onglet doit être sélectionné';
|
||||||
|
|
||||||
@@ -99,12 +150,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Effacer toutes les statistiques';
|
String get clearAllStatsTitle => 'Effacer toutes les statistiques';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"$serverName\" ? Cette action ne peut pas être annulée.';
|
return 'Êtes-vous sûr de vouloir effacer les statistiques de connexion du serveur \"$serverName\" ? Cette action ne peut pas être annulée.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Effacer les statistiques de $serverName';
|
return 'Effacer les statistiques de $serverName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,6 +585,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Clé privée';
|
String get privateKey => 'Clé privée';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Clé privée [$keyId] introuvable.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Processus';
|
String get process => 'Processus';
|
||||||
|
|
||||||
@@ -718,6 +774,34 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
return '$count serveurs importés depuis la configuration SSH';
|
return '$count serveurs importés depuis la configuration SSH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'La clé d\'hôte SSH de $serverName a changé. Ne continuez que si vous faites confiance à ce serveur.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Empreinte (MD5 Base64) : $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Empreinte (MD5 hex) : $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Type de clé d\'hôte SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Une nouvelle clé d\'hôte SSH a été reçue de $serverName. Vérifiez l\'empreinte avant de faire confiance.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Empreinte enregistrée : $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?';
|
'Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?';
|
||||||
@@ -928,4 +1012,28 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
|
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Journaux';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,56 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Sudah di direktori terakhir.';
|
String get alreadyLastDir => 'Sudah di direktori terakhir.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Tanya AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Kunci API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Menunggu respons AI...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL dasar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Perintah dimasukkan ke terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Harap konfigurasikan $fields di Pengaturan.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Konfirmasi sebelum menjalankan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Percakapan AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'AI bisa saja salah. Gunakan dengan hati-hati.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Ajukan pertanyaan lanjutan...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Masukkan ke terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Tidak ada respons';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Perintah yang disarankan AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Konten yang dipilih';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Digunakan di Terminal SSH';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Setidaknya satu tab harus dipilih';
|
String get atLeastOneTab => 'Setidaknya satu tab harus dipilih';
|
||||||
|
|
||||||
@@ -98,12 +148,12 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Hapus Semua Statistik';
|
String get clearAllStatsTitle => 'Hapus Semua Statistik';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"$serverName\"? Tindakan ini tidak dapat dibatalkan.';
|
return 'Apakah Anda yakin ingin menghapus statistik koneksi untuk server \"$serverName\"? Tindakan ini tidak dapat dibatalkan.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Hapus Statistik $serverName';
|
return 'Hapus Statistik $serverName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +578,11 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Kunci Pribadi';
|
String get privateKey => 'Kunci Pribadi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Kunci privat [$keyId] tidak ditemukan.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Proses';
|
String get process => 'Proses';
|
||||||
|
|
||||||
@@ -709,6 +764,34 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
return 'Berhasil mengimpor $count server dari konfigurasi SSH';
|
return 'Berhasil mengimpor $count server dari konfigurasi SSH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'Kunci host SSH untuk $serverName telah berubah. Lanjutkan hanya jika Anda mempercayai server ini.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Sidik jari (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Sidik jari (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Jenis kunci host SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Kunci host SSH baru diterima dari $serverName. Periksa sidik jarinya sebelum mempercayai.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Sidik jari tersimpan: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Apakah Anda ingin memilih file konfigurasi SSH secara manual?';
|
'Apakah Anda ingin memilih file konfigurasi SSH secara manual?';
|
||||||
@@ -915,4 +998,28 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
'Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Log';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,56 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'すでに最上位のディレクトリです';
|
String get alreadyLastDir => 'すでに最上位のディレクトリです';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'AI に質問';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API キー';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'AI の応答を待機中...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'ベース URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'コマンドをターミナルに挿入しました';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return '設定で $fields を構成してください。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => '実行前に確認';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI 会話';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'AI が誤る可能性があります。注意してご利用ください。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => '追質問をする...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'ターミナルに挿入';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'モデル';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => '応答なし';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI 推奨コマンド';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => '選択した内容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'SSH ターミナルで使用';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります';
|
String get atLeastOneTab => '少なくとも1つのタブを選択する必要があります';
|
||||||
|
|
||||||
@@ -93,12 +143,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'すべての統計をクリア';
|
String get clearAllStatsTitle => 'すべての統計をクリア';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'サーバー\"$serverName\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。';
|
return 'サーバー\"$serverName\"の接続統計を削除してもよろしいですか?この操作は元に戻せません。';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return '$serverNameの統計をクリア';
|
return '$serverNameの統計をクリア';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,6 +560,11 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => '秘密鍵';
|
String get privateKey => '秘密鍵';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return '秘密鍵 [$keyId] が見つかりません。';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'プロセス';
|
String get process => 'プロセス';
|
||||||
|
|
||||||
@@ -687,6 +742,34 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
return 'SSH設定から$count個のサーバーをインポートしました';
|
return 'SSH設定から$count個のサーバーをインポートしました';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return '$serverName の SSH ホスト鍵が変更されました。このサーバーを信頼できる場合のみ続行してください。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'フィンガープリント (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'フィンガープリント (MD5 16進): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'SSH ホストキーの種類';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return '$serverName から新しい SSH ホスト鍵を受信しました。信頼する前にフィンガープリントを確認してください。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return '保存済みフィンガープリント: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect => 'SSH設定ファイルを手動で選択しますか?';
|
String get sshConfigManualSelect => 'SSH設定ファイルを手動で選択しますか?';
|
||||||
|
|
||||||
@@ -884,5 +967,29 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
'サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'ログ';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,56 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Al in de laatst gebruikte map.';
|
String get alreadyLastDir => 'Al in de laatst gebruikte map.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'AI vragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API-sleutel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Wachten op AI-reactie...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Basis-URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Commando in terminal ingevoegd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Configureer $fields in de instellingen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Bevestigen voor uitvoeren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI-gesprek';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'AI kan fouten maken. Gebruik het zorgvuldig.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Stel een vervolgvraag...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'In terminal invoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Geen reactie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Door AI voorgestelde opdracht';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Geselecteerde inhoud';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Gebruikt in de SSH-terminal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab =>
|
String get atLeastOneTab =>
|
||||||
'Er moet minimaal één tabblad worden geselecteerd';
|
'Er moet minimaal één tabblad worden geselecteerd';
|
||||||
@@ -99,12 +149,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Alle statistieken wissen';
|
String get clearAllStatsTitle => 'Alle statistieken wissen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Weet u zeker dat u de verbindingsstatistieken voor server \"$serverName\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
|
return 'Weet u zeker dat u de verbindingsstatistieken voor server \"$serverName\" wilt wissen? Deze actie kan niet ongedaan worden gemaakt.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Statistieken van $serverName wissen';
|
return 'Statistieken van $serverName wissen';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +580,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Privésleutel';
|
String get privateKey => 'Privésleutel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Privésleutel [$keyId] niet gevonden.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Proces';
|
String get process => 'Proces';
|
||||||
|
|
||||||
@@ -713,6 +768,34 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
return '$count servers geïmporteerd uit SSH-configuratie';
|
return '$count servers geïmporteerd uit SSH-configuratie';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'De SSH-hostsleutel voor $serverName is gewijzigd. Ga alleen verder als u deze server vertrouwt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Vingerafdruk (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Vingerafdruk (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Type SSH-hostsleutel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Er is een nieuwe SSH-hostsleutel ontvangen van $serverName. Controleer de vingerafdruk voordat u vertrouwt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Opgeslagen vingerafdruk: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Wilt u het SSH-configuratiebestand handmatig selecteren?';
|
'Wilt u het SSH-configuratiebestand handmatig selecteren?';
|
||||||
@@ -922,4 +1005,28 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
'Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Logboeken';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,56 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Já é o diretório mais alto';
|
String get alreadyLastDir => 'Já é o diretório mais alto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Perguntar à IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Chave de API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Aguardando resposta da IA...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'URL base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Comando inserido no terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Configure $fields nas configurações.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Confirmar antes de executar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Conversa com a IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'A IA pode errar. Use com cautela.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Faça uma pergunta adicional...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Inserir no terminal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Modelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Sem resposta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Comando sugerido pela IA';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Conteúdo selecionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Usado no terminal SSH';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada';
|
String get atLeastOneTab => 'Pelo menos uma aba deve ser selecionada';
|
||||||
|
|
||||||
@@ -99,12 +149,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Limpar todas as estatísticas';
|
String get clearAllStatsTitle => 'Limpar todas as estatísticas';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"$serverName\"? Esta ação não pode ser desfeita.';
|
return 'Tem certeza de que deseja limpar as estatísticas de conexão para o servidor \"$serverName\"? Esta ação não pode ser desfeita.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Limpar estatísticas de $serverName';
|
return 'Limpar estatísticas de $serverName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +578,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Chave privada';
|
String get privateKey => 'Chave privada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Chave privada [$keyId] não encontrada.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Processo';
|
String get process => 'Processo';
|
||||||
|
|
||||||
@@ -709,6 +764,34 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
return 'Importados $count servidores da configuração SSH';
|
return 'Importados $count servidores da configuração SSH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'A chave de host SSH de $serverName foi alterada. Continue apenas se confiar neste servidor.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Impressão digital (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Impressão digital (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Tipo de chave de host SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Uma nova chave de host SSH foi recebida de $serverName. Verifique a impressão digital antes de confiar.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Impressão digital armazenada: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Gostaria de selecionar manualmente o arquivo de configuração SSH?';
|
'Gostaria de selecionar manualmente o arquivo de configuração SSH?';
|
||||||
@@ -917,4 +1000,28 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
'Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Logs';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,57 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Уже в корневом каталоге';
|
String get alreadyLastDir => 'Уже в корневом каталоге';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Спросить ИИ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Ключ API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Ожидание ответа ИИ...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Базовый URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Команда вставлена в терминал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Настройте $fields в настройках.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Подтвердите перед выполнением';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Разговор с ИИ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer =>
|
||||||
|
'ИИ может ошибаться. Используйте с осторожностью.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Задайте дополнительный вопрос...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Вставить в терминал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Нет ответа';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Команда, предложенная ИИ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Выбранное содержимое';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Используется в SSH-терминале';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка';
|
String get atLeastOneTab => 'Должна быть выбрана хотя бы одна вкладка';
|
||||||
|
|
||||||
@@ -99,12 +150,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Очистить всю статистику';
|
String get clearAllStatsTitle => 'Очистить всю статистику';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Вы уверены, что хотите очистить статистику соединений для сервера \"$serverName\"? Это действие не может быть отменено.';
|
return 'Вы уверены, что хотите очистить статистику соединений для сервера \"$serverName\"? Это действие не может быть отменено.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Очистить статистику $serverName';
|
return 'Очистить статистику $serverName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +581,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Приватный ключ';
|
String get privateKey => 'Приватный ключ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Закрытый ключ [$keyId] не найден.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Процесс';
|
String get process => 'Процесс';
|
||||||
|
|
||||||
@@ -713,6 +769,34 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Импортировано $count серверов из SSH-конфигурации';
|
return 'Импортировано $count серверов из SSH-конфигурации';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'SSH-ключ хоста для $serverName изменился. Продолжайте только если доверяете этому серверу.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Отпечаток (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Отпечаток (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Тип ключа хоста SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Получен новый SSH-ключ хоста от $serverName. Проверьте отпечаток перед продолжением.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Сохранённый отпечаток: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Хотели бы вы вручную выбрать файл конфигурации SSH?';
|
'Хотели бы вы вручную выбрать файл конфигурации SSH?';
|
||||||
@@ -920,4 +1004,28 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
'После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Журналы';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,57 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Zaten son dizindesiniz.';
|
String get alreadyLastDir => 'Zaten son dizindesiniz.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Yapay zekaya sor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API anahtarı';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Yapay zekâ yanıtı bekleniyor...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Temel URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Komut terminale eklendi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Lütfen Ayarlar\'da $fields öğesini yapılandırın.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Çalıştırmadan önce onayla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'YZ sohbeti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer =>
|
||||||
|
'Yapay zeka hata yapabilir. Lütfen dikkatli kullanın.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Yeni bir soru sor...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Terminale ekle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Yanıt yok';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'YZ önerilen komut';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Seçilen içerik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'SSH Terminalinde kullanılır';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'En az bir sekme seçilmelidir';
|
String get atLeastOneTab => 'En az bir sekme seçilmelidir';
|
||||||
|
|
||||||
@@ -97,12 +148,12 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Tüm İstatistikleri Temizle';
|
String get clearAllStatsTitle => 'Tüm İstatistikleri Temizle';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return '\"$serverName\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
|
return '\"$serverName\" sunucusu için bağlantı istatistiklerini temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return '$serverName İstatistiklerini Temizle';
|
return '$serverName İstatistiklerini Temizle';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +578,11 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Özel Anahtar';
|
String get privateKey => 'Özel Anahtar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Özel anahtar [$keyId] bulunamadı.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'İşlem';
|
String get process => 'İşlem';
|
||||||
|
|
||||||
@@ -709,6 +765,34 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
return 'SSH yapılandırmasından $count sunucu içe aktarıldı';
|
return 'SSH yapılandırmasından $count sunucu içe aktarıldı';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return '$serverName için SSH ana bilgisayar anahtarı değişti. Yalnızca bu sunucuya güveniyorsanız devam edin.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Parmak izi (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Parmak izi (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'SSH ana bilgisayar anahtarı türü';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return '$serverName üzerinden yeni bir SSH ana bilgisayar anahtarı alındı. Güvenmeden önce parmak izini kontrol edin.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Kaydedilen parmak izi: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?';
|
'SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?';
|
||||||
@@ -915,4 +999,28 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
'Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Günlükler';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,56 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => 'Вже в останньому каталозі.';
|
String get alreadyLastDir => 'Вже в останньому каталозі.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => 'Запитати ШІ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'Ключ API';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => 'Очікування відповіді ШІ...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => 'Базова URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => 'Команду вставлено в термінал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return 'Налаштуйте $fields у налаштуваннях.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => 'Підтвердити перед виконанням';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'Розмова з ШІ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'ШІ може помилятися. Користуйтеся обережно.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => 'Поставте додаткове запитання...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => 'Вставити в термінал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => 'Модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => 'Відповідь відсутня';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'Команда, запропонована ШІ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => 'Вибраний вміст';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => 'Використовується в SSH-терміналі';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку';
|
String get atLeastOneTab => 'Потрібно вибрати принаймні одну вкладку';
|
||||||
|
|
||||||
@@ -99,12 +149,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => 'Очистити всю статистику';
|
String get clearAllStatsTitle => 'Очистити всю статистику';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return 'Ви впевнені, що хочете очистити статистику з\'єднань для сервера \"$serverName\"? Цю дію не можна скасувати.';
|
return 'Ви впевнені, що хочете очистити статистику з\'єднань для сервера \"$serverName\"? Цю дію не можна скасувати.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return 'Очистити статистику $serverName';
|
return 'Очистити статистику $serverName';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +582,11 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => 'Приватний ключ';
|
String get privateKey => 'Приватний ключ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return 'Приватний ключ [$keyId] не знайдено.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => 'Процес';
|
String get process => 'Процес';
|
||||||
|
|
||||||
@@ -714,6 +769,34 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
return 'Імпортовано $count серверів з SSH-конфігурації';
|
return 'Імпортовано $count серверів з SSH-конфігурації';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return 'SSH-ключ хоста для $serverName змінено. Продовжуйте лише якщо довіряєте цьому серверу.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return 'Відбиток (MD5 Base64): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return 'Відбиток (MD5 hex): $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'Тип ключа хоста SSH';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return 'Отримано новий SSH-ключ хоста від $serverName. Перевірте відбиток перед тим, як довіряти.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return 'Збережений відбиток: $fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect =>
|
String get sshConfigManualSelect =>
|
||||||
'Чи хочете ви вручну вибрати файл конфігурації SSH?';
|
'Чи хочете ви вручну вибрати файл конфігурації SSH?';
|
||||||
@@ -921,4 +1004,28 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
'Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => 'Setting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => 'Quit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => 'Navigate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => 'Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub Repository';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => 'Help';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => 'Журнали';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,56 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => '已是顶级目录';
|
String get alreadyLastDir => '已是顶级目录';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => '问 AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API 密钥';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => '等待 AI 响应...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => '基础 URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => '命令已插入终端';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return '请前往设置配置 $fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => '执行前确认';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI 对话';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'AI 可能会犯错,请谨慎使用。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => '继续提问...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => '插入终端';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => '模型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => '无回复内容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI 推荐命令';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => '选中的内容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => '用于 SSH 终端';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => '至少需要选择一个标签';
|
String get atLeastOneTab => '至少需要选择一个标签';
|
||||||
|
|
||||||
@@ -91,12 +141,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get clearAllStatsTitle => '清空所有统计';
|
String get clearAllStatsTitle => '清空所有统计';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return '确定要清空服务器 \"$serverName\" 的连接统计数据吗?此操作无法撤销。';
|
return '确定要清空服务器 \"$serverName\" 的连接统计数据吗?此操作无法撤销。';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return '清空 $serverName 统计';
|
return '清空 $serverName 统计';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +554,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => '私钥';
|
String get privateKey => '私钥';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return '未找到私钥 [$keyId]。';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => '进程';
|
String get process => '进程';
|
||||||
|
|
||||||
@@ -677,6 +732,34 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '从 SSH 配置导入了 $count 个服务器';
|
return '从 SSH 配置导入了 $count 个服务器';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return '服务器 $serverName 的 SSH 主机密钥已更改,仅在信任该服务器时继续。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return '指纹(MD5 Base64):$fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return '指纹(MD5 十六进制):$fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'SSH 主机密钥类型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return '收到来自 $serverName 的新 SSH 主机密钥,在信任前请检查指纹。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return '已存储的指纹:$fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect => '是否要手动选择 SSH 配置文件?';
|
String get sshConfigManualSelect => '是否要手动选择 SSH 配置文件?';
|
||||||
|
|
||||||
@@ -870,6 +953,30 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
'在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuSettings => '设置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuQuit => '退出';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuNavigate => '导航';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuInfo => '信息';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuGitHubRepository => 'GitHub 仓库';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuWiki => 'Wiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get menuHelp => '帮助';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => '日志';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||||
@@ -894,6 +1001,56 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get alreadyLastDir => '已是頂層目錄';
|
String get alreadyLastDir => '已是頂層目錄';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAi => '詢問 AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiApiKey => 'API 金鑰';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiAwaitingResponse => '等待 AI 回應...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiBaseUrl => '基礎 URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiCommandInserted => '指令已插入終端機';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askAiConfigMissing(Object fields) {
|
||||||
|
return '請前往設定配置 $fields';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConfirmExecute => '執行前確認';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiConversation => 'AI 對話';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiDisclaimer => 'AI 可能會犯錯,請謹慎使用。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiFollowUpHint => '繼續提問...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiInsertTerminal => '插入終端機';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiModel => '模型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiNoResponse => '無回覆內容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiRecommendedCommand => 'AI 推薦指令';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiSelectedContent => '選取的內容';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askAiUsageHint => '於 SSH 終端機中使用';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get atLeastOneTab => '至少需要選擇一個標籤';
|
String get atLeastOneTab => '至少需要選擇一個標籤';
|
||||||
|
|
||||||
@@ -959,12 +1116,12 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
String get clearAllStatsTitle => '清空所有統計';
|
String get clearAllStatsTitle => '清空所有統計';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsContent(String serverName) {
|
String clearServerStatsContent(Object serverName) {
|
||||||
return '確定要清空伺服器 \"$serverName\" 的連線統計資料嗎?此操作無法撤銷。';
|
return '確定要清空伺服器 \"$serverName\" 的連線統計資料嗎?此操作無法撤銷。';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String clearServerStatsTitle(String serverName) {
|
String clearServerStatsTitle(Object serverName) {
|
||||||
return '清空 $serverName 統計';
|
return '清空 $serverName 統計';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,6 +1529,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get privateKey => '私鑰';
|
String get privateKey => '私鑰';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String privateKeyNotFoundFmt(Object keyId) {
|
||||||
|
return '未找到私鑰 [$keyId]。';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get process => '處理程序';
|
String get process => '處理程序';
|
||||||
|
|
||||||
@@ -1545,6 +1707,34 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
return '已從SSH設定匯入$count個伺服器';
|
return '已從SSH設定匯入$count個伺服器';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyChangedDesc(Object serverName) {
|
||||||
|
return '伺服器 $serverName 的 SSH 主機金鑰已變更,僅在信任該伺服器時繼續。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Base64(Object fingerprint) {
|
||||||
|
return '指紋(MD5 Base64):$fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyFingerprintMd5Hex(Object fingerprint) {
|
||||||
|
return '指紋(MD5 十六進位):$fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sshHostKeyType => 'SSH 主機金鑰類型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyNewDesc(Object serverName) {
|
||||||
|
return '收到來自 $serverName 的新 SSH 主機金鑰,信任前請先檢查指紋。';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sshHostKeyStoredFingerprint(Object fingerprint) {
|
||||||
|
return '已儲存的指紋:$fingerprint';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sshConfigManualSelect => '是否要手動選擇 SSH 設定檔案?';
|
String get sshConfigManualSelect => '是否要手動選擇 SSH 設定檔案?';
|
||||||
|
|
||||||
@@ -1738,4 +1928,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get writeScriptTip =>
|
String get writeScriptTip =>
|
||||||
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
'連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logs => '日誌';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
alterUrl: fields[7] as String?,
|
alterUrl: fields[7] as String?,
|
||||||
autoConnect: fields[8] == null ? true : fields[8] as bool,
|
autoConnect: fields[8] == null ? true : fields[8] as bool,
|
||||||
jumpId: fields[9] as String?,
|
jumpId: fields[9] as String?,
|
||||||
|
jumpChainIds: (fields[16] as List?)?.cast<String>(),
|
||||||
custom: fields[10] as ServerCustom?,
|
custom: fields[10] as ServerCustom?,
|
||||||
wolCfg: fields[11] as WakeOnLanCfg?,
|
wolCfg: fields[11] as WakeOnLanCfg?,
|
||||||
envs: (fields[12] as Map?)?.cast<String, String>(),
|
envs: (fields[12] as Map?)?.cast<String, String>(),
|
||||||
@@ -119,7 +120,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, Spi obj) {
|
void write(BinaryWriter writer, Spi obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(16)
|
..writeByte(17)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.name)
|
..write(obj.name)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -151,7 +152,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
..writeByte(14)
|
..writeByte(14)
|
||||||
..write(obj.customSystemType)
|
..write(obj.customSystemType)
|
||||||
..writeByte(15)
|
..writeByte(15)
|
||||||
..write(obj.disabledCmdTypes);
|
..write(obj.disabledCmdTypes)
|
||||||
|
..writeByte(16)
|
||||||
|
..write(obj.jumpChainIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ types:
|
|||||||
index: 4
|
index: 4
|
||||||
Spi:
|
Spi:
|
||||||
typeId: 3
|
typeId: 3
|
||||||
nextIndex: 16
|
nextIndex: 17
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
index: 0
|
index: 0
|
||||||
@@ -61,6 +61,8 @@ types:
|
|||||||
index: 14
|
index: 14
|
||||||
disabledCmdTypes:
|
disabledCmdTypes:
|
||||||
index: 15
|
index: 15
|
||||||
|
jumpChainIds:
|
||||||
|
index: 16
|
||||||
VirtKey:
|
VirtKey:
|
||||||
typeId: 4
|
typeId: 4
|
||||||
nextIndex: 45
|
nextIndex: 45
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "de",
|
"@@locale": "de",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\n",
|
"aboutThanks": "Vielen Dank an die folgenden Personen, die daran teilgenommen haben.\n",
|
||||||
"acceptBeta": "Akzeptieren Sie Testversion-Updates",
|
"acceptBeta": "Akzeptieren Sie Testversion-Updates",
|
||||||
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Zur Aufgabenliste hinzugefügt",
|
"added2List": "Zur Aufgabenliste hinzugefügt",
|
||||||
"addr": "Adresse",
|
"addr": "Adresse",
|
||||||
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
|
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
|
||||||
|
"askAi": "KI fragen",
|
||||||
|
"askAiApiKey": "API-Schlüssel",
|
||||||
|
"askAiAwaitingResponse": "Warte auf KI-Antwort...",
|
||||||
|
"askAiBaseUrl": "Basis-URL",
|
||||||
|
"askAiCommandInserted": "Befehl ins Terminal eingefügt",
|
||||||
|
"askAiConfigMissing": "Bitte konfigurieren Sie {fields} in den Einstellungen.",
|
||||||
|
"askAiConfirmExecute": "Vor Ausführung bestätigen",
|
||||||
|
"askAiConversation": "KI-Unterhaltung",
|
||||||
|
"askAiDisclaimer": "KI kann Fehler machen. Bitte vorsichtig verwenden.",
|
||||||
|
"askAiFollowUpHint": "Weitere Frage stellen...",
|
||||||
|
"askAiInsertTerminal": "In Terminal einfügen",
|
||||||
|
"askAiModel": "Modell",
|
||||||
|
"askAiNoResponse": "Keine Antwort",
|
||||||
|
"askAiRecommendedCommand": "KI-empfohlener Befehl",
|
||||||
|
"askAiSelectedContent": "Ausgewählter Inhalt",
|
||||||
|
"askAiUsageHint": "Verwendet im SSH-Terminal",
|
||||||
"atLeastOneTab": "Mindestens ein Tab muss ausgewählt sein",
|
"atLeastOneTab": "Mindestens ein Tab muss ausgewählt sein",
|
||||||
"authFailTip": "Authentifizierung fehlgeschlagen, bitte überprüfen Sie, ob das Passwort/Schlüssel/Host/Benutzer usw. falsch sind.",
|
"authFailTip": "Authentifizierung fehlgeschlagen, bitte überprüfen Sie, ob das Passwort/Schlüssel/Host/Benutzer usw. falsch sind.",
|
||||||
"autoBackupConflict": "Es kann nur eine automatische Sicherung gleichzeitig aktiviert werden.",
|
"autoBackupConflict": "Es kann nur eine automatische Sicherung gleichzeitig aktiviert werden.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferDiskAmount": "Festplattenkapazität vorrangig anzeigen",
|
"preferDiskAmount": "Festplattenkapazität vorrangig anzeigen",
|
||||||
"privateKey": "Private Key",
|
"privateKey": "Private Key",
|
||||||
|
"privateKeyNotFoundFmt": "Privater Schlüssel [{keyId}] wurde nicht gefunden.",
|
||||||
"process": "Prozess",
|
"process": "Prozess",
|
||||||
"prune": "Beschneiden",
|
"prune": "Beschneiden",
|
||||||
"pushToken": "Push Token",
|
"pushToken": "Push Token",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?",
|
"sshConfigImportPermission": "Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?",
|
||||||
"sshConfigImportTip": "Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern",
|
"sshConfigImportTip": "Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern",
|
||||||
"sshConfigImported": "{count} Server aus SSH-Konfiguration importiert",
|
"sshConfigImported": "{count} Server aus SSH-Konfiguration importiert",
|
||||||
|
"sshHostKeyChangedDesc": "Der SSH-Hostschlüssel für {serverName} hat sich geändert. Fahren Sie nur fort, wenn Sie diesem Server vertrauen.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Fingerabdruck (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Fingerabdruck (MD5 Hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "SSH-Hostschlüsseltyp",
|
||||||
|
"sshHostKeyNewDesc": "Ein neuer SSH-Hostschlüssel wurde von {serverName} empfangen. Prüfen Sie den Fingerabdruck, bevor Sie vertrauen.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Gespeicherter Fingerabdruck: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?",
|
"sshConfigManualSelect": "Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?",
|
||||||
"sshConfigNoServers": "Keine Server in der SSH-Konfiguration gefunden",
|
"sshConfigNoServers": "Keine Server in der SSH-Konfiguration gefunden",
|
||||||
"sshConfigPermissionDenied": "Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.",
|
"sshConfigPermissionDenied": "Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
"wolTip": "Nach der Konfiguration von WOL (Wake-on-LAN) wird jedes Mal, wenn der Server verbunden wird, eine WOL-Anfrage gesendet.",
|
||||||
"write": "Schreiben",
|
"write": "Schreiben",
|
||||||
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
|
||||||
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen."
|
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
|
||||||
}
|
"logs": "Protokolle"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Thanks to the following people who participated in.",
|
"aboutThanks": "Thanks to the following people who participated in.",
|
||||||
"acceptBeta": "Accept beta version updates",
|
"acceptBeta": "Accept beta version updates",
|
||||||
"addSystemPrivateKeyTip": "Currently private keys don't exist, do you want to add the one that comes with the system (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Currently private keys don't exist, do you want to add the one that comes with the system (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Added to task list",
|
"added2List": "Added to task list",
|
||||||
"addr": "Address",
|
"addr": "Address",
|
||||||
"alreadyLastDir": "Already in last directory.",
|
"alreadyLastDir": "Already in last directory.",
|
||||||
|
"askAi": "Ask AI",
|
||||||
|
"askAiApiKey": "API Key",
|
||||||
|
"askAiAwaitingResponse": "Waiting for AI response...",
|
||||||
|
"askAiBaseUrl": "Base URL",
|
||||||
|
"askAiCommandInserted": "Command inserted into terminal",
|
||||||
|
"askAiConfigMissing": "Please configure {fields} in Settings.",
|
||||||
|
"askAiConfirmExecute": "Confirm before executing",
|
||||||
|
"askAiConversation": "AI conversation",
|
||||||
|
"askAiDisclaimer": "AI may be incorrect. Review carefully before applying.",
|
||||||
|
"askAiFollowUpHint": "Ask a follow-up...",
|
||||||
|
"askAiInsertTerminal": "Insert into terminal",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiNoResponse": "No response",
|
||||||
|
"askAiRecommendedCommand": "AI suggested command",
|
||||||
|
"askAiSelectedContent": "Selected content",
|
||||||
|
"askAiUsageHint": "Used in SSH Terminal",
|
||||||
"atLeastOneTab": "At least one tab must be selected",
|
"atLeastOneTab": "At least one tab must be selected",
|
||||||
"authFailTip": "Authentication failed, please check whether credentials are correct",
|
"authFailTip": "Authentication failed, please check whether credentials are correct",
|
||||||
"autoBackupConflict": "Only one automatic backup can be turned on at the same time.",
|
"autoBackupConflict": "Only one automatic backup can be turned on at the same time.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferDiskAmount": "Prioritize displaying disk capacity",
|
"preferDiskAmount": "Prioritize displaying disk capacity",
|
||||||
"privateKey": "Private Key",
|
"privateKey": "Private Key",
|
||||||
|
"privateKeyNotFoundFmt": "Private key [{keyId}] not found.",
|
||||||
"process": "Process",
|
"process": "Process",
|
||||||
"prune": "Prune",
|
"prune": "Prune",
|
||||||
"pushToken": "Push token",
|
"pushToken": "Push token",
|
||||||
@@ -223,6 +226,15 @@
|
|||||||
"sshConfigImportPermission": "Would you like to give permission to read ~/.ssh/config and automatically import server settings?",
|
"sshConfigImportPermission": "Would you like to give permission to read ~/.ssh/config and automatically import server settings?",
|
||||||
"sshConfigImportTip": "Prompt to read ~/.ssh/config on first server creation",
|
"sshConfigImportTip": "Prompt to read ~/.ssh/config on first server creation",
|
||||||
"sshConfigImported": "Imported {count} servers from SSH config",
|
"sshConfigImported": "Imported {count} servers from SSH config",
|
||||||
|
"sshHostKeyChangedDesc": "The SSH host key changed for {serverName}. Only continue if you trust this server.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Fingerprint (MD5 base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Fingerprint (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "SSH host key type",
|
||||||
|
"@sshHostKeyType": {
|
||||||
|
"description": "Label for the SSH host key type displayed in the host key verification dialog."
|
||||||
|
},
|
||||||
|
"sshHostKeyNewDesc": "A new SSH host key was received from {serverName}. Review the fingerprint before trusting.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Stored fingerprint: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Would you like to select the SSH config file manually?",
|
"sshConfigManualSelect": "Would you like to select the SSH config file manually?",
|
||||||
"sshConfigNoServers": "No servers found in SSH config",
|
"sshConfigNoServers": "No servers found in SSH config",
|
||||||
"sshConfigPermissionDenied": "Cannot access SSH config file due to macOS permissions.",
|
"sshConfigPermissionDenied": "Cannot access SSH config file due to macOS permissions.",
|
||||||
@@ -284,5 +296,13 @@
|
|||||||
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
"wolTip": "After configuring WOL (Wake-on-LAN), a WOL request is sent each time the server is connected.",
|
||||||
"write": "Write",
|
"write": "Write",
|
||||||
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
||||||
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content."
|
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.",
|
||||||
}
|
"menuSettings": "Setting",
|
||||||
|
"menuQuit": "Quit",
|
||||||
|
"menuNavigate": "Navigate",
|
||||||
|
"menuInfo": "Info",
|
||||||
|
"menuGitHubRepository": "GitHub Repository",
|
||||||
|
"menuWiki": "Wiki",
|
||||||
|
"menuHelp": "Help",
|
||||||
|
"logs": "Logs"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "es",
|
"@@locale": "es",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Gracias a los siguientes participantes.",
|
"aboutThanks": "Gracias a los siguientes participantes.",
|
||||||
"acceptBeta": "Aceptar actualizaciones de la versión de prueba",
|
"acceptBeta": "Aceptar actualizaciones de la versión de prueba",
|
||||||
"addSystemPrivateKeyTip": "Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Añadido a la lista de tareas",
|
"added2List": "Añadido a la lista de tareas",
|
||||||
"addr": "Dirección",
|
"addr": "Dirección",
|
||||||
"alreadyLastDir": "Ya estás en el directorio superior",
|
"alreadyLastDir": "Ya estás en el directorio superior",
|
||||||
|
"askAi": "Preguntar a la IA",
|
||||||
|
"askAiApiKey": "Clave API",
|
||||||
|
"askAiAwaitingResponse": "Esperando la respuesta de la IA...",
|
||||||
|
"askAiBaseUrl": "URL base",
|
||||||
|
"askAiCommandInserted": "Comando insertado en el terminal",
|
||||||
|
"askAiConfigMissing": "Configura {fields} en Ajustes.",
|
||||||
|
"askAiConfirmExecute": "Confirmar antes de ejecutar",
|
||||||
|
"askAiConversation": "Conversación con la IA",
|
||||||
|
"askAiDisclaimer": "La IA puede equivocarse. Úsala con precaución.",
|
||||||
|
"askAiFollowUpHint": "Haz una pregunta adicional...",
|
||||||
|
"askAiInsertTerminal": "Insertar en el terminal",
|
||||||
|
"askAiModel": "Modelo",
|
||||||
|
"askAiNoResponse": "Sin respuesta",
|
||||||
|
"askAiRecommendedCommand": "Comando sugerido por la IA",
|
||||||
|
"askAiSelectedContent": "Contenido seleccionado",
|
||||||
|
"askAiUsageHint": "Usado en el terminal SSH",
|
||||||
"atLeastOneTab": "Al menos una pestaña debe estar seleccionada",
|
"atLeastOneTab": "Al menos una pestaña debe estar seleccionada",
|
||||||
"authFailTip": "La autenticación ha fallado, por favor verifica si la contraseña/llave/host/usuario, etc., son incorrectos.",
|
"authFailTip": "La autenticación ha fallado, por favor verifica si la contraseña/llave/host/usuario, etc., son incorrectos.",
|
||||||
"autoBackupConflict": "Solo se puede activar una copia de seguridad automática a la vez",
|
"autoBackupConflict": "Solo se puede activar una copia de seguridad automática a la vez",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Puerto",
|
"port": "Puerto",
|
||||||
"preferDiskAmount": "Priorizar la visualización de la capacidad del disco",
|
"preferDiskAmount": "Priorizar la visualización de la capacidad del disco",
|
||||||
"privateKey": "Llave privada",
|
"privateKey": "Llave privada",
|
||||||
|
"privateKeyNotFoundFmt": "No se encontró la clave privada [{keyId}].",
|
||||||
"process": "Proceso",
|
"process": "Proceso",
|
||||||
"prune": "Podar",
|
"prune": "Podar",
|
||||||
"pushToken": "Token de notificaciones",
|
"pushToken": "Token de notificaciones",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?",
|
"sshConfigImportPermission": "¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?",
|
||||||
"sshConfigImportTip": "Sugerencia para leer ~/.ssh/config al crear el primer servidor",
|
"sshConfigImportTip": "Sugerencia para leer ~/.ssh/config al crear el primer servidor",
|
||||||
"sshConfigImported": "Se importaron {count} servidores desde la configuración SSH",
|
"sshConfigImported": "Se importaron {count} servidores desde la configuración SSH",
|
||||||
|
"sshHostKeyChangedDesc": "La clave de host SSH de {serverName} ha cambiado. Continúa solo si confías en este servidor.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Huella (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Huella (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "Tipo de clave de host SSH",
|
||||||
|
"sshHostKeyNewDesc": "Se recibió una nueva clave de host SSH de {serverName}. Revisa la huella antes de confiar.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Huella almacenada: {fingerprint}",
|
||||||
"sshConfigManualSelect": "¿Te gustaría seleccionar manualmente el archivo de configuración SSH?",
|
"sshConfigManualSelect": "¿Te gustaría seleccionar manualmente el archivo de configuración SSH?",
|
||||||
"sshConfigNoServers": "No se encontraron servidores en la configuración SSH",
|
"sshConfigNoServers": "No se encontraron servidores en la configuración SSH",
|
||||||
"sshConfigPermissionDenied": "No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.",
|
"sshConfigPermissionDenied": "No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
"wolTip": "Después de configurar WOL (Wake-on-LAN), se envía una solicitud de WOL cada vez que se conecta el servidor.",
|
||||||
"write": "Escribir",
|
"write": "Escribir",
|
||||||
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
|
||||||
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script."
|
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
|
||||||
}
|
"logs": "Registros"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "fr",
|
"@@locale": "fr",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Merci aux personnes suivantes qui ont participé.",
|
"aboutThanks": "Merci aux personnes suivantes qui ont participé.",
|
||||||
"acceptBeta": "Accepter les mises à jour de la version de test",
|
"acceptBeta": "Accepter les mises à jour de la version de test",
|
||||||
"addSystemPrivateKeyTip": "Actuellement, vous n'avez aucune clé privée. Souhaitez-vous ajouter celle qui vient avec le système (~/.ssh/id_rsa) ?",
|
"addSystemPrivateKeyTip": "Actuellement, vous n'avez aucune clé privée. Souhaitez-vous ajouter celle qui vient avec le système (~/.ssh/id_rsa) ?",
|
||||||
"added2List": "Ajouté à la liste des tâches",
|
"added2List": "Ajouté à la liste des tâches",
|
||||||
"addr": "Adresse",
|
"addr": "Adresse",
|
||||||
"alreadyLastDir": "Déjà dans le dernier répertoire.",
|
"alreadyLastDir": "Déjà dans le dernier répertoire.",
|
||||||
|
"askAi": "Demander à l'IA",
|
||||||
|
"askAiApiKey": "Clé API",
|
||||||
|
"askAiAwaitingResponse": "En attente de la réponse de l'IA...",
|
||||||
|
"askAiBaseUrl": "URL de base",
|
||||||
|
"askAiCommandInserted": "Commande insérée dans le terminal",
|
||||||
|
"askAiConfigMissing": "Veuillez configurer {fields} dans les paramètres.",
|
||||||
|
"askAiConfirmExecute": "Confirmer avant d'exécuter",
|
||||||
|
"askAiConversation": "Conversation avec l'IA",
|
||||||
|
"askAiDisclaimer": "L'IA peut se tromper. Utilisez-la avec prudence.",
|
||||||
|
"askAiFollowUpHint": "Poser une question supplémentaire...",
|
||||||
|
"askAiInsertTerminal": "Insérer dans le terminal",
|
||||||
|
"askAiModel": "Modèle",
|
||||||
|
"askAiNoResponse": "Aucune réponse",
|
||||||
|
"askAiRecommendedCommand": "Commande suggérée par l'IA",
|
||||||
|
"askAiSelectedContent": "Contenu sélectionné",
|
||||||
|
"askAiUsageHint": "Utilisé dans le terminal SSH",
|
||||||
"atLeastOneTab": "Au moins un onglet doit être sélectionné",
|
"atLeastOneTab": "Au moins un onglet doit être sélectionné",
|
||||||
"authFailTip": "Échec de l'authentification. Veuillez vérifier si le mot de passe/clé/hôte/utilisateur, etc., est incorrect.",
|
"authFailTip": "Échec de l'authentification. Veuillez vérifier si le mot de passe/clé/hôte/utilisateur, etc., est incorrect.",
|
||||||
"autoBackupConflict": "Un seul sauvegarde automatique peut être activé en même temps.",
|
"autoBackupConflict": "Un seul sauvegarde automatique peut être activé en même temps.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferDiskAmount": "Prioriser l’affichage de la capacité du disque",
|
"preferDiskAmount": "Prioriser l’affichage de la capacité du disque",
|
||||||
"privateKey": "Clé privée",
|
"privateKey": "Clé privée",
|
||||||
|
"privateKeyNotFoundFmt": "Clé privée [{keyId}] introuvable.",
|
||||||
"process": "Processus",
|
"process": "Processus",
|
||||||
"prune": "Élaguer",
|
"prune": "Élaguer",
|
||||||
"pushToken": "Jeton d'identification",
|
"pushToken": "Jeton d'identification",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Souhaitez-vous donner la permission de lire ~/.ssh/config et d'importer automatiquement les paramètres du serveur ?",
|
"sshConfigImportPermission": "Souhaitez-vous donner la permission de lire ~/.ssh/config et d'importer automatiquement les paramètres du serveur ?",
|
||||||
"sshConfigImportTip": "Proposer de lire ~/.ssh/config lors de la première création de serveur",
|
"sshConfigImportTip": "Proposer de lire ~/.ssh/config lors de la première création de serveur",
|
||||||
"sshConfigImported": "{count} serveurs importés depuis la configuration SSH",
|
"sshConfigImported": "{count} serveurs importés depuis la configuration SSH",
|
||||||
|
"sshHostKeyChangedDesc": "La clé d'hôte SSH de {serverName} a changé. Ne continuez que si vous faites confiance à ce serveur.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Empreinte (MD5 Base64) : {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Empreinte (MD5 hex) : {fingerprint}",
|
||||||
|
"sshHostKeyType": "Type de clé d'hôte SSH",
|
||||||
|
"sshHostKeyNewDesc": "Une nouvelle clé d'hôte SSH a été reçue de {serverName}. Vérifiez l'empreinte avant de faire confiance.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Empreinte enregistrée : {fingerprint}",
|
||||||
"sshConfigManualSelect": "Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?",
|
"sshConfigManualSelect": "Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?",
|
||||||
"sshConfigNoServers": "Aucun serveur trouvé dans la configuration SSH",
|
"sshConfigNoServers": "Aucun serveur trouvé dans la configuration SSH",
|
||||||
"sshConfigPermissionDenied": "Impossible d'accéder au fichier de configuration SSH en raison des permissions macOS.",
|
"sshConfigPermissionDenied": "Impossible d'accéder au fichier de configuration SSH en raison des permissions macOS.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
|
||||||
"write": "Écrire",
|
"write": "Écrire",
|
||||||
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
|
||||||
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script."
|
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.",
|
||||||
}
|
"logs": "Journaux"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "id",
|
"@@locale": "id",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Terima kasih kepada orang -orang berikut yang berpartisipasi.",
|
"aboutThanks": "Terima kasih kepada orang -orang berikut yang berpartisipasi.",
|
||||||
"acceptBeta": "Terima pembaruan versi uji coba",
|
"acceptBeta": "Terima pembaruan versi uji coba",
|
||||||
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Ditambahkan ke Daftar Tugas",
|
"added2List": "Ditambahkan ke Daftar Tugas",
|
||||||
"addr": "Alamat",
|
"addr": "Alamat",
|
||||||
"alreadyLastDir": "Sudah di direktori terakhir.",
|
"alreadyLastDir": "Sudah di direktori terakhir.",
|
||||||
|
"askAi": "Tanya AI",
|
||||||
|
"askAiApiKey": "Kunci API",
|
||||||
|
"askAiAwaitingResponse": "Menunggu respons AI...",
|
||||||
|
"askAiBaseUrl": "URL dasar",
|
||||||
|
"askAiCommandInserted": "Perintah dimasukkan ke terminal",
|
||||||
|
"askAiConfigMissing": "Harap konfigurasikan {fields} di Pengaturan.",
|
||||||
|
"askAiConfirmExecute": "Konfirmasi sebelum menjalankan",
|
||||||
|
"askAiConversation": "Percakapan AI",
|
||||||
|
"askAiDisclaimer": "AI bisa saja salah. Gunakan dengan hati-hati.",
|
||||||
|
"askAiFollowUpHint": "Ajukan pertanyaan lanjutan...",
|
||||||
|
"askAiInsertTerminal": "Masukkan ke terminal",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiNoResponse": "Tidak ada respons",
|
||||||
|
"askAiRecommendedCommand": "Perintah yang disarankan AI",
|
||||||
|
"askAiSelectedContent": "Konten yang dipilih",
|
||||||
|
"askAiUsageHint": "Digunakan di Terminal SSH",
|
||||||
"atLeastOneTab": "Setidaknya satu tab harus dipilih",
|
"atLeastOneTab": "Setidaknya satu tab harus dipilih",
|
||||||
"authFailTip": "Otentikasi gagal, silakan periksa apakah kata sandi/kunci/host/pengguna, dll, salah.",
|
"authFailTip": "Otentikasi gagal, silakan periksa apakah kata sandi/kunci/host/pengguna, dll, salah.",
|
||||||
"autoBackupConflict": "Hanya satu pencadangan otomatis yang dapat diaktifkan pada saat yang bersamaan.",
|
"autoBackupConflict": "Hanya satu pencadangan otomatis yang dapat diaktifkan pada saat yang bersamaan.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferDiskAmount": "Prioritaskan tampilan kapasitas disk",
|
"preferDiskAmount": "Prioritaskan tampilan kapasitas disk",
|
||||||
"privateKey": "Kunci Pribadi",
|
"privateKey": "Kunci Pribadi",
|
||||||
|
"privateKeyNotFoundFmt": "Kunci privat [{keyId}] tidak ditemukan.",
|
||||||
"process": "Proses",
|
"process": "Proses",
|
||||||
"prune": "Pangkas",
|
"prune": "Pangkas",
|
||||||
"pushToken": "Dorong token",
|
"pushToken": "Dorong token",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?",
|
"sshConfigImportPermission": "Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?",
|
||||||
"sshConfigImportTip": "Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama",
|
"sshConfigImportTip": "Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama",
|
||||||
"sshConfigImported": "Berhasil mengimpor {count} server dari konfigurasi SSH",
|
"sshConfigImported": "Berhasil mengimpor {count} server dari konfigurasi SSH",
|
||||||
|
"sshHostKeyChangedDesc": "Kunci host SSH untuk {serverName} telah berubah. Lanjutkan hanya jika Anda mempercayai server ini.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Sidik jari (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Sidik jari (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "Jenis kunci host SSH",
|
||||||
|
"sshHostKeyNewDesc": "Kunci host SSH baru diterima dari {serverName}. Periksa sidik jarinya sebelum mempercayai.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Sidik jari tersimpan: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Apakah Anda ingin memilih file konfigurasi SSH secara manual?",
|
"sshConfigManualSelect": "Apakah Anda ingin memilih file konfigurasi SSH secara manual?",
|
||||||
"sshConfigNoServers": "Tidak ada server yang ditemukan dalam konfigurasi SSH",
|
"sshConfigNoServers": "Tidak ada server yang ditemukan dalam konfigurasi SSH",
|
||||||
"sshConfigPermissionDenied": "Tidak dapat mengakses file konfigurasi SSH karena izin macOS.",
|
"sshConfigPermissionDenied": "Tidak dapat mengakses file konfigurasi SSH karena izin macOS.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
"wolTip": "Setelah mengonfigurasi WOL (Wake-on-LAN), permintaan WOL dikirim setiap kali server terhubung.",
|
||||||
"write": "Tulis",
|
"write": "Tulis",
|
||||||
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
|
||||||
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut."
|
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
|
||||||
}
|
"logs": "Log"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "ja",
|
"@@locale": "ja",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "以下の参加者に感謝します。",
|
"aboutThanks": "以下の参加者に感謝します。",
|
||||||
"acceptBeta": "テストバージョンの更新を受け入れる",
|
"acceptBeta": "テストバージョンの更新を受け入れる",
|
||||||
"addSystemPrivateKeyTip": "現在秘密鍵がありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか?",
|
"addSystemPrivateKeyTip": "現在秘密鍵がありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか?",
|
||||||
"added2List": "タスクリストに追加されました",
|
"added2List": "タスクリストに追加されました",
|
||||||
"addr": "アドレス",
|
"addr": "アドレス",
|
||||||
"alreadyLastDir": "すでに最上位のディレクトリです",
|
"alreadyLastDir": "すでに最上位のディレクトリです",
|
||||||
|
"askAi": "AI に質問",
|
||||||
|
"askAiApiKey": "API キー",
|
||||||
|
"askAiAwaitingResponse": "AI の応答を待機中...",
|
||||||
|
"askAiBaseUrl": "ベース URL",
|
||||||
|
"askAiCommandInserted": "コマンドをターミナルに挿入しました",
|
||||||
|
"askAiConfigMissing": "設定で {fields} を構成してください。",
|
||||||
|
"askAiConfirmExecute": "実行前に確認",
|
||||||
|
"askAiConversation": "AI 会話",
|
||||||
|
"askAiDisclaimer": "AI が誤る可能性があります。注意してご利用ください。",
|
||||||
|
"askAiFollowUpHint": "追質問をする...",
|
||||||
|
"askAiInsertTerminal": "ターミナルに挿入",
|
||||||
|
"askAiModel": "モデル",
|
||||||
|
"askAiNoResponse": "応答なし",
|
||||||
|
"askAiRecommendedCommand": "AI 推奨コマンド",
|
||||||
|
"askAiSelectedContent": "選択した内容",
|
||||||
|
"askAiUsageHint": "SSH ターミナルで使用",
|
||||||
"atLeastOneTab": "少なくとも1つのタブを選択する必要があります",
|
"atLeastOneTab": "少なくとも1つのタブを選択する必要があります",
|
||||||
"authFailTip": "認証に失敗しました。パスワード/鍵/ホスト/ユーザーなどが間違っていないか確認してください。",
|
"authFailTip": "認証に失敗しました。パスワード/鍵/ホスト/ユーザーなどが間違っていないか確認してください。",
|
||||||
"autoBackupConflict": "自動バックアップは一度に一つしか開始できません",
|
"autoBackupConflict": "自動バックアップは一度に一つしか開始できません",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "ポート",
|
"port": "ポート",
|
||||||
"preferDiskAmount": "ディスク容量を優先的に表示",
|
"preferDiskAmount": "ディスク容量を優先的に表示",
|
||||||
"privateKey": "秘密鍵",
|
"privateKey": "秘密鍵",
|
||||||
|
"privateKeyNotFoundFmt": "秘密鍵 [{keyId}] が見つかりません。",
|
||||||
"process": "プロセス",
|
"process": "プロセス",
|
||||||
"prune": "剪定する",
|
"prune": "剪定する",
|
||||||
"pushToken": "プッシュトークン",
|
"pushToken": "プッシュトークン",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか?",
|
"sshConfigImportPermission": "~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか?",
|
||||||
"sshConfigImportTip": "初回サーバー作成時に~/.ssh/configの読み取りを促す",
|
"sshConfigImportTip": "初回サーバー作成時に~/.ssh/configの読み取りを促す",
|
||||||
"sshConfigImported": "SSH設定から{count}個のサーバーをインポートしました",
|
"sshConfigImported": "SSH設定から{count}個のサーバーをインポートしました",
|
||||||
|
"sshHostKeyChangedDesc": "{serverName} の SSH ホスト鍵が変更されました。このサーバーを信頼できる場合のみ続行してください。",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "フィンガープリント (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "フィンガープリント (MD5 16進): {fingerprint}",
|
||||||
|
"sshHostKeyType": "SSH ホストキーの種類",
|
||||||
|
"sshHostKeyNewDesc": "{serverName} から新しい SSH ホスト鍵を受信しました。信頼する前にフィンガープリントを確認してください。",
|
||||||
|
"sshHostKeyStoredFingerprint": "保存済みフィンガープリント: {fingerprint}",
|
||||||
"sshConfigManualSelect": "SSH設定ファイルを手動で選択しますか?",
|
"sshConfigManualSelect": "SSH設定ファイルを手動で選択しますか?",
|
||||||
"sshConfigNoServers": "SSH設定でサーバーが見つかりませんでした",
|
"sshConfigNoServers": "SSH設定でサーバーが見つかりませんでした",
|
||||||
"sshConfigPermissionDenied": "macOSの権限により、SSH設定ファイルにアクセスできません。",
|
"sshConfigPermissionDenied": "macOSの権限により、SSH設定ファイルにアクセスできません。",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
"wolTip": "WOL(Wake-on-LAN)を設定した後、サーバーに接続するたびにWOLリクエストが送信されます。",
|
||||||
"write": "書き込み",
|
"write": "書き込み",
|
||||||
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
|
||||||
"writeScriptTip": "サーバーに接続すると、システムの状態を監視するためのスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。"
|
"writeScriptTip": "サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
|
||||||
}
|
"logs": "ログ"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "nl",
|
"@@locale": "nl",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Met dank aan de volgende mensen die hebben deelgenomen aan.",
|
"aboutThanks": "Met dank aan de volgende mensen die hebben deelgenomen aan.",
|
||||||
"acceptBeta": "Accepteer testversie-updates",
|
"acceptBeta": "Accepteer testversie-updates",
|
||||||
"addSystemPrivateKeyTip": "Er is momenteel geen privésleutel, wilt u degene toevoegen die bij het systeem wordt geleverd (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Er is momenteel geen privésleutel, wilt u degene toevoegen die bij het systeem wordt geleverd (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Toegevoegd aan takenlijst",
|
"added2List": "Toegevoegd aan takenlijst",
|
||||||
"addr": "Adres",
|
"addr": "Adres",
|
||||||
"alreadyLastDir": "Al in de laatst gebruikte map.",
|
"alreadyLastDir": "Al in de laatst gebruikte map.",
|
||||||
|
"askAi": "AI vragen",
|
||||||
|
"askAiApiKey": "API-sleutel",
|
||||||
|
"askAiAwaitingResponse": "Wachten op AI-reactie...",
|
||||||
|
"askAiBaseUrl": "Basis-URL",
|
||||||
|
"askAiCommandInserted": "Commando in terminal ingevoegd",
|
||||||
|
"askAiConfigMissing": "Configureer {fields} in de instellingen.",
|
||||||
|
"askAiConfirmExecute": "Bevestigen voor uitvoeren",
|
||||||
|
"askAiConversation": "AI-gesprek",
|
||||||
|
"askAiDisclaimer": "AI kan fouten maken. Gebruik het zorgvuldig.",
|
||||||
|
"askAiFollowUpHint": "Stel een vervolgvraag...",
|
||||||
|
"askAiInsertTerminal": "In terminal invoegen",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiNoResponse": "Geen reactie",
|
||||||
|
"askAiRecommendedCommand": "Door AI voorgestelde opdracht",
|
||||||
|
"askAiSelectedContent": "Geselecteerde inhoud",
|
||||||
|
"askAiUsageHint": "Gebruikt in de SSH-terminal",
|
||||||
"atLeastOneTab": "Er moet minimaal één tabblad worden geselecteerd",
|
"atLeastOneTab": "Er moet minimaal één tabblad worden geselecteerd",
|
||||||
"authFailTip": "Authenticatie mislukt, controleer of het wachtwoord/sleutel/host/gebruiker, enz., incorrect zijn.",
|
"authFailTip": "Authenticatie mislukt, controleer of het wachtwoord/sleutel/host/gebruiker, enz., incorrect zijn.",
|
||||||
"autoBackupConflict": "Er kan slechts één automatische back-up tegelijk worden ingeschakeld.",
|
"autoBackupConflict": "Er kan slechts één automatische back-up tegelijk worden ingeschakeld.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Poort",
|
"port": "Poort",
|
||||||
"preferDiskAmount": "Geef de schijfcapaciteit prioriteit bij weergave",
|
"preferDiskAmount": "Geef de schijfcapaciteit prioriteit bij weergave",
|
||||||
"privateKey": "Privésleutel",
|
"privateKey": "Privésleutel",
|
||||||
|
"privateKeyNotFoundFmt": "Privésleutel [{keyId}] niet gevonden.",
|
||||||
"process": "Proces",
|
"process": "Proces",
|
||||||
"prune": "Snoeien",
|
"prune": "Snoeien",
|
||||||
"pushToken": "Push-token",
|
"pushToken": "Push-token",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?",
|
"sshConfigImportPermission": "Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?",
|
||||||
"sshConfigImportTip": "Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server",
|
"sshConfigImportTip": "Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server",
|
||||||
"sshConfigImported": "{count} servers geïmporteerd uit SSH-configuratie",
|
"sshConfigImported": "{count} servers geïmporteerd uit SSH-configuratie",
|
||||||
|
"sshHostKeyChangedDesc": "De SSH-hostsleutel voor {serverName} is gewijzigd. Ga alleen verder als u deze server vertrouwt.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Vingerafdruk (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Vingerafdruk (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "Type SSH-hostsleutel",
|
||||||
|
"sshHostKeyNewDesc": "Er is een nieuwe SSH-hostsleutel ontvangen van {serverName}. Controleer de vingerafdruk voordat u vertrouwt.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Opgeslagen vingerafdruk: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Wilt u het SSH-configuratiebestand handmatig selecteren?",
|
"sshConfigManualSelect": "Wilt u het SSH-configuratiebestand handmatig selecteren?",
|
||||||
"sshConfigNoServers": "Geen servers gevonden in SSH-configuratie",
|
"sshConfigNoServers": "Geen servers gevonden in SSH-configuratie",
|
||||||
"sshConfigPermissionDenied": "Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.",
|
"sshConfigPermissionDenied": "Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
"wolTip": "Na het configureren van WOL (Wake-on-LAN), wordt elke keer dat de server wordt verbonden een WOL-verzoek verzonden.",
|
||||||
"write": "Schrijven",
|
"write": "Schrijven",
|
||||||
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
|
||||||
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren."
|
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
|
||||||
}
|
"logs": "Logboeken"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "pt",
|
"@@locale": "pt",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Agradecimentos a todos os participantes.",
|
"aboutThanks": "Agradecimentos a todos os participantes.",
|
||||||
"acceptBeta": "Aceitar atualizações da versão de teste",
|
"acceptBeta": "Aceitar atualizações da versão de teste",
|
||||||
"addSystemPrivateKeyTip": "Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Adicionado à lista de tarefas",
|
"added2List": "Adicionado à lista de tarefas",
|
||||||
"addr": "Endereço",
|
"addr": "Endereço",
|
||||||
"alreadyLastDir": "Já é o diretório mais alto",
|
"alreadyLastDir": "Já é o diretório mais alto",
|
||||||
|
"askAi": "Perguntar à IA",
|
||||||
|
"askAiApiKey": "Chave de API",
|
||||||
|
"askAiAwaitingResponse": "Aguardando resposta da IA...",
|
||||||
|
"askAiBaseUrl": "URL base",
|
||||||
|
"askAiCommandInserted": "Comando inserido no terminal",
|
||||||
|
"askAiConfigMissing": "Configure {fields} nas configurações.",
|
||||||
|
"askAiConfirmExecute": "Confirmar antes de executar",
|
||||||
|
"askAiConversation": "Conversa com a IA",
|
||||||
|
"askAiDisclaimer": "A IA pode errar. Use com cautela.",
|
||||||
|
"askAiFollowUpHint": "Faça uma pergunta adicional...",
|
||||||
|
"askAiInsertTerminal": "Inserir no terminal",
|
||||||
|
"askAiModel": "Modelo",
|
||||||
|
"askAiNoResponse": "Sem resposta",
|
||||||
|
"askAiRecommendedCommand": "Comando sugerido pela IA",
|
||||||
|
"askAiSelectedContent": "Conteúdo selecionado",
|
||||||
|
"askAiUsageHint": "Usado no terminal SSH",
|
||||||
"atLeastOneTab": "Pelo menos uma aba deve ser selecionada",
|
"atLeastOneTab": "Pelo menos uma aba deve ser selecionada",
|
||||||
"authFailTip": "Autenticação falhou, por favor verifique se a senha/chave/host/usuário, etc., estão incorretos.",
|
"authFailTip": "Autenticação falhou, por favor verifique se a senha/chave/host/usuário, etc., estão incorretos.",
|
||||||
"autoBackupConflict": "Apenas um backup automático pode ser ativado por vez",
|
"autoBackupConflict": "Apenas um backup automático pode ser ativado por vez",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Porta",
|
"port": "Porta",
|
||||||
"preferDiskAmount": "Priorizar a exibição da capacidade do disco",
|
"preferDiskAmount": "Priorizar a exibição da capacidade do disco",
|
||||||
"privateKey": "Chave privada",
|
"privateKey": "Chave privada",
|
||||||
|
"privateKeyNotFoundFmt": "Chave privada [{keyId}] não encontrada.",
|
||||||
"process": "Processo",
|
"process": "Processo",
|
||||||
"prune": "Podar",
|
"prune": "Podar",
|
||||||
"pushToken": "Token de notificação push",
|
"pushToken": "Token de notificação push",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?",
|
"sshConfigImportPermission": "Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?",
|
||||||
"sshConfigImportTip": "Sugestão para ler ~/.ssh/config na criação do primeiro servidor",
|
"sshConfigImportTip": "Sugestão para ler ~/.ssh/config na criação do primeiro servidor",
|
||||||
"sshConfigImported": "Importados {count} servidores da configuração SSH",
|
"sshConfigImported": "Importados {count} servidores da configuração SSH",
|
||||||
|
"sshHostKeyChangedDesc": "A chave de host SSH de {serverName} foi alterada. Continue apenas se confiar neste servidor.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Impressão digital (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Impressão digital (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "Tipo de chave de host SSH",
|
||||||
|
"sshHostKeyNewDesc": "Uma nova chave de host SSH foi recebida de {serverName}. Verifique a impressão digital antes de confiar.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Impressão digital armazenada: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Gostaria de selecionar manualmente o arquivo de configuração SSH?",
|
"sshConfigManualSelect": "Gostaria de selecionar manualmente o arquivo de configuração SSH?",
|
||||||
"sshConfigNoServers": "Nenhum servidor encontrado na configuração SSH",
|
"sshConfigNoServers": "Nenhum servidor encontrado na configuração SSH",
|
||||||
"sshConfigPermissionDenied": "Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.",
|
"sshConfigPermissionDenied": "Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
"wolTip": "Após configurar o WOL (Wake-on-LAN), um pedido de WOL é enviado cada vez que o servidor é conectado.",
|
||||||
"write": "Escrita",
|
"write": "Escrita",
|
||||||
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
|
||||||
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script."
|
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
|
||||||
}
|
"logs": "Logs"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "ru",
|
"@@locale": "ru",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Благодарности всем участникам.",
|
"aboutThanks": "Благодарности всем участникам.",
|
||||||
"acceptBeta": "Принять обновления тестовой версии",
|
"acceptBeta": "Принять обновления тестовой версии",
|
||||||
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Добавлено в список задач",
|
"added2List": "Добавлено в список задач",
|
||||||
"addr": "Адрес",
|
"addr": "Адрес",
|
||||||
"alreadyLastDir": "Уже в корневом каталоге",
|
"alreadyLastDir": "Уже в корневом каталоге",
|
||||||
|
"askAi": "Спросить ИИ",
|
||||||
|
"askAiApiKey": "Ключ API",
|
||||||
|
"askAiAwaitingResponse": "Ожидание ответа ИИ...",
|
||||||
|
"askAiBaseUrl": "Базовый URL",
|
||||||
|
"askAiCommandInserted": "Команда вставлена в терминал",
|
||||||
|
"askAiConfigMissing": "Настройте {fields} в настройках.",
|
||||||
|
"askAiConfirmExecute": "Подтвердите перед выполнением",
|
||||||
|
"askAiConversation": "Разговор с ИИ",
|
||||||
|
"askAiDisclaimer": "ИИ может ошибаться. Используйте с осторожностью.",
|
||||||
|
"askAiFollowUpHint": "Задайте дополнительный вопрос...",
|
||||||
|
"askAiInsertTerminal": "Вставить в терминал",
|
||||||
|
"askAiModel": "Модель",
|
||||||
|
"askAiNoResponse": "Нет ответа",
|
||||||
|
"askAiRecommendedCommand": "Команда, предложенная ИИ",
|
||||||
|
"askAiSelectedContent": "Выбранное содержимое",
|
||||||
|
"askAiUsageHint": "Используется в SSH-терминале",
|
||||||
"atLeastOneTab": "Должна быть выбрана хотя бы одна вкладка",
|
"atLeastOneTab": "Должна быть выбрана хотя бы одна вкладка",
|
||||||
"authFailTip": "Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.",
|
"authFailTip": "Аутентификация не удалась, пожалуйста, проверьте, правильны ли пароль/ключ/хост/пользователь и т.д.",
|
||||||
"autoBackupConflict": "Может быть включено только одно автоматическое резервное копирование",
|
"autoBackupConflict": "Может быть включено только одно автоматическое резервное копирование",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Порт",
|
"port": "Порт",
|
||||||
"preferDiskAmount": "Приоритетное отображение объёма диска",
|
"preferDiskAmount": "Приоритетное отображение объёма диска",
|
||||||
"privateKey": "Приватный ключ",
|
"privateKey": "Приватный ключ",
|
||||||
|
"privateKeyNotFoundFmt": "Закрытый ключ [{keyId}] не найден.",
|
||||||
"process": "Процесс",
|
"process": "Процесс",
|
||||||
"prune": "Обрезать",
|
"prune": "Обрезать",
|
||||||
"pushToken": "Токен уведомлений",
|
"pushToken": "Токен уведомлений",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?",
|
"sshConfigImportPermission": "Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?",
|
||||||
"sshConfigImportTip": "Предложение прочитать ~/.ssh/config при создании первого сервера",
|
"sshConfigImportTip": "Предложение прочитать ~/.ssh/config при создании первого сервера",
|
||||||
"sshConfigImported": "Импортировано {count} серверов из SSH-конфигурации",
|
"sshConfigImported": "Импортировано {count} серверов из SSH-конфигурации",
|
||||||
|
"sshHostKeyChangedDesc": "SSH-ключ хоста для {serverName} изменился. Продолжайте только если доверяете этому серверу.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Отпечаток (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Отпечаток (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "Тип ключа хоста SSH",
|
||||||
|
"sshHostKeyNewDesc": "Получен новый SSH-ключ хоста от {serverName}. Проверьте отпечаток перед продолжением.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Сохранённый отпечаток: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Хотели бы вы вручную выбрать файл конфигурации SSH?",
|
"sshConfigManualSelect": "Хотели бы вы вручную выбрать файл конфигурации SSH?",
|
||||||
"sshConfigNoServers": "Серверы не найдены в SSH-конфигурации",
|
"sshConfigNoServers": "Серверы не найдены в SSH-конфигурации",
|
||||||
"sshConfigPermissionDenied": "Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.",
|
"sshConfigPermissionDenied": "Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
"wolTip": "После настройки WOL (Wake-on-LAN) при каждом подключении к серверу отправляется запрос WOL.",
|
||||||
"write": "Запись",
|
"write": "Запись",
|
||||||
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
|
||||||
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта."
|
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
|
||||||
}
|
"logs": "Журналы"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "tr",
|
"@@locale": "tr",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Aşağıdaki katılımcılara teşekkürler.",
|
"aboutThanks": "Aşağıdaki katılımcılara teşekkürler.",
|
||||||
"acceptBeta": "Beta sürüm güncellemelerini kabul et",
|
"acceptBeta": "Beta sürüm güncellemelerini kabul et",
|
||||||
"addSystemPrivateKeyTip": "Şu anda özel anahtarlar mevcut değil, sistemle birlikte gelen anahtarı (~/.ssh/id_rsa) eklemek ister misiniz?",
|
"addSystemPrivateKeyTip": "Şu anda özel anahtarlar mevcut değil, sistemle birlikte gelen anahtarı (~/.ssh/id_rsa) eklemek ister misiniz?",
|
||||||
"added2List": "Görev listesine eklendi",
|
"added2List": "Görev listesine eklendi",
|
||||||
"addr": "Adres",
|
"addr": "Adres",
|
||||||
"alreadyLastDir": "Zaten son dizindesiniz.",
|
"alreadyLastDir": "Zaten son dizindesiniz.",
|
||||||
|
"askAi": "Yapay zekaya sor",
|
||||||
|
"askAiApiKey": "API anahtarı",
|
||||||
|
"askAiAwaitingResponse": "Yapay zekâ yanıtı bekleniyor...",
|
||||||
|
"askAiBaseUrl": "Temel URL",
|
||||||
|
"askAiCommandInserted": "Komut terminale eklendi",
|
||||||
|
"askAiConfigMissing": "Lütfen Ayarlar'da {fields} öğesini yapılandırın.",
|
||||||
|
"askAiConfirmExecute": "Çalıştırmadan önce onayla",
|
||||||
|
"askAiConversation": "YZ sohbeti",
|
||||||
|
"askAiDisclaimer": "Yapay zeka hata yapabilir. Lütfen dikkatli kullanın.",
|
||||||
|
"askAiFollowUpHint": "Yeni bir soru sor...",
|
||||||
|
"askAiInsertTerminal": "Terminale ekle",
|
||||||
|
"askAiModel": "Model",
|
||||||
|
"askAiNoResponse": "Yanıt yok",
|
||||||
|
"askAiRecommendedCommand": "YZ önerilen komut",
|
||||||
|
"askAiSelectedContent": "Seçilen içerik",
|
||||||
|
"askAiUsageHint": "SSH Terminalinde kullanılır",
|
||||||
"atLeastOneTab": "En az bir sekme seçilmelidir",
|
"atLeastOneTab": "En az bir sekme seçilmelidir",
|
||||||
"authFailTip": "Kimlik doğrulama başarısız oldu, lütfen kimlik bilgilerinin doğru olup olmadığını kontrol edin",
|
"authFailTip": "Kimlik doğrulama başarısız oldu, lütfen kimlik bilgilerinin doğru olup olmadığını kontrol edin",
|
||||||
"autoBackupConflict": "Aynı anda yalnızca bir otomatik yedekleme açık olabilir.",
|
"autoBackupConflict": "Aynı anda yalnızca bir otomatik yedekleme açık olabilir.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferDiskAmount": "Disk kapasitesini öncelikli olarak göster",
|
"preferDiskAmount": "Disk kapasitesini öncelikli olarak göster",
|
||||||
"privateKey": "Özel Anahtar",
|
"privateKey": "Özel Anahtar",
|
||||||
|
"privateKeyNotFoundFmt": "Özel anahtar [{keyId}] bulunamadı.",
|
||||||
"process": "İşlem",
|
"process": "İşlem",
|
||||||
"prune": "Budamak",
|
"prune": "Budamak",
|
||||||
"pushToken": "Push belirteci",
|
"pushToken": "Push belirteci",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?",
|
"sshConfigImportPermission": "~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?",
|
||||||
"sshConfigImportTip": "İlk sunucu oluşturulurken ~/.ssh/config okuma istemi",
|
"sshConfigImportTip": "İlk sunucu oluşturulurken ~/.ssh/config okuma istemi",
|
||||||
"sshConfigImported": "SSH yapılandırmasından {count} sunucu içe aktarıldı",
|
"sshConfigImported": "SSH yapılandırmasından {count} sunucu içe aktarıldı",
|
||||||
|
"sshHostKeyChangedDesc": "{serverName} için SSH ana bilgisayar anahtarı değişti. Yalnızca bu sunucuya güveniyorsanız devam edin.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Parmak izi (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Parmak izi (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "SSH ana bilgisayar anahtarı türü",
|
||||||
|
"sshHostKeyNewDesc": "{serverName} üzerinden yeni bir SSH ana bilgisayar anahtarı alındı. Güvenmeden önce parmak izini kontrol edin.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Kaydedilen parmak izi: {fingerprint}",
|
||||||
"sshConfigManualSelect": "SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?",
|
"sshConfigManualSelect": "SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?",
|
||||||
"sshConfigNoServers": "SSH yapılandırmasında sunucu bulunamadı",
|
"sshConfigNoServers": "SSH yapılandırmasında sunucu bulunamadı",
|
||||||
"sshConfigPermissionDenied": "macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.",
|
"sshConfigPermissionDenied": "macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
"wolTip": "WOL (Wake-on-LAN) yapılandırıldıktan sonra, sunucuya her bağlanıldığında bir WOL isteği gönderilir.",
|
||||||
"write": "Yaz",
|
"write": "Yaz",
|
||||||
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
|
||||||
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz."
|
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
|
||||||
}
|
"logs": "Günlükler"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "uk",
|
"@@locale": "uk",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "Дякуємо наступним особам, які взяли участь.",
|
"aboutThanks": "Дякуємо наступним особам, які взяли участь.",
|
||||||
"acceptBeta": "Прийняти оновлення бета-версії",
|
"acceptBeta": "Прийняти оновлення бета-версії",
|
||||||
"addSystemPrivateKeyTip": "Наразі приватних ключів нема, хочете додати той, що йде з системою (~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "Наразі приватних ключів нема, хочете додати той, що йде з системою (~/.ssh/id_rsa)?",
|
||||||
"added2List": "Додано до списку завдань",
|
"added2List": "Додано до списку завдань",
|
||||||
"addr": "Адреса",
|
"addr": "Адреса",
|
||||||
"alreadyLastDir": "Вже в останньому каталозі.",
|
"alreadyLastDir": "Вже в останньому каталозі.",
|
||||||
|
"askAi": "Запитати ШІ",
|
||||||
|
"askAiApiKey": "Ключ API",
|
||||||
|
"askAiAwaitingResponse": "Очікування відповіді ШІ...",
|
||||||
|
"askAiBaseUrl": "Базова URL",
|
||||||
|
"askAiCommandInserted": "Команду вставлено в термінал",
|
||||||
|
"askAiConfigMissing": "Налаштуйте {fields} у налаштуваннях.",
|
||||||
|
"askAiConfirmExecute": "Підтвердити перед виконанням",
|
||||||
|
"askAiConversation": "Розмова з ШІ",
|
||||||
|
"askAiDisclaimer": "ШІ може помилятися. Користуйтеся обережно.",
|
||||||
|
"askAiFollowUpHint": "Поставте додаткове запитання...",
|
||||||
|
"askAiInsertTerminal": "Вставити в термінал",
|
||||||
|
"askAiModel": "Модель",
|
||||||
|
"askAiNoResponse": "Відповідь відсутня",
|
||||||
|
"askAiRecommendedCommand": "Команда, запропонована ШІ",
|
||||||
|
"askAiSelectedContent": "Вибраний вміст",
|
||||||
|
"askAiUsageHint": "Використовується в SSH-терміналі",
|
||||||
"atLeastOneTab": "Потрібно вибрати принаймні одну вкладку",
|
"atLeastOneTab": "Потрібно вибрати принаймні одну вкладку",
|
||||||
"authFailTip": "Авторизація не вдалася, будь ласка, перевірте правильність облікових даних",
|
"authFailTip": "Авторизація не вдалася, будь ласка, перевірте правильність облікових даних",
|
||||||
"autoBackupConflict": "Тільки одне автоматичне резервне копіювання може бути активне одночасно.",
|
"autoBackupConflict": "Тільки одне автоматичне резервне копіювання може бути активне одночасно.",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "Порт",
|
"port": "Порт",
|
||||||
"preferDiskAmount": "Пріоритетно показувати ємність диска",
|
"preferDiskAmount": "Пріоритетно показувати ємність диска",
|
||||||
"privateKey": "Приватний ключ",
|
"privateKey": "Приватний ключ",
|
||||||
|
"privateKeyNotFoundFmt": "Приватний ключ [{keyId}] не знайдено.",
|
||||||
"process": "Процес",
|
"process": "Процес",
|
||||||
"prune": "Обрізати",
|
"prune": "Обрізати",
|
||||||
"pushToken": "Надіслати токен",
|
"pushToken": "Надіслати токен",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?",
|
"sshConfigImportPermission": "Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?",
|
||||||
"sshConfigImportTip": "Пропозиція прочитати ~/.ssh/config при створенні першого сервера",
|
"sshConfigImportTip": "Пропозиція прочитати ~/.ssh/config при створенні першого сервера",
|
||||||
"sshConfigImported": "Імпортовано {count} серверів з SSH-конфігурації",
|
"sshConfigImported": "Імпортовано {count} серверів з SSH-конфігурації",
|
||||||
|
"sshHostKeyChangedDesc": "SSH-ключ хоста для {serverName} змінено. Продовжуйте лише якщо довіряєте цьому серверу.",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "Відбиток (MD5 Base64): {fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "Відбиток (MD5 hex): {fingerprint}",
|
||||||
|
"sshHostKeyType": "Тип ключа хоста SSH",
|
||||||
|
"sshHostKeyNewDesc": "Отримано новий SSH-ключ хоста від {serverName}. Перевірте відбиток перед тим, як довіряти.",
|
||||||
|
"sshHostKeyStoredFingerprint": "Збережений відбиток: {fingerprint}",
|
||||||
"sshConfigManualSelect": "Чи хочете ви вручну вибрати файл конфігурації SSH?",
|
"sshConfigManualSelect": "Чи хочете ви вручну вибрати файл конфігурації SSH?",
|
||||||
"sshConfigNoServers": "Сервери не знайдені в SSH-конфігурації",
|
"sshConfigNoServers": "Сервери не знайдені в SSH-конфігурації",
|
||||||
"sshConfigPermissionDenied": "Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.",
|
"sshConfigPermissionDenied": "Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
"wolTip": "Після налаштування WOL (Wake-on-LAN), при кожному підключенні до сервера відправляється запит WOL.",
|
||||||
"write": "Записати",
|
"write": "Записати",
|
||||||
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
|
||||||
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта."
|
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
|
||||||
}
|
"logs": "Журнали"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "zh",
|
"@@locale": "zh",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "感谢以下参与的各位。",
|
"aboutThanks": "感谢以下参与的各位。",
|
||||||
"acceptBeta": "接受测试版更新推送",
|
"acceptBeta": "接受测试版更新推送",
|
||||||
"addSystemPrivateKeyTip": "检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "检测到暂无私钥,是否添加系统默认的私钥(~/.ssh/id_rsa)?",
|
||||||
"added2List": "已添加至任务列表",
|
"added2List": "已添加至任务列表",
|
||||||
"addr": "地址",
|
"addr": "地址",
|
||||||
"alreadyLastDir": "已是顶级目录",
|
"alreadyLastDir": "已是顶级目录",
|
||||||
|
"askAi": "问 AI",
|
||||||
|
"askAiApiKey": "API 密钥",
|
||||||
|
"askAiAwaitingResponse": "等待 AI 响应...",
|
||||||
|
"askAiBaseUrl": "基础 URL",
|
||||||
|
"askAiCommandInserted": "命令已插入终端",
|
||||||
|
"askAiConfigMissing": "请前往设置配置 {fields}",
|
||||||
|
"askAiConfirmExecute": "执行前确认",
|
||||||
|
"askAiConversation": "AI 对话",
|
||||||
|
"askAiDisclaimer": "AI 可能会犯错,请谨慎使用。",
|
||||||
|
"askAiFollowUpHint": "继续提问...",
|
||||||
|
"askAiInsertTerminal": "插入终端",
|
||||||
|
"askAiModel": "模型",
|
||||||
|
"askAiNoResponse": "无回复内容",
|
||||||
|
"askAiRecommendedCommand": "AI 推荐命令",
|
||||||
|
"askAiSelectedContent": "选中的内容",
|
||||||
|
"askAiUsageHint": "用于 SSH 终端",
|
||||||
"atLeastOneTab": "至少需要选择一个标签",
|
"atLeastOneTab": "至少需要选择一个标签",
|
||||||
"authFailTip": "认证失败,请检查连接信息是否正确",
|
"authFailTip": "认证失败,请检查连接信息是否正确",
|
||||||
"autoBackupConflict": "仅可启用一个自动备份任务",
|
"autoBackupConflict": "仅可启用一个自动备份任务",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "端口",
|
"port": "端口",
|
||||||
"preferDiskAmount": "优先显示硬盘容量",
|
"preferDiskAmount": "优先显示硬盘容量",
|
||||||
"privateKey": "私钥",
|
"privateKey": "私钥",
|
||||||
|
"privateKeyNotFoundFmt": "未找到私钥 [{keyId}]。",
|
||||||
"process": "进程",
|
"process": "进程",
|
||||||
"prune": "修剪",
|
"prune": "修剪",
|
||||||
"pushToken": "消息推送 Token",
|
"pushToken": "消息推送 Token",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "是否允许读取 ~/.ssh/config 并自动导入服务器设置?",
|
"sshConfigImportPermission": "是否允许读取 ~/.ssh/config 并自动导入服务器设置?",
|
||||||
"sshConfigImportTip": "首次创建服务器时提示读取 ~/.ssh/config",
|
"sshConfigImportTip": "首次创建服务器时提示读取 ~/.ssh/config",
|
||||||
"sshConfigImported": "从 SSH 配置导入了 {count} 个服务器",
|
"sshConfigImported": "从 SSH 配置导入了 {count} 个服务器",
|
||||||
|
"sshHostKeyChangedDesc": "服务器 {serverName} 的 SSH 主机密钥已更改,仅在信任该服务器时继续。",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "指纹(MD5 Base64):{fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "指纹(MD5 十六进制):{fingerprint}",
|
||||||
|
"sshHostKeyType": "SSH 主机密钥类型",
|
||||||
|
"sshHostKeyNewDesc": "收到来自 {serverName} 的新 SSH 主机密钥,在信任前请检查指纹。",
|
||||||
|
"sshHostKeyStoredFingerprint": "已存储的指纹:{fingerprint}",
|
||||||
"sshConfigManualSelect": "是否要手动选择 SSH 配置文件?",
|
"sshConfigManualSelect": "是否要手动选择 SSH 配置文件?",
|
||||||
"sshConfigNoServers": "SSH 配置中未找到服务器",
|
"sshConfigNoServers": "SSH 配置中未找到服务器",
|
||||||
"sshConfigPermissionDenied": "由于 macOS 权限限制,无法访问 SSH 配置文件。",
|
"sshConfigPermissionDenied": "由于 macOS 权限限制,无法访问 SSH 配置文件。",
|
||||||
@@ -284,5 +293,13 @@
|
|||||||
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
"wolTip": "配置 WOL 后,每次连接服务器时将自动发送唤醒请求",
|
||||||
"write": "写",
|
"write": "写",
|
||||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。"
|
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。",
|
||||||
}
|
"menuSettings": "设置",
|
||||||
|
"menuQuit": "退出",
|
||||||
|
"menuNavigate": "导航",
|
||||||
|
"menuInfo": "信息",
|
||||||
|
"menuGitHubRepository": "GitHub 仓库",
|
||||||
|
"menuWiki": "Wiki",
|
||||||
|
"menuHelp": "帮助",
|
||||||
|
"logs": "日志"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "zh_TW",
|
"@@locale": "zh_TW",
|
||||||
"@clearServerStatsContent": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@clearServerStatsTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"serverName": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aboutThanks": "感謝以下參與的各位。",
|
"aboutThanks": "感謝以下參與的各位。",
|
||||||
"acceptBeta": "接受測試版更新推送",
|
"acceptBeta": "接受測試版更新推送",
|
||||||
"addSystemPrivateKeyTip": "偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?",
|
"addSystemPrivateKeyTip": "偵測到尚無私鑰,是否要加入系統預設的私鑰(~/.ssh/id_rsa)?",
|
||||||
"added2List": "已新增至任務清單",
|
"added2List": "已新增至任務清單",
|
||||||
"addr": "位址",
|
"addr": "位址",
|
||||||
"alreadyLastDir": "已是頂層目錄",
|
"alreadyLastDir": "已是頂層目錄",
|
||||||
|
"askAi": "詢問 AI",
|
||||||
|
"askAiApiKey": "API 金鑰",
|
||||||
|
"askAiAwaitingResponse": "等待 AI 回應...",
|
||||||
|
"askAiBaseUrl": "基礎 URL",
|
||||||
|
"askAiCommandInserted": "指令已插入終端機",
|
||||||
|
"askAiConfigMissing": "請前往設定配置 {fields}",
|
||||||
|
"askAiConfirmExecute": "執行前確認",
|
||||||
|
"askAiConversation": "AI 對話",
|
||||||
|
"askAiDisclaimer": "AI 可能會犯錯,請謹慎使用。",
|
||||||
|
"askAiFollowUpHint": "繼續提問...",
|
||||||
|
"askAiInsertTerminal": "插入終端機",
|
||||||
|
"askAiModel": "模型",
|
||||||
|
"askAiNoResponse": "無回覆內容",
|
||||||
|
"askAiRecommendedCommand": "AI 推薦指令",
|
||||||
|
"askAiSelectedContent": "選取的內容",
|
||||||
|
"askAiUsageHint": "於 SSH 終端機中使用",
|
||||||
"atLeastOneTab": "至少需要選擇一個標籤",
|
"atLeastOneTab": "至少需要選擇一個標籤",
|
||||||
"authFailTip": "認證失敗,請檢查連線資訊是否正確",
|
"authFailTip": "認證失敗,請檢查連線資訊是否正確",
|
||||||
"autoBackupConflict": "僅能啟用一項自動備份任務",
|
"autoBackupConflict": "僅能啟用一項自動備份任務",
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
"port": "埠",
|
"port": "埠",
|
||||||
"preferDiskAmount": "優先顯示硬碟容量",
|
"preferDiskAmount": "優先顯示硬碟容量",
|
||||||
"privateKey": "私鑰",
|
"privateKey": "私鑰",
|
||||||
|
"privateKeyNotFoundFmt": "未找到私鑰 [{keyId}]。",
|
||||||
"process": "處理程序",
|
"process": "處理程序",
|
||||||
"prune": "修剪",
|
"prune": "修剪",
|
||||||
"pushToken": "消息推送 Token",
|
"pushToken": "消息推送 Token",
|
||||||
@@ -223,6 +226,12 @@
|
|||||||
"sshConfigImportPermission": "您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定?",
|
"sshConfigImportPermission": "您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定?",
|
||||||
"sshConfigImportTip": "在建立第一個伺服器時提示讀取 ~/.ssh/config",
|
"sshConfigImportTip": "在建立第一個伺服器時提示讀取 ~/.ssh/config",
|
||||||
"sshConfigImported": "已從SSH設定匯入{count}個伺服器",
|
"sshConfigImported": "已從SSH設定匯入{count}個伺服器",
|
||||||
|
"sshHostKeyChangedDesc": "伺服器 {serverName} 的 SSH 主機金鑰已變更,僅在信任該伺服器時繼續。",
|
||||||
|
"sshHostKeyFingerprintMd5Base64": "指紋(MD5 Base64):{fingerprint}",
|
||||||
|
"sshHostKeyFingerprintMd5Hex": "指紋(MD5 十六進位):{fingerprint}",
|
||||||
|
"sshHostKeyType": "SSH 主機金鑰類型",
|
||||||
|
"sshHostKeyNewDesc": "收到來自 {serverName} 的新 SSH 主機金鑰,信任前請先檢查指紋。",
|
||||||
|
"sshHostKeyStoredFingerprint": "已儲存的指紋:{fingerprint}",
|
||||||
"sshConfigManualSelect": "是否要手動選擇 SSH 設定檔案?",
|
"sshConfigManualSelect": "是否要手動選擇 SSH 設定檔案?",
|
||||||
"sshConfigNoServers": "SSH設定中未找到伺服器",
|
"sshConfigNoServers": "SSH設定中未找到伺服器",
|
||||||
"sshConfigPermissionDenied": "由於 macOS 權限限制,無法存取 SSH 設定檔案。",
|
"sshConfigPermissionDenied": "由於 macOS 權限限制,無法存取 SSH 設定檔案。",
|
||||||
@@ -284,5 +293,6 @@
|
|||||||
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
"wolTip": "設定 WOL 後,每次連線伺服器時將自動發送喚醒請求",
|
||||||
"write": "寫入",
|
"write": "寫入",
|
||||||
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
|
||||||
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。"
|
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
|
||||||
}
|
"logs": "日誌"
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ void _setupDebug() {
|
|||||||
Logger.root.level = Level.ALL;
|
Logger.root.level = Level.ALL;
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
DebugProvider.addLog(record);
|
DebugProvider.addLog(record);
|
||||||
lprint(record);
|
|
||||||
if (record.error != null) print(record.error);
|
if (record.error != null) print(record.error);
|
||||||
if (record.stackTrace != null) print(record.stackTrace);
|
if (record.stackTrace != null) print(record.stackTrace);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ extension on _ContainerPageState {
|
|||||||
'${switch (_containerState.type) {
|
'${switch (_containerState.type) {
|
||||||
ContainerType.podman => 'podman',
|
ContainerType.podman => 'podman',
|
||||||
ContainerType.docker => 'docker',
|
ContainerType.docker => 'docker',
|
||||||
}} exec -it ${dItem.id} sh',
|
}} exec -it ${dItem.id} sh -c "command -v bash && exec bash || command -v ash && exec ash || exec sh"',
|
||||||
);
|
);
|
||||||
SSHPage.route.go(context, args);
|
SSHPage.route.go(context, args);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/foundation.dart' show kReleaseMode;
|
import 'package:flutter/foundation.dart' show kReleaseMode;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -5,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:server_box/core/chan.dart';
|
import 'package:server_box/core/chan.dart';
|
||||||
import 'package:server_box/core/sync.dart';
|
import 'package:server_box/core/sync.dart';
|
||||||
|
import 'package:server_box/data/model/app/menu/platform.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/server/all.dart';
|
import 'package:server_box/data/provider/server/all.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
@@ -134,7 +137,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||||
|
|
||||||
return Scaffold(
|
final Widget mainContent = Scaffold(
|
||||||
appBar: _AppBar(MediaQuery.paddingOf(context).top),
|
appBar: _AppBar(MediaQuery.paddingOf(context).top),
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -157,6 +160,16 @@ class _HomePageState extends ConsumerState<HomePage>
|
|||||||
),
|
),
|
||||||
bottomNavigationBar: isMobile ? _buildBottomBar() : null,
|
bottomNavigationBar: isMobile ? _buildBottomBar() : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
return PlatformMenuBar(
|
||||||
|
menus: MacOSMenuBarManager.buildMenuBar(context, (int index) {
|
||||||
|
_onDestinationSelected(index);
|
||||||
|
}),
|
||||||
|
child: mainContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return mainContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomBar() {
|
Widget _buildBottomBar() {
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
|||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: Text(l10n.connectionStats),
|
title: Text(l10n.connectionStats),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(onPressed: _loadStats, icon: const Icon(Icons.refresh), tooltip: libL10n.refresh),
|
||||||
onPressed: _loadStats,
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
tooltip: libL10n.refresh,
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _showClearAllDialog,
|
onPressed: _showClearAllDialog,
|
||||||
icon: const Icon(Icons.clear_all, color: Colors.red),
|
icon: const Icon(Icons.clear_all, color: Colors.red),
|
||||||
@@ -75,140 +71,90 @@ class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildServerStatsCard(ServerConnectionStats stats) {
|
Widget _buildServerStatsCard(ServerConnectionStats stats) {
|
||||||
final successRate = stats.totalAttempts == 0
|
final successRate = stats.totalAttempts == 0 ? 'N/A' : '${(stats.successRate * 100).toStringAsFixed(1)}%';
|
||||||
? 'N/A'
|
|
||||||
: '${(stats.successRate * 100).toStringAsFixed(1)}%';
|
|
||||||
final lastSuccessTime = stats.lastSuccessTime;
|
final lastSuccessTime = stats.lastSuccessTime;
|
||||||
final lastFailureTime = stats.lastFailureTime;
|
final lastFailureTime = stats.lastFailureTime;
|
||||||
|
|
||||||
return Card(
|
return Padding(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(16),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: Text(
|
||||||
Expanded(
|
stats.serverName,
|
||||||
child: Text(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
stats.serverName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
'${libL10n.success}: $successRate',
|
Text(
|
||||||
style: TextStyle(
|
'${libL10n.success}: $successRate',
|
||||||
fontSize: 16,
|
style: TextStyle(
|
||||||
color: stats.successRate >= 0.8
|
fontSize: 16,
|
||||||
? Colors.green
|
color: stats.successRate >= 0.8
|
||||||
: stats.successRate >= 0.5
|
? Colors.green
|
||||||
? Colors.orange
|
: stats.successRate >= 0.5
|
||||||
: Colors.red,
|
? Colors.orange
|
||||||
fontWeight: FontWeight.bold,
|
: Colors.red,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
_buildStatItem(
|
|
||||||
l10n.totalAttempts,
|
|
||||||
stats.totalAttempts.toString(),
|
|
||||||
Icons.all_inclusive,
|
|
||||||
),
|
|
||||||
_buildStatItem(
|
|
||||||
libL10n.success,
|
|
||||||
stats.successCount.toString(),
|
|
||||||
Icons.check_circle,
|
|
||||||
Colors.green,
|
|
||||||
),
|
|
||||||
_buildStatItem(
|
|
||||||
libL10n.fail,
|
|
||||||
stats.failureCount.toString(),
|
|
||||||
Icons.error,
|
|
||||||
Colors.red,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (lastSuccessTime != null || lastFailureTime != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (lastSuccessTime != null)
|
|
||||||
_buildTimeItem(
|
|
||||||
l10n.lastSuccess,
|
|
||||||
lastSuccessTime,
|
|
||||||
Icons.check_circle,
|
|
||||||
Colors.green,
|
|
||||||
),
|
|
||||||
if (lastFailureTime != null)
|
|
||||||
_buildTimeItem(
|
|
||||||
l10n.lastFailure,
|
|
||||||
lastFailureTime,
|
|
||||||
Icons.error,
|
|
||||||
Colors.red,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildStatItem(l10n.totalAttempts, stats.totalAttempts.toString(), Icons.all_inclusive),
|
||||||
|
_buildStatItem(
|
||||||
|
libL10n.success,
|
||||||
|
stats.successCount.toString(),
|
||||||
|
Icons.check_circle,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
_buildStatItem(libL10n.fail, stats.failureCount.toString(), Icons.error, Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (lastSuccessTime != null || lastFailureTime != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
const Divider(),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l10n.recentConnections,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => _showServerDetailsDialog(stats),
|
|
||||||
child: Text(l10n.viewDetails),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...stats.recentConnections.take(3).map(_buildConnectionItem),
|
if (lastSuccessTime != null)
|
||||||
|
_buildTimeItem(l10n.lastSuccess, lastSuccessTime, Icons.check_circle, Colors.green),
|
||||||
|
if (lastFailureTime != null)
|
||||||
|
_buildTimeItem(l10n.lastFailure, lastFailureTime, Icons.error, Colors.red),
|
||||||
],
|
],
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(l10n.recentConnections, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||||
|
TextButton(onPressed: () => _showServerDetailsDialog(stats), child: Text(l10n.viewDetails)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...stats.recentConnections.take(3).map(_buildConnectionItem),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
).cardx;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatItem(
|
Widget _buildStatItem(String label, String value, IconData icon, [Color? color]) {
|
||||||
String label,
|
|
||||||
String value,
|
|
||||||
IconData icon, [
|
|
||||||
Color? color,
|
|
||||||
]) {
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 24, color: color ?? Colors.grey),
|
Icon(icon, size: 24, color: color ?? Colors.grey),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTimeItem(
|
Widget _buildTimeItem(String label, DateTime time, IconData icon, Color color) {
|
||||||
String label,
|
|
||||||
DateTime time,
|
|
||||||
IconData icon,
|
|
||||||
Color color,
|
|
||||||
) {
|
|
||||||
final timeStr = time.simple();
|
final timeStr = time.simple();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
@@ -216,10 +162,7 @@ class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: color),
|
Icon(icon, size: 16, color: color),
|
||||||
UIs.width7,
|
UIs.width7,
|
||||||
Text(
|
Text('$label: ', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||||
'$label: ',
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -244,13 +187,8 @@ class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
|||||||
UIs.width7,
|
UIs.width7,
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
isSuccess
|
isSuccess ? '${libL10n.success} (${stat.durationMs}ms)' : stat.result.displayName,
|
||||||
? '${libL10n.success} (${stat.durationMs}ms)'
|
style: TextStyle(fontSize: 12, color: isSuccess ? Colors.green : Colors.red),
|
||||||
: stat.result.displayName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: isSuccess ? Colors.green : Colors.red,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -289,9 +227,7 @@ extension on _ConnectionStatsPageState {
|
|||||||
isSuccess
|
isSuccess
|
||||||
? '${libL10n.success} (${stat.durationMs}ms)'
|
? '${libL10n.success} (${stat.durationMs}ms)'
|
||||||
: '${libL10n.fail}: ${stat.result.displayName}',
|
: '${libL10n.fail}: ${stat.result.displayName}',
|
||||||
style: TextStyle(
|
style: TextStyle(color: isSuccess ? Colors.green : Colors.red),
|
||||||
color: isSuccess ? Colors.green : Colors.red,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!isSuccess && stat.errorMessage.isNotEmpty)
|
if (!isSuccess && stat.errorMessage.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
@@ -313,10 +249,7 @@ extension on _ConnectionStatsPageState {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_showClearServerStatsDialog(stats);
|
_showClearServerStatsDialog(stats);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(l10n.clearThisServerStats, style: TextStyle(color: Colors.red)),
|
||||||
l10n.clearThisServerStats,
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
final _netSortType = ValueNotifier(_NetSortType.device);
|
final _netSortType = ValueNotifier(_NetSortType.device);
|
||||||
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());
|
||||||
|
late final _cpuViewAsProgress = _settings.cpuViewAsProgress.fetch();
|
||||||
|
late final _moveServerFuncs = _settings.moveServerFuncs.fetch();
|
||||||
|
late final _displayCpuIndex = _settings.displayCpuIndex.fetch();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -97,7 +100,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainPage(ServerState si) {
|
Widget _buildMainPage(ServerState si) {
|
||||||
final buildFuncs = !Stores.setting.moveServerFuncs.fetch();
|
final buildFuncs = !_moveServerFuncs;
|
||||||
final logo = _buildLogo(si);
|
final logo = _buildLogo(si);
|
||||||
final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
|
final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
|
||||||
for (final card in _cardsOrder) {
|
for (final card in _cardsOrder) {
|
||||||
@@ -197,7 +200,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> children = Stores.setting.cpuViewAsProgress.fetch()
|
final List<Widget> children = _cpuViewAsProgress
|
||||||
? _buildCPUProgress(ss.cpu)
|
? _buildCPUProgress(ss.cpu)
|
||||||
: [_buildCPUChart(ss)];
|
: [_buildCPUChart(ss)];
|
||||||
|
|
||||||
@@ -258,7 +261,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
|
|||||||
const kRowThreshold = 4;
|
const kRowThreshold = 4;
|
||||||
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
|
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
final displayCpuIndexSetting = Stores.setting.displayCpuIndex.fetch();
|
final displayCpuIndexSetting = _displayCpuIndex;
|
||||||
|
|
||||||
if (cs.coresCount > kCoresCountThreshold) {
|
if (cs.coresCount > kCoresCountThreshold) {
|
||||||
final numCoresToDisplay = cs.coresCount - 1;
|
final numCoresToDisplay = cs.coresCount - 1;
|
||||||
|
|||||||
@@ -222,6 +222,30 @@ extension _Actions on _ServerEditPageState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final oldSpi = this.spi;
|
||||||
|
if (oldSpi != null) {
|
||||||
|
final originalJumpChain = oldSpi.jumpChainIds ?? (oldSpi.jumpId == null ? const <String>[] : [oldSpi.jumpId!]);
|
||||||
|
final currentJumpChain = _jumpChain.value;
|
||||||
|
|
||||||
|
final jumpChainChanged = () {
|
||||||
|
if (originalJumpChain.isEmpty && currentJumpChain.isEmpty) return false;
|
||||||
|
if (originalJumpChain.length != currentJumpChain.length) return true;
|
||||||
|
for (var i = 0; i < originalJumpChain.length; i++) {
|
||||||
|
if (originalJumpChain[i] != currentJumpChain[i]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (jumpChainChanged) {
|
||||||
|
final ok = await context.showRoundDialog<bool>(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: Text(libL10n.askContinue('${l10n.jumpServer} ${libL10n.setting}')),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
if (ok != true) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||||
final ok = await context.showRoundDialog<bool>(
|
final ok = await context.showRoundDialog<bool>(
|
||||||
title: libL10n.attention,
|
title: libL10n.attention,
|
||||||
@@ -277,7 +301,8 @@ extension _Actions on _ServerEditPageState {
|
|||||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||||
autoConnect: _autoConnect.value,
|
autoConnect: _autoConnect.value,
|
||||||
jumpId: _jumpServer.value,
|
jumpId: null,
|
||||||
|
jumpChainIds: _jumpChain.value.isEmpty ? null : _jumpChain.value,
|
||||||
custom: custom,
|
custom: custom,
|
||||||
wolCfg: wol,
|
wolCfg: wol,
|
||||||
envs: _env.value.isEmpty ? null : _env.value,
|
envs: _env.value.isEmpty ? null : _env.value,
|
||||||
@@ -421,7 +446,7 @@ extension _Utils on _ServerEditPageState {
|
|||||||
|
|
||||||
_altUrlController.text = spi.alterUrl ?? '';
|
_altUrlController.text = spi.alterUrl ?? '';
|
||||||
_autoConnect.value = spi.autoConnect;
|
_autoConnect.value = spi.autoConnect;
|
||||||
_jumpServer.value = spi.jumpId;
|
_jumpChain.value = spi.jumpChainIds ?? (spi.jumpId == null ? const <String>[] : [spi.jumpId!]);
|
||||||
|
|
||||||
final custom = spi.custom;
|
final custom = spi.custom;
|
||||||
if (custom != null) {
|
if (custom != null) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:server_box/view/page/private_key/edit.dart';
|
|||||||
import 'package:server_box/view/page/server/discovery/discovery.dart';
|
import 'package:server_box/view/page/server/discovery/discovery.dart';
|
||||||
|
|
||||||
part 'actions.dart';
|
part 'actions.dart';
|
||||||
|
part 'jump_chain.dart';
|
||||||
part 'widget.dart';
|
part 'widget.dart';
|
||||||
|
|
||||||
class ServerEditPage extends ConsumerStatefulWidget {
|
class ServerEditPage extends ConsumerStatefulWidget {
|
||||||
@@ -66,7 +67,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
/// -1: non selected, null: password, others: index of private key
|
/// -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 _jumpChain = <String>[].vn;
|
||||||
final _pveIgnoreCert = ValueNotifier(false);
|
final _pveIgnoreCert = ValueNotifier(false);
|
||||||
final _env = <String, String>{}.vn;
|
final _env = <String, String>{}.vn;
|
||||||
final _customCmds = <String, String>{}.vn;
|
final _customCmds = <String, String>{}.vn;
|
||||||
@@ -100,7 +101,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
|
|
||||||
_keyIdx.dispose();
|
_keyIdx.dispose();
|
||||||
_autoConnect.dispose();
|
_autoConnect.dispose();
|
||||||
_jumpServer.dispose();
|
_jumpChain.dispose();
|
||||||
_pveIgnoreCert.dispose();
|
_pveIgnoreCert.dispose();
|
||||||
_env.dispose();
|
_env.dispose();
|
||||||
_customCmds.dispose();
|
_customCmds.dispose();
|
||||||
@@ -199,7 +200,6 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
),
|
),
|
||||||
_buildAuth(),
|
_buildAuth(),
|
||||||
_buildSystemType(),
|
_buildSystemType(),
|
||||||
_buildJumpServer(),
|
|
||||||
_buildMore(),
|
_buildMore(),
|
||||||
];
|
];
|
||||||
return AutoMultiList(children: children);
|
return AutoMultiList(children: children);
|
||||||
|
|||||||
176
lib/view/page/server/edit/jump_chain.dart
Normal file
176
lib/view/page/server/edit/jump_chain.dart
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
part of 'edit.dart';
|
||||||
|
|
||||||
|
extension _JumpChain on _ServerEditPageState {
|
||||||
|
Widget _buildJumpChain() {
|
||||||
|
final serversState = ref.watch(serversProvider);
|
||||||
|
final servers = serversState.servers;
|
||||||
|
final selfId = spi?.id;
|
||||||
|
|
||||||
|
if (selfId == null) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.map),
|
||||||
|
title: Text(l10n.jumpServer),
|
||||||
|
subtitle: Text(libL10n.empty, style: UIs.textGrey),
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
String serverNameOrId(String id) {
|
||||||
|
return servers[id]?.name ?? id;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> flattenHopIds(String id, {required Set<String> visited}) {
|
||||||
|
if (!visited.add(id)) return const <String>[];
|
||||||
|
final spi = servers[id];
|
||||||
|
if (spi == null) return const <String>[];
|
||||||
|
|
||||||
|
final hops = spi.jumpChainIds;
|
||||||
|
if (hops == null || hops.isEmpty) return const <String>[];
|
||||||
|
|
||||||
|
final flat = <String>[];
|
||||||
|
for (final hopId in hops) {
|
||||||
|
flat.add(hopId);
|
||||||
|
flat.addAll(flattenHopIds(hopId, visited: visited));
|
||||||
|
}
|
||||||
|
return flat;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsCycleWithCandidate(String candidateId) {
|
||||||
|
final queue = [..._jumpChain.value, candidateId];
|
||||||
|
|
||||||
|
final directVisited = <String>{selfId};
|
||||||
|
for (final hopId in queue) {
|
||||||
|
if (hopId == selfId) return true;
|
||||||
|
if (!directVisited.add(hopId)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final hopId in queue) {
|
||||||
|
final extra = flattenHopIds(hopId, visited: <String>{selfId});
|
||||||
|
for (final id in extra) {
|
||||||
|
if (id == selfId) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? buildTextNearToFar() {
|
||||||
|
if (_jumpChain.value.isEmpty) return null;
|
||||||
|
final flat = <String>[];
|
||||||
|
final visited = <String>{selfId};
|
||||||
|
for (final hopId in _jumpChain.value) {
|
||||||
|
flat.add(hopId);
|
||||||
|
flat.addAll(flattenHopIds(hopId, visited: visited));
|
||||||
|
}
|
||||||
|
final names = flat.map(serverNameOrId).toList();
|
||||||
|
if (names.isEmpty) return null;
|
||||||
|
return names.join(' → ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? buildTextFarToNear() {
|
||||||
|
final text = buildTextNearToFar();
|
||||||
|
if (text == null) return null;
|
||||||
|
return text.split(' → ').reversed.join(' → ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _jumpChain.listenVal((_) {
|
||||||
|
final nearToFar2 = buildTextNearToFar();
|
||||||
|
final farToNear2 = buildTextFarToNear();
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.map),
|
||||||
|
title: Text(l10n.jumpServer),
|
||||||
|
subtitle: (nearToFar2 == null)
|
||||||
|
? Text(libL10n.empty, style: UIs.textGrey)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('${l10n.route}: $nearToFar2', style: UIs.textGrey),
|
||||||
|
Text('${libL10n.path}: $farToNear2', style: UIs.textGrey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: () async {
|
||||||
|
if (serversState.serverOrder.isEmpty) {
|
||||||
|
context.showSnackBar(libL10n.empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final candidates = serversState.serverOrder.where((e) => e != selfId).toList();
|
||||||
|
if (candidates.isEmpty) {
|
||||||
|
context.showSnackBar(libL10n.empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a hop
|
||||||
|
final nextHop = await context.showPickSingleDialog<String>(
|
||||||
|
title: '${l10n.jumpServer} (+1)',
|
||||||
|
items: candidates.where((id) => !containsCycleWithCandidate(id)).toList(),
|
||||||
|
display: serverNameOrId,
|
||||||
|
clearable: true,
|
||||||
|
);
|
||||||
|
if (nextHop == null) return;
|
||||||
|
|
||||||
|
_jumpChain.value = [..._jumpChain.value, nextHop];
|
||||||
|
|
||||||
|
// If user wants to manage order/remove, offer a simple editor dialog
|
||||||
|
await context.showRoundDialog<void>(
|
||||||
|
title: l10n.jumpServer,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 320,
|
||||||
|
child: _jumpChain.listenVal((hops) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: hops.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final id = hops[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(serverNameOrId(id)),
|
||||||
|
subtitle: Text(id, style: UIs.textGrey),
|
||||||
|
trailing: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_upward, size: 18),
|
||||||
|
onPressed: index == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final list = [..._jumpChain.value];
|
||||||
|
final tmp = list[index - 1];
|
||||||
|
list[index - 1] = list[index];
|
||||||
|
list[index] = tmp;
|
||||||
|
_jumpChain.value = list;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_downward, size: 18),
|
||||||
|
onPressed: index == hops.length - 1
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final list = [..._jumpChain.value];
|
||||||
|
final tmp = list[index + 1];
|
||||||
|
list[index + 1] = list[index];
|
||||||
|
list[index] = tmp;
|
||||||
|
_jumpChain.value = list;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete, size: 18),
|
||||||
|
onPressed: () {
|
||||||
|
final list = [..._jumpChain.value]..removeAt(index);
|
||||||
|
_jumpChain.value = list;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
actions: Btnx.oks,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).cardx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,6 +132,7 @@ extension _Widgets on _ServerEditPageState {
|
|||||||
return ExpandTile(
|
return ExpandTile(
|
||||||
title: Text(l10n.more),
|
title: Text(l10n.more),
|
||||||
children: [
|
children: [
|
||||||
|
_buildJumpChain(),
|
||||||
Input(
|
Input(
|
||||||
controller: _logoUrlCtrl,
|
controller: _logoUrlCtrl,
|
||||||
type: TextInputType.url,
|
type: TextInputType.url,
|
||||||
@@ -347,48 +348,6 @@ extension _Widgets on _ServerEditPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildJumpServer() {
|
|
||||||
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
|
|
||||||
final srvs = ref
|
|
||||||
.watch(serversProvider)
|
|
||||||
.servers
|
|
||||||
.values
|
|
||||||
.where((e) => e.jumpId == null)
|
|
||||||
.where((e) => e.id != spi?.id)
|
|
||||||
.toList();
|
|
||||||
final choice = _jumpServer.listenVal((val) {
|
|
||||||
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
|
|
||||||
return Choice<Spi>(
|
|
||||||
multiple: false,
|
|
||||||
clearable: true,
|
|
||||||
value: srv != null ? [srv] : [],
|
|
||||||
builder: (state, _) => Wrap(
|
|
||||||
children: List<Widget>.generate(srvs.length, (index) {
|
|
||||||
final item = srvs[index];
|
|
||||||
return ChoiceChipX<Spi>(
|
|
||||||
label: item.name,
|
|
||||||
state: state,
|
|
||||||
value: item,
|
|
||||||
onSelected: (srv, on) {
|
|
||||||
if (on) {
|
|
||||||
_jumpServer.value = srv.id;
|
|
||||||
} else {
|
|
||||||
_jumpServer.value = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return ExpandTile(
|
|
||||||
leading: const Icon(Icons.map),
|
|
||||||
initiallyExpanded: _jumpServer.value != null,
|
|
||||||
childrenPadding: padding,
|
|
||||||
title: Text(l10n.jumpServer),
|
|
||||||
children: [choice],
|
|
||||||
).cardx;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildWriteScriptTip() {
|
Widget _buildWriteScriptTip() {
|
||||||
return Btn.tile(
|
return Btn.tile(
|
||||||
|
|||||||
@@ -346,6 +346,6 @@ class _ServerPageState extends ConsumerState<ServerPage>
|
|||||||
|
|
||||||
static const _kCardHeightMin = 23.0;
|
static const _kCardHeightMin = 23.0;
|
||||||
static const _kCardHeightFlip = 99.0;
|
static const _kCardHeightFlip = 99.0;
|
||||||
static const _kCardHeightNormal = 108.0;
|
static const _kCardHeightNormal = 110.0;
|
||||||
static const _kCardHeightMoveOutFuncs = 135.0;
|
static const _kCardHeightMoveOutFuncs = 135.0;
|
||||||
}
|
}
|
||||||
|
|||||||
95
lib/view/page/setting/entries/ai.dart
Normal file
95
lib/view/page/setting/entries/ai.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
part of '../entry.dart';
|
||||||
|
|
||||||
|
extension _AI on _AppSettingsPageState {
|
||||||
|
Widget _buildAskAiConfig() {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(LineAwesome.robot_solid, size: _kIconSize),
|
||||||
|
title: TipText(l10n.askAi, l10n.askAiUsageHint),
|
||||||
|
children: [
|
||||||
|
_setting.askAiBaseUrl.listenable().listenVal((val) {
|
||||||
|
final display = val.isEmpty ? libL10n.empty : val;
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(MingCute.link_2_line),
|
||||||
|
title: Text(l10n.askAiBaseUrl),
|
||||||
|
subtitle: Text(display, style: UIs.textGrey, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||||
|
onTap: () => _showAskAiFieldDialog(
|
||||||
|
prop: _setting.askAiBaseUrl,
|
||||||
|
title: l10n.askAiBaseUrl,
|
||||||
|
hint: 'https://api.openai.com',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_setting.askAiModel.listenable().listenVal((val) {
|
||||||
|
final display = val.isEmpty ? libL10n.empty : val;
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.view_module),
|
||||||
|
title: Text(l10n.askAiModel),
|
||||||
|
subtitle: Text(display, style: UIs.textGrey),
|
||||||
|
onTap: () => _showAskAiFieldDialog(
|
||||||
|
prop: _setting.askAiModel,
|
||||||
|
title: l10n.askAiModel,
|
||||||
|
hint: 'gpt-4o-mini',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_setting.askAiApiKey.listenable().listenVal((val) {
|
||||||
|
final hasKey = val.isNotEmpty;
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(MingCute.key_2_line),
|
||||||
|
title: Text(l10n.askAiApiKey),
|
||||||
|
subtitle: Text(hasKey ? '••••••••' : libL10n.empty, style: UIs.textGrey),
|
||||||
|
onTap: () => _showAskAiFieldDialog(
|
||||||
|
prop: _setting.askAiApiKey,
|
||||||
|
title: l10n.askAiApiKey,
|
||||||
|
hint: 'sk-...',
|
||||||
|
obscure: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> _showAskAiFieldDialog({
|
||||||
|
required HiveProp<String> prop,
|
||||||
|
required String title,
|
||||||
|
required String hint,
|
||||||
|
bool obscure = false,
|
||||||
|
}) async {
|
||||||
|
return withTextFieldController((ctrl) async {
|
||||||
|
final fetched = prop.fetch();
|
||||||
|
if (fetched != null && fetched.isNotEmpty) ctrl.text = fetched;
|
||||||
|
|
||||||
|
void onSave() {
|
||||||
|
prop.put(ctrl.text.trim());
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.showRoundDialog(
|
||||||
|
title: title,
|
||||||
|
child: Input(
|
||||||
|
controller: ctrl,
|
||||||
|
autoFocus: true,
|
||||||
|
label: title,
|
||||||
|
hint: hint,
|
||||||
|
icon: obscure ? MingCute.key_2_line : Icons.edit,
|
||||||
|
obscureText: obscure,
|
||||||
|
suggestion: !obscure,
|
||||||
|
onSubmitted: (_) => onSave(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
prop.delete();
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: Text(libL10n.clear),
|
||||||
|
),
|
||||||
|
TextButton(onPressed: onSave, child: Text(libL10n.ok)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,49 +92,73 @@ extension _App on _AppSettingsPageState {
|
|||||||
trailing: _setting.colorSeed.listenable().listenVal((_) {
|
trailing: _setting.colorSeed.listenable().listenVal((_) {
|
||||||
return ClipOval(child: Container(color: UIs.primaryColor, height: 27, width: 27));
|
return ClipOval(child: Container(color: UIs.primaryColor, height: 27, width: 27));
|
||||||
}),
|
}),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final ctrl = TextEditingController(text: UIs.primaryColor.toHex);
|
withTextFieldController((ctrl) async {
|
||||||
await context.showRoundDialog(
|
ctrl.text = Color(_setting.colorSeed.fetch()).toHex;
|
||||||
title: libL10n.primaryColorSeed,
|
await context.showRoundDialog(
|
||||||
child: StatefulBuilder(
|
title: libL10n.primaryColorSeed,
|
||||||
builder: (context, setState) {
|
child: StatefulBuilder(
|
||||||
final children = <Widget>[
|
builder: (context, setState) {
|
||||||
/// Plugin [dynamic_color] is not supported on iOS
|
final children = <Widget>[
|
||||||
if (!isIOS)
|
if (!isIOS)
|
||||||
ListTile(
|
DynamicColorBuilder(
|
||||||
title: Text(l10n.followSystem),
|
builder: (light, dark) {
|
||||||
trailing: StoreSwitch(
|
final supported = light != null || dark != null;
|
||||||
prop: _setting.useSystemPrimaryColor,
|
if (!supported) {
|
||||||
callback: (_) => setState(() {}),
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
|
_setting.useSystemPrimaryColor.put(false);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
title: Text(l10n.followSystem),
|
||||||
|
trailing: StoreSwitch(
|
||||||
|
prop: _setting.useSystemPrimaryColor,
|
||||||
|
callback: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
];
|
||||||
];
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
if (!_setting.useSystemPrimaryColor.fetch()) {
|
children.add(
|
||||||
children.add(
|
ColorPicker(
|
||||||
ColorPicker(
|
color: Color(_setting.colorSeed.fetch()),
|
||||||
color: Color(_setting.colorSeed.fetch()),
|
onColorChanged: (c) => ctrl.text = c.toHex,
|
||||||
onColorChanged: (c) => ctrl.text = c.toHex,
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
||||||
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
},
|
||||||
},
|
),
|
||||||
),
|
actions: Btn.ok(onTap: () => _onSaveColor(ctrl.text)).toList,
|
||||||
actions: Btn.ok(onTap: () => _onSaveColor(ctrl.text)).toList,
|
);
|
||||||
);
|
});
|
||||||
ctrl.dispose();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSaveColor(String s) {
|
void _onSaveColor(String s) {
|
||||||
final color = s.fromColorHex;
|
final color = s.fromColorHex;
|
||||||
|
|
||||||
if (color == null) {
|
if (color == null) {
|
||||||
context.showSnackBar(libL10n.fail);
|
context.showSnackBar(libL10n.fail);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UIs.colorSeed = color;
|
|
||||||
|
// Save the color seed to settings
|
||||||
_setting.colorSeed.put(color.value255);
|
_setting.colorSeed.put(color.value255);
|
||||||
|
|
||||||
|
// Only update UIs colors if we're not in system mode
|
||||||
|
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||||
|
UIs.primaryColor = color;
|
||||||
|
UIs.colorSeed = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
RNodes.app.notify();
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,4 +308,122 @@ extension _App on _AppSettingsPageState {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEditRawSettings() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('(Dev) Edit raw json'),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: _editRawSettings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _editRawSettings() async {
|
||||||
|
final rawMap = Stores.setting.getAllMap(includeInternalKeys: true);
|
||||||
|
final map = Map<String, Object?>.from(rawMap);
|
||||||
|
final initialKeys = Set<String>.from(map.keys);
|
||||||
|
Map<String, Object?> mapForEditor = map;
|
||||||
|
String? encryptedKey;
|
||||||
|
String? passwordUsed;
|
||||||
|
|
||||||
|
Future<String?> resolvePassword() async {
|
||||||
|
final saved = await _setting.backupasswd.read();
|
||||||
|
if (saved?.isNotEmpty == true) return saved;
|
||||||
|
final backupPwd = await SecureStoreProps.bakPwd.read();
|
||||||
|
if (backupPwd?.isNotEmpty == true) return backupPwd;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
try {
|
||||||
|
final result = await context.showRoundDialog<String>(
|
||||||
|
title: libL10n.pwd,
|
||||||
|
child: Input(
|
||||||
|
controller: controller,
|
||||||
|
label: libL10n.pwd,
|
||||||
|
obscureText: true,
|
||||||
|
onSubmitted: (_) => context.pop(controller.text.trim()),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => context.pop(null), child: Text(libL10n.cancel)),
|
||||||
|
TextButton(onPressed: () => context.pop(controller.text.trim()), child: Text(libL10n.ok)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return result?.trim();
|
||||||
|
} finally {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in map.entries) {
|
||||||
|
final value = entry.value;
|
||||||
|
if (value is String && Cryptor.isEncrypted(value)) {
|
||||||
|
final password = await resolvePassword();
|
||||||
|
if (password == null || password.isEmpty) {
|
||||||
|
context.showSnackBar(libL10n.cancel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final decrypted = Cryptor.decrypt(value, password);
|
||||||
|
final decoded = json.decode(decrypted);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
mapForEditor = Map<String, Object?>.from(decoded);
|
||||||
|
encryptedKey = entry.key;
|
||||||
|
passwordUsed = password;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
final msg = e.toString().contains('Failed to decrypt') || e.toString().contains('incorrect password')
|
||||||
|
? l10n.backupPasswordWrong
|
||||||
|
: '${libL10n.error}:\n$e';
|
||||||
|
context.showRoundDialog(title: libL10n.fail, child: Text(msg));
|
||||||
|
Loggers.app.warning('Decrypt raw settings failed', e, stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSave(EditorPageRet ret) {
|
||||||
|
if (ret.typ != EditorPageRetType.text) {
|
||||||
|
context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final newSettings = json.decode(ret.val) as Map<String, dynamic>;
|
||||||
|
if (encryptedKey != null) {
|
||||||
|
final pwd = passwordUsed;
|
||||||
|
if (pwd == null || pwd.isEmpty) {
|
||||||
|
context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final encrypted = Cryptor.encrypt(json.encode(newSettings), pwd);
|
||||||
|
Stores.setting.box.put(encryptedKey, encrypted);
|
||||||
|
} else {
|
||||||
|
Stores.setting.box.putAll(newSettings);
|
||||||
|
final newKeys = newSettings.keys.toSet();
|
||||||
|
final removedKeys = initialKeys.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode [map] to String with indent `\t`
|
||||||
|
final text = jsonIndentEncoder.convert(mapForEditor);
|
||||||
|
await EditorPage.route.go(
|
||||||
|
context,
|
||||||
|
args: EditorPageArgs(
|
||||||
|
text: text,
|
||||||
|
lang: ProgLang.json,
|
||||||
|
title: libL10n.setting,
|
||||||
|
onSave: onSave,
|
||||||
|
closeAfterSave: SettingStore.instance.closeAfterSave.fetch(),
|
||||||
|
softWrap: SettingStore.instance.editorSoftWrap.fetch(),
|
||||||
|
enableHighlight: SettingStore.instance.editorHighlight.fetch(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ class _HomeTabsConfigPageState extends ConsumerState<HomeTabsConfigPage> {
|
|||||||
onTap: isSelected && canRemove ? () => _removeTab(tab) : null,
|
onTap: isSelected && canRemove ? () => _removeTab(tab) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Card(
|
return Padding(
|
||||||
key: ValueKey(tab.name),
|
key: ValueKey(tab.name),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||||
child: isSelected ? ReorderableDragStartListener(index: index, child: child) : child,
|
child: (isSelected ? ReorderableDragStartListener(index: index, child: child) : child).cardx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,53 +207,6 @@ extension _Server on _AppSettingsPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEditRawSettings() {
|
|
||||||
return ListTile(
|
|
||||||
title: const Text('(Dev) Edit raw json'),
|
|
||||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
|
||||||
onTap: _editRawSettings,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _editRawSettings() async {
|
|
||||||
final map = Stores.setting.getAllMap(includeInternalKeys: true);
|
|
||||||
final keys = map.keys;
|
|
||||||
|
|
||||||
void onSave(EditorPageRet ret) {
|
|
||||||
if (ret.typ != EditorPageRetType.text) {
|
|
||||||
context.showRoundDialog(title: libL10n.fail, child: Text(l10n.invalid));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final newSettings = json.decode(ret.val) 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode [map] to String with indent `\t`
|
|
||||||
final text = jsonIndentEncoder.convert(map);
|
|
||||||
await EditorPage.route.go(
|
|
||||||
context,
|
|
||||||
args: EditorPageArgs(
|
|
||||||
text: text,
|
|
||||||
lang: ProgLang.json,
|
|
||||||
title: libL10n.setting,
|
|
||||||
onSave: onSave,
|
|
||||||
closeAfterSave: SettingStore.instance.closeAfterSave.fetch(),
|
|
||||||
softWrap: SettingStore.instance.editorSoftWrap.fetch(),
|
|
||||||
enableHighlight: SettingStore.instance.editorHighlight.fetch(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCpuView() {
|
Widget _buildCpuView() {
|
||||||
return ExpandTile(
|
return ExpandTile(
|
||||||
leading: const Icon(OctIcons.cpu, size: _kIconSize),
|
leading: const Icon(OctIcons.cpu, size: _kIconSize),
|
||||||
|
|||||||
@@ -44,28 +44,28 @@ extension _SFTP on _AppSettingsPageState {
|
|||||||
leading: const Icon(MingCute.edit_fill),
|
leading: const Icon(MingCute.edit_fill),
|
||||||
title: TipText(libL10n.editor, l10n.sftpEditorTip),
|
title: TipText(libL10n.editor, l10n.sftpEditorTip),
|
||||||
trailing: Text(val.isEmpty ? l10n.inner : val, style: UIs.text15),
|
trailing: Text(val.isEmpty ? l10n.inner : val, style: UIs.text15),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final ctrl = TextEditingController(text: val);
|
withTextFieldController((ctrl) async {
|
||||||
void onSave() {
|
void onSave() {
|
||||||
final s = ctrl.text.trim();
|
final s = ctrl.text.trim();
|
||||||
_setting.sftpEditor.put(s);
|
_setting.sftpEditor.put(s);
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.showRoundDialog<bool>(
|
await context.showRoundDialog<bool>(
|
||||||
title: libL10n.select,
|
title: libL10n.select,
|
||||||
child: Input(
|
child: Input(
|
||||||
controller: ctrl,
|
controller: ctrl,
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
label: libL10n.editor,
|
label: libL10n.editor,
|
||||||
hint: '\$EDITOR / vim / nano ...',
|
hint: '\$EDITOR / vim / nano ...',
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
suggestion: false,
|
suggestion: false,
|
||||||
onSubmitted: (_) => onSave(),
|
onSubmitted: (_) => onSave(),
|
||||||
),
|
),
|
||||||
actions: Btn.ok(onTap: onSave).toList,
|
actions: Btn.ok(onTap: onSave).toList,
|
||||||
);
|
);
|
||||||
ctrl.dispose();
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,27 +116,28 @@ extension _SSH on _AppSettingsPageState {
|
|||||||
leading: const Icon(Icons.terminal),
|
leading: const Icon(Icons.terminal),
|
||||||
title: TipText(l10n.terminal, l10n.desktopTerminalTip),
|
title: TipText(l10n.terminal, l10n.desktopTerminalTip),
|
||||||
trailing: Text(val, style: UIs.text15, maxLines: 1, overflow: TextOverflow.ellipsis),
|
trailing: Text(val, style: UIs.text15, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
final ctrl = TextEditingController(text: val);
|
withTextFieldController((ctrl) async {
|
||||||
void onSave() {
|
ctrl.text = val;
|
||||||
_setting.desktopTerminal.put(ctrl.text.trim());
|
void onSave() {
|
||||||
context.pop();
|
_setting.desktopTerminal.put(ctrl.text.trim());
|
||||||
}
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
await context.showRoundDialog<bool>(
|
await context.showRoundDialog<bool>(
|
||||||
title: libL10n.select,
|
title: libL10n.select,
|
||||||
child: Input(
|
child: Input(
|
||||||
controller: ctrl,
|
controller: ctrl,
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
label: l10n.terminal,
|
label: l10n.terminal,
|
||||||
hint: 'x-terminal-emulator / gnome-terminal',
|
hint: 'x-terminal-emulator / gnome-terminal',
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
suggestion: false,
|
suggestion: false,
|
||||||
onSubmitted: (_) => onSave(),
|
onSubmitted: (_) => onSave(),
|
||||||
),
|
),
|
||||||
actions: Btn.ok(onTap: onSave).toList,
|
actions: Btn.ok(onTap: onSave).toList,
|
||||||
);
|
);
|
||||||
ctrl.dispose();
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user