mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-16 05:05:39 +01:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
860c11d4a8 | ||
|
|
bd949288ed | ||
|
|
bb3e3b4848 | ||
|
|
3307fca620 | ||
|
|
da8517bcf7 | ||
|
|
f68c4a851b | ||
|
|
17db393c12 | ||
|
|
275581cfa3 | ||
|
|
d7168ea1ff | ||
|
|
fd2bf08f78 | ||
|
|
98e13c39cf | ||
|
|
e70abeef04 | ||
|
|
194774d6fb | ||
|
|
640d61bab9 | ||
|
|
7f4cf22cc9 | ||
|
|
05a927753f | ||
|
|
0c7b72fb2c | ||
|
|
a869b97502 | ||
|
|
eadd343205 | ||
|
|
1bac986fe0 | ||
|
|
a94be6c2c3 | ||
|
|
fc8e9b4bb1 | ||
|
|
ec4b633889 | ||
|
|
e51804fa70 | ||
|
|
2466341999 | ||
|
|
929061213f | ||
|
|
6b52679942 | ||
|
|
efc0315c93 | ||
|
|
8e4c2a7cde | ||
|
|
4ec7f5895e | ||
|
|
ee22cdb55f | ||
|
|
b1b0d9a18f | ||
|
|
56e67f4725 | ||
|
|
3b7fdf36fb | ||
|
|
5291d316a2 | ||
|
|
4c369546da | ||
|
|
12a243d139 | ||
|
|
a97b3cf43e | ||
|
|
53a7c0d8ff | ||
|
|
9cb705f8dd | ||
|
|
8270674b7d | ||
|
|
24fd4b782d |
11
.github/workflows/analysis.yml
vendored
11
.github/workflows/analysis.yml
vendored
@@ -16,18 +16,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable' # or: 'beta', 'dev' or 'master'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
# Uncomment this step to verify the use of 'dart format' on each commit.
|
||||
- name: Verify formatting
|
||||
run: dart format --output=none .
|
||||
|
||||
# Consider passing '--fatal-infos' for slightly stricter analysis.
|
||||
- name: Analyze project source
|
||||
run: dart analyze
|
||||
|
||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -9,18 +9,23 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
# Set by fl_build
|
||||
# env:
|
||||
# APP_NAME: ServerBox
|
||||
# BUILD_NUMBER: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
releaseAndroid:
|
||||
name: Release android
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.35.1"
|
||||
flutter-version: "3.38.0"
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
@@ -48,10 +53,10 @@ jobs:
|
||||
|
||||
releaseLinux:
|
||||
name: Release linux
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Install dependencies
|
||||
@@ -77,7 +82,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
@@ -95,19 +100,15 @@ jobs:
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# uses: actions/checkout@v6
|
||||
# - name: Install Flutter
|
||||
# uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# channel: 'stable'
|
||||
# flutter-version: '3.32.1'
|
||||
# - name: Build
|
||||
# run: dart run fl_build -p ios,mac
|
||||
# run: dart run fl_build -p ios
|
||||
# - name: Create Release
|
||||
# uses: softprops/action-gh-release@v2
|
||||
# with:
|
||||
# files: |
|
||||
# ${{ env.APP_NAME }}_universal_macos.zip
|
||||
# ${{ env.APP_NAME }}_universal.ipa
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
95
CLAUDE.md
Normal file
95
CLAUDE.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
|
||||
- `flutter run` - Run the app in development mode
|
||||
- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)
|
||||
- `dart run build_runner build --delete-conflicting-outputs` - Generate code for models with annotations (json_serializable, freezed, hive, riverpod)
|
||||
- Every time you change model files, run this command to regenerate code (Hive adapters, Riverpod providers, etc.)
|
||||
- Generated files include: `*.g.dart`, `*.freezed.dart` files
|
||||
|
||||
### Testing
|
||||
|
||||
- `flutter test` - Run unit tests
|
||||
- `flutter test test/battery_test.dart` - Run specific test file
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a Flutter application for managing Linux servers with the following key architectural components:
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `lib/core/` - Core utilities, extensions, and routing
|
||||
- `lib/data/` - Data layer with models, providers, and storage
|
||||
- `model/` - Data models organized by feature (server, container, ssh, etc.)
|
||||
- `provider/` - Riverpod providers for state management
|
||||
- `store/` - Local storage implementations using Hive
|
||||
- `lib/view/` - UI layer with pages and widgets
|
||||
- `lib/generated/` - Generated localization files
|
||||
- `lib/hive/` - Hive adapters for local storage
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **State Management**: Riverpod with code generation (riverpod_annotation)
|
||||
- **Local Storage**: Hive for persistent data with generated adapters
|
||||
- **SSH/SFTP**: Custom dartssh2 fork for server connections
|
||||
- **Terminal**: Custom xterm.dart fork for SSH terminal interface
|
||||
- **Networking**: dio for HTTP requests
|
||||
- **Charts**: fl_chart for server status visualization
|
||||
- **Localization**: Flutter's built-in i18n with ARB files
|
||||
- **Code Generation**: Uses build_runner with json_serializable, freezed, hive_generator, riverpod_generator
|
||||
|
||||
### Data Models
|
||||
|
||||
- Server management models in `lib/data/model/server/`
|
||||
- Container/Docker models in `lib/data/model/container/`
|
||||
- SSH and SFTP models in respective directories
|
||||
- Most models use freezed for immutability and json_annotation for serialization
|
||||
|
||||
### Features
|
||||
|
||||
- Server status monitoring (CPU, memory, disk, network)
|
||||
- SSH terminal with virtual keyboard
|
||||
- SFTP file browser
|
||||
- Docker container management
|
||||
- Process and systemd service management
|
||||
- Server snippets and custom commands
|
||||
- Multi-language support (12+ languages)
|
||||
- Cross-platform support (iOS, Android, macOS, Linux, Windows)
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
- Uses Riverpod providers for dependency injection and state management
|
||||
- Uses Freezed for immutable state models
|
||||
- Providers are organized by feature in `lib/data/provider/`
|
||||
- State is often persisted using Hive stores in `lib/data/store/`
|
||||
|
||||
### Build System
|
||||
|
||||
- Uses custom `fl_build` package for cross-platform building
|
||||
- `make.dart` script handles pre/post build tasks (metadata generation)
|
||||
- Supports building for multiple platforms with platform-specific configurations
|
||||
- Many dependencies are custom forks hosted on GitHub (dartssh2, xterm, fl_lib, etc.)
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Never run code formatting commands** - The codebase has specific formatting that should not be changed
|
||||
- **Always run code generation** after modifying models with annotations (freezed, json_serializable, hive, riverpod)
|
||||
- Generated files (`*.g.dart`, `*.freezed.dart`) should not be manually edited
|
||||
- AGAIN, NEVER run code formatting commands.
|
||||
- USE dependency injection via GetIt for services like Stores, Services and etc.
|
||||
- Generate all l10n files using `flutter gen-l10n` command after modifying ARB files.
|
||||
- USE `hive_ce` not `hive` package for Hive integration.
|
||||
- Which no need to config `HiveField` and `HiveType` manually.
|
||||
- USE widgets and utilities from `fl_lib` package for common functionalities.
|
||||
- Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc.
|
||||
- You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities.
|
||||
- USE `libL10n` and `l10n` for localization strings.
|
||||
- `libL10n` is from `fl_lib` package, and `l10n` is from this project.
|
||||
- Before adding new strings, check if it already exists in `libL10n`.
|
||||
- Prioritize using strings from `libL10n` to avoid duplication, even if the meaning is not 100% exact, just use the substitution of `libL10n`.
|
||||
- Split UI into Widget build, Actions, Utils. use `extension on` to achieve this
|
||||
143
LICENSE
143
LICENSE
@@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
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
|
||||
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
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
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.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
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
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
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:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
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
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
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.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
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
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
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
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
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.
|
||||
|
||||
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
|
||||
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>
|
||||
|
||||
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
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
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".
|
||||
If your software can interact with users remotely through a computer
|
||||
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
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
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.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<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>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -5,7 +5,7 @@ English | [简体中文](README_zh.md)
|
||||
<div align="center">
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
@@ -85,4 +85,4 @@ If I forgot to add your name to the contributors list, please add a comment in t
|
||||
|
||||
## 📝 License
|
||||
|
||||
`GPL v3 lollipopkit`
|
||||
`AGPL v3 lollipopkit & all contributors`
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div align="center">
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
@@ -86,4 +86,4 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
||||
|
||||
## 📝 协议
|
||||
|
||||
`GPL v3 lollipopkit`
|
||||
`AGPL v3 lollipopkit & 所有贡献者`
|
||||
|
||||
@@ -113,7 +113,7 @@ android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
|
||||
if (abiVersionCode != null) {
|
||||
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
|
||||
output.versionCodeOverride = variant.versionCode * 100 + abiVersionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package tech.lolli.toolbox
|
||||
|
||||
import android.app.*
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
@@ -16,8 +18,7 @@ class ForegroundService : Service() {
|
||||
var isRunning: Boolean = false
|
||||
}
|
||||
private val chanId = "ForegroundServiceChannel"
|
||||
private val GROUP_KEY = "ssh_sessions_group"
|
||||
private val SUMMARY_ID = 1000
|
||||
private val NOTIFICATION_ID = 1000
|
||||
private val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
|
||||
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
|
||||
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
|
||||
@@ -49,19 +50,22 @@ class ForegroundService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
try {
|
||||
// Check notification permission for Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
androidx.core.content.ContextCompat.checkSelfPermission(
|
||||
this, android.Manifest.permission.POST_NOTIFICATIONS
|
||||
) != android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log.w("ForegroundService", "Notification permission denied. Stopping service.")
|
||||
stopForegroundService()
|
||||
Log.w("ForegroundService", "Notification permission denied. Stopping service gracefully.")
|
||||
// Don't call stopForegroundService() here as we haven't started foreground yet
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (intent == null) {
|
||||
Log.w("ForegroundService", "onStartCommand called with null intent")
|
||||
stopForegroundService()
|
||||
// Don't call stopForegroundService() here as we haven't started foreground yet
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
@@ -70,6 +74,9 @@ class ForegroundService : Service() {
|
||||
|
||||
return when (action) {
|
||||
ACTION_STOP_FOREGROUND -> {
|
||||
// Notify Flutter to stop all connections before stopping service
|
||||
val stopAllIntent = Intent("tech.lolli.toolbox.STOP_ALL_CONNECTIONS")
|
||||
sendBroadcast(stopAllIntent)
|
||||
clearAll()
|
||||
stopForegroundService()
|
||||
START_NOT_STICKY
|
||||
@@ -81,7 +88,7 @@ class ForegroundService : Service() {
|
||||
}
|
||||
else -> {
|
||||
// Default bring up foreground with placeholder
|
||||
ensureForeground(createSummaryNotification(0, emptyList()))
|
||||
ensureForeground(createMergedNotification(0, emptyList(), emptyList()))
|
||||
START_STICKY
|
||||
}
|
||||
}
|
||||
@@ -99,37 +106,67 @@ class ForegroundService : Service() {
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
if (manager == null) {
|
||||
Log.e("ForegroundService", "Failed to get NotificationManager")
|
||||
return
|
||||
try {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
if (manager == null) {
|
||||
Log.e("ForegroundService", "Failed to get NotificationManager")
|
||||
return
|
||||
}
|
||||
val serviceChannel = NotificationChannel(
|
||||
chanId,
|
||||
"ForegroundServiceChannel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "For foreground service"
|
||||
}
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
Log.d("ForegroundService", "Notification channel created successfully")
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to create notification channel", e)
|
||||
}
|
||||
val serviceChannel = NotificationChannel(
|
||||
chanId,
|
||||
"ForegroundServiceChannel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "For foreground service"
|
||||
}
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureForeground(notification: Notification) {
|
||||
try {
|
||||
// Double-check notification permission before starting foreground service
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
androidx.core.content.ContextCompat.checkSelfPermission(
|
||||
this, android.Manifest.permission.POST_NOTIFICATIONS
|
||||
) != android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log.w("ForegroundService", "Cannot start foreground service without notification permission")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
|
||||
if (!isFgStarted) {
|
||||
startForeground(SUMMARY_ID, notification)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
isFgStarted = true
|
||||
Log.d("ForegroundService", "Foreground service started successfully")
|
||||
} else {
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
nm?.notify(SUMMARY_ID, notification)
|
||||
if (nm != null) {
|
||||
nm.notify(NOTIFICATION_ID, notification)
|
||||
} else {
|
||||
Log.w("ForegroundService", "NotificationManager is null, cannot update notification")
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
logError("Security exception when starting foreground service (likely missing permission)", e)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to start/update foreground", e)
|
||||
// Don't stop the service for other exceptions, just log them
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSummaryNotification(count: Int, lines: List<String>): Notification {
|
||||
|
||||
private fun createMergedNotification(count: Int, lines: List<String>, sessions: List<SessionItem>): Notification {
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
@@ -140,24 +177,66 @@ class ForegroundService : Service() {
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, chanId)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Notification.Builder(this)
|
||||
}
|
||||
|
||||
val inbox = Notification.InboxStyle()
|
||||
lines.forEach { inbox.addLine(it) }
|
||||
// Use the earliest session's start time for chronometer
|
||||
val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis()
|
||||
|
||||
return builder
|
||||
.setContentTitle("SSH sessions: $count active")
|
||||
.setContentText(if (lines.isNotEmpty()) lines.first() else "Running")
|
||||
val title = when (count) {
|
||||
0 -> "Server Box"
|
||||
1 -> sessions.first().title
|
||||
else -> "SSH sessions: $count active"
|
||||
}
|
||||
|
||||
val contentText = when (count) {
|
||||
0 -> "Ready for connections"
|
||||
1 -> {
|
||||
val session = sessions.first()
|
||||
"${session.subtitle} · ${session.status}"
|
||||
}
|
||||
else -> "Multiple SSH connections active"
|
||||
}
|
||||
|
||||
// For multiple sessions, show details in expanded view
|
||||
val style = if (count > 1) {
|
||||
val inbox = Notification.InboxStyle()
|
||||
val maxLines = 5
|
||||
val displayLines = if (lines.size > maxLines) {
|
||||
lines.take(maxLines) + "...and ${lines.size - maxLines} more"
|
||||
} else {
|
||||
lines
|
||||
}
|
||||
displayLines.forEach { inbox.addLine(it) }
|
||||
inbox.setBigContentTitle(title)
|
||||
inbox
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val notification = builder
|
||||
.setContentTitle(title)
|
||||
.setContentText(contentText)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setStyle(inbox)
|
||||
.setWhen(earliestStartTime)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setGroup(GROUP_KEY)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(android.R.drawable.ic_delete, "Stop", stopPending)
|
||||
.build()
|
||||
.addAction(
|
||||
Notification.Action.Builder(
|
||||
Icon.createWithResource(this, android.R.drawable.ic_delete),
|
||||
"Stop All",
|
||||
stopPending
|
||||
).build()
|
||||
)
|
||||
|
||||
if (style != null) {
|
||||
notification.setStyle(style)
|
||||
}
|
||||
|
||||
return notification.build()
|
||||
}
|
||||
|
||||
private fun handleUpdateSessions(payload: String) {
|
||||
@@ -192,71 +271,21 @@ class ForegroundService : Service() {
|
||||
return
|
||||
}
|
||||
|
||||
// Build per-session notifications
|
||||
val currentIds = mutableSetOf<Int>()
|
||||
val summaryLines = mutableListOf<String>()
|
||||
sessions.forEach { s ->
|
||||
// Assign a stable, collision-resistant id per session for this service lifecycle
|
||||
val nid = notificationIdMap.getOrPut(s.id) { nextNotificationId.getAndIncrement() }
|
||||
currentIds.add(nid)
|
||||
summaryLines.add("${s.title}: ${s.status}")
|
||||
|
||||
val disconnectIntent = Intent(this, MainActivity::class.java).apply {
|
||||
action = ACTION_DISCONNECT_SESSION
|
||||
putExtra("session_id", s.id)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
val disconnectPending = PendingIntent.getActivity(
|
||||
this, nid, disconnectIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, chanId)
|
||||
} else {
|
||||
Notification.Builder(this)
|
||||
}
|
||||
|
||||
val noti = builder
|
||||
.setContentTitle(s.title)
|
||||
.setContentText("${s.subtitle} · ${s.status}")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setWhen(s.startWhen)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setGroup(GROUP_KEY)
|
||||
.addAction(android.R.drawable.ic_media_pause, "Disconnect", disconnectPending)
|
||||
.build()
|
||||
|
||||
nm.notify(nid, noti)
|
||||
}
|
||||
|
||||
// Cancel stale ones
|
||||
val toCancel = postedIds - currentIds
|
||||
// Cancel any existing individual notifications (we only show merged notification now)
|
||||
val toCancel = postedIds.toSet()
|
||||
toCancel.forEach { nm.cancel(it) }
|
||||
// Clean up id mappings for canceled notifications to prevent growth
|
||||
if (toCancel.isNotEmpty()) {
|
||||
val keysToRemove = notificationIdMap.filterValues { it in toCancel }.keys
|
||||
keysToRemove.forEach { notificationIdMap.remove(it) }
|
||||
}
|
||||
postedIds.clear()
|
||||
postedIds.addAll(currentIds)
|
||||
notificationIdMap.clear()
|
||||
|
||||
// Post/update summary and ensure foreground
|
||||
val maxSummaryLines = 5
|
||||
val truncated = summaryLines.size > maxSummaryLines
|
||||
val displaySummaryLines = if (truncated) {
|
||||
summaryLines.take(maxSummaryLines) + "...and ${summaryLines.size - maxSummaryLines} more"
|
||||
} else {
|
||||
summaryLines
|
||||
}
|
||||
val summary = createSummaryNotification(sessions.size, displaySummaryLines)
|
||||
ensureForeground(summary)
|
||||
// Create merged notification content
|
||||
val summaryLines = sessions.map { "${it.title}: ${it.status}" }
|
||||
val mergedNotification = createMergedNotification(sessions.size, summaryLines, sessions)
|
||||
ensureForeground(mergedNotification)
|
||||
}
|
||||
|
||||
private fun clearAll() {
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
nm?.cancel(SUMMARY_ID)
|
||||
nm?.cancel(NOTIFICATION_ID)
|
||||
postedIds.forEach { id -> nm?.cancel(id) }
|
||||
postedIds.clear()
|
||||
isFgStarted = false
|
||||
@@ -272,7 +301,10 @@ class ForegroundService : Service() {
|
||||
|
||||
private fun stopForegroundService() {
|
||||
try {
|
||||
stopForeground(true)
|
||||
if (isFgStarted) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
isFgStarted = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError("Error stopping foreground", e)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
@@ -16,6 +19,8 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
private lateinit var channel: MethodChannel
|
||||
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
|
||||
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
|
||||
private val ACTION_STOP_ALL_CONNECTIONS = "tech.lolli.toolbox.STOP_ALL_CONNECTIONS"
|
||||
private var stopAllReceiver: BroadcastReceiver? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
@@ -92,24 +97,32 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
|
||||
// Handle intent if launched via notification action
|
||||
handleActionIntent(intent)
|
||||
|
||||
// Register broadcast receiver for stop all connections
|
||||
setupStopAllReceiver()
|
||||
}
|
||||
|
||||
private fun reqPerm() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||
|
||||
// Check if we already have the permission to avoid unnecessary prompts
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
try {
|
||||
try {
|
||||
// Check if we already have the permission to avoid unnecessary prompts
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// Check if we should show rationale
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
|
||||
android.util.Log.i("MainActivity", "User previously denied notification permission")
|
||||
}
|
||||
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
123,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// Log error but don't crash
|
||||
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log error but don't crash
|
||||
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,4 +154,52 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupStopAllReceiver() {
|
||||
stopAllReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == ACTION_STOP_ALL_CONNECTIONS && ::channel.isInitialized) {
|
||||
try {
|
||||
channel.invokeMethod("stopAllConnections", null)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to invoke stopAllConnections: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val filter = IntentFilter(ACTION_STOP_ALL_CONNECTIONS)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.registerReceiver(this, stopAllReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(stopAllReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == 123) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
android.util.Log.i("MainActivity", "Notification permission granted")
|
||||
} else {
|
||||
android.util.Log.w("MainActivity", "Notification permission denied")
|
||||
// Optionally inform user about the limitation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopAllReceiver?.let {
|
||||
try {
|
||||
unregisterReceiver(it)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to unregister receiver: ${e.message}")
|
||||
}
|
||||
stopAllReceiver = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
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:
|
||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
||||
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
|
||||
file_picker: fb04e739ae6239a76ce1f571863a196a922c87d4
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2
|
||||
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
plain_notification_token: 047876b9d80a5b93565ddcc13a487a7e7b906f7d
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
watch_connectivity: 88e5bea25b473e66ef8d3f960954d154ed0356d6
|
||||
|
||||
|
||||
@@ -748,7 +748,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -758,7 +758,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -884,7 +884,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -894,7 +894,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -912,7 +912,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||
@@ -922,7 +922,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -943,7 +943,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -956,7 +956,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
@@ -982,7 +982,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -995,7 +995,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1018,7 +1018,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1031,7 +1031,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1054,7 +1054,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1066,7 +1066,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
@@ -1095,7 +1095,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1107,7 +1107,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
@@ -1133,7 +1133,7 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1220;
|
||||
CURRENT_PROJECT_VERSION = 1291;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1145,7 +1145,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.1220;
|
||||
MARKETING_VERSION = 1.0.1291;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd;
|
||||
PRODUCT_NAME = ServerBox;
|
||||
|
||||
38
lib/app.dart
38
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:flutter/material.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/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
@@ -11,12 +12,20 @@ import 'package:server_box/view/page/home.dart';
|
||||
|
||||
part 'intro.dart';
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
late final Future<List<IntroPageBuilder>> _introFuture = _IntroPage.builders;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_setup(context);
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: RNodes.app,
|
||||
builder: (context, _) {
|
||||
@@ -31,6 +40,7 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
final colorSeed = Color(Stores.setting.colorSeed.fetch());
|
||||
|
||||
UIs.colorSeed = colorSeed;
|
||||
UIs.primaryColor = colorSeed;
|
||||
|
||||
@@ -53,12 +63,31 @@ class MyApp extends StatelessWidget {
|
||||
Widget _buildDynamicColor(BuildContext context) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (light, dark) {
|
||||
final lightTheme = ThemeData(useMaterial3: true, colorScheme: light);
|
||||
final darkTheme = ThemeData(useMaterial3: true, brightness: Brightness.dark, colorScheme: dark);
|
||||
final lightSeed = light?.primary;
|
||||
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) {
|
||||
UIs.primaryColor = dark.primary;
|
||||
UIs.colorSeed = dark.primary;
|
||||
} else if (!context.isDark && light != null) {
|
||||
UIs.primaryColor = 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);
|
||||
@@ -78,6 +107,7 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
return MaterialApp(
|
||||
key: ValueKey(locale),
|
||||
navigatorKey: AppNavigator.key,
|
||||
builder: ResponsivePoints.builder,
|
||||
locale: locale,
|
||||
localizationsDelegates: const [LibLocalizations.delegate, ...AppLocalizations.localizationsDelegates],
|
||||
@@ -89,7 +119,7 @@ class MyApp extends StatelessWidget {
|
||||
theme: light.fixWindowsFont,
|
||||
darkTheme: (tMode < 3 ? dark : dark.toAmoled).fixWindowsFont,
|
||||
home: FutureBuilder<List<IntroPageBuilder>>(
|
||||
future: _IntroPage.builders,
|
||||
future: _introFuture,
|
||||
builder: (context, snapshot) {
|
||||
context.setLibL10n();
|
||||
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 {
|
||||
Loggers.app.info('Updating Android sessions: $payload');
|
||||
await _channel.invokeMethod('updateSessions', payload);
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to update Android sessions', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ abstract final class MethodChans {
|
||||
try {
|
||||
final res = await _channel.invokeMethod('isServiceRunning');
|
||||
return res == true;
|
||||
} catch (_) {
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to check if Android service is running', e, s);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +58,9 @@ abstract final class MethodChans {
|
||||
try {
|
||||
Loggers.app.info('Starting iOS Live Activity: $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 {
|
||||
@@ -65,7 +68,9 @@ abstract final class MethodChans {
|
||||
try {
|
||||
Loggers.app.info('Updating iOS Live Activity: $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 {
|
||||
@@ -73,12 +78,16 @@ abstract final class MethodChans {
|
||||
try {
|
||||
Loggers.app.info('Stopping iOS Live Activity');
|
||||
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.
|
||||
/// Currently handles: `disconnectSession` with argument map {id: string}
|
||||
static void registerHandler(Future<void> Function(String id) onDisconnect) {
|
||||
/// Currently handles:
|
||||
/// - `disconnectSession` with argument map {id: string}
|
||||
/// - `stopAllConnections` with no arguments
|
||||
static void registerHandler(Future<void> Function(String id) onDisconnect, [VoidCallback? onStopAll]) {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case 'disconnectSession':
|
||||
@@ -88,6 +97,9 @@ abstract final class MethodChans {
|
||||
await onDisconnect(id);
|
||||
}
|
||||
return;
|
||||
case 'stopAllConnections':
|
||||
onStopAll?.call();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/dist.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
extension LogoExt on Server {
|
||||
extension LogoExt on ServerState {
|
||||
String? getLogoUrl(BuildContext context) {
|
||||
var logoUrl = spi.custom?.logoUrl ?? Stores.setting.serverLogoUrl.fetch().selfNotEmptyOrNull;
|
||||
if (logoUrl == null) {
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.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/res/misc.dart';
|
||||
@@ -170,4 +171,98 @@ extension SSHClientX on SSHClient {
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
411
lib/core/service/ssh_discovery.dart
Normal file
411
lib/core/service/ssh_discovery.dart
Normal file
@@ -0,0 +1,411 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/discovery_result.dart';
|
||||
|
||||
class SshDiscoveryService {
|
||||
static const _sshPort = 22;
|
||||
|
||||
static Future<SshDiscoveryReport> discover([SshDiscoveryConfig config = const SshDiscoveryConfig()]) async {
|
||||
final t0 = DateTime.now();
|
||||
final candidates = <InternetAddress>{};
|
||||
|
||||
// 1) Get neighbors from ARP/NDP tables
|
||||
candidates.addAll(await _neighborsIPv4());
|
||||
candidates.addAll(await _neighborsIPv6());
|
||||
|
||||
// 2) Enumerate small subnets from local interfaces (IPv4 only)
|
||||
final cidrs = await _localIPv4Cidrs();
|
||||
for (final c in cidrs) {
|
||||
if (c.prefix >= 24 && c.prefix <= 30) {
|
||||
candidates.addAll(c.enumerateHosts(limit: config.hostEnumerationLimit));
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Optional: mDNS/Bonjour SSH services
|
||||
if (config.enableMdns) {
|
||||
candidates.addAll(await _mdnsSshCandidates());
|
||||
}
|
||||
|
||||
// Filter out unwanted addresses: loopback, link-local, 0.0.0.0, broadcast, multicast
|
||||
candidates.removeWhere(
|
||||
(a) => a.isLoopback || a.isLinkLocal || a.address == '0.0.0.0' || _isBroadcastOrMulticast(a),
|
||||
);
|
||||
|
||||
// 4) Concurrent SSH port scanning
|
||||
final scanner = _Scanner(
|
||||
timeout: Duration(milliseconds: config.timeoutMs),
|
||||
maxConcurrency: config.maxConcurrency,
|
||||
);
|
||||
|
||||
final results = await scanner.scan(candidates.toList(growable: false));
|
||||
results.sort((a, b) => a.addr.address.compareTo(b.addr.address));
|
||||
|
||||
final discoveryResults = results
|
||||
.map((r) => SshDiscoveryResult(ip: r.addr.address, port: _sshPort, banner: r.banner?.trim()))
|
||||
.toList();
|
||||
|
||||
return SshDiscoveryReport(
|
||||
generatedAt: DateTime.now().toIso8601String(),
|
||||
durationMs: DateTime.now().difference(t0).inMilliseconds,
|
||||
count: discoveryResults.length,
|
||||
items: discoveryResults,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<String?> _run(String exe, List<String> args, {Duration? timeout}) async {
|
||||
try {
|
||||
final p = await Process.start(exe, args, runInShell: false);
|
||||
final out = await p.stdout
|
||||
.transform(utf8.decoder)
|
||||
.join()
|
||||
.timeout(
|
||||
timeout ?? const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
p.kill();
|
||||
return '';
|
||||
},
|
||||
);
|
||||
final code = await p.exitCode;
|
||||
if (code == 0) return out;
|
||||
// Some tools return non-zero but still have useful output
|
||||
if (out.trim().isNotEmpty) return out;
|
||||
return null;
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to run command: $exe ${args.join(' ')}', e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static bool get _isLinux => Platform.isLinux;
|
||||
static bool get _isMac => Platform.isMacOS;
|
||||
|
||||
static Future<Set<InternetAddress>> _neighborsIPv4() async {
|
||||
final set = <InternetAddress>{};
|
||||
if (_isLinux) {
|
||||
final s = await _run('ip', ['neigh']);
|
||||
if (s != null) {
|
||||
for (final line in const LineSplitter().convert(s)) {
|
||||
final tok = line.split(RegExp(r'\s+'));
|
||||
if (tok.isNotEmpty) {
|
||||
final ip = tok[0];
|
||||
if (InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv4) {
|
||||
set.add(InternetAddress(ip));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_isMac) {
|
||||
final s = await _run('/usr/sbin/arp', ['-an']);
|
||||
if (s != null) {
|
||||
int matchCount = 0;
|
||||
for (final line in const LineSplitter().convert(s)) {
|
||||
final m = RegExp(r'\((\d+\.\d+\.\d+\.\d+)\)').firstMatch(line);
|
||||
if (m != null) {
|
||||
set.add(InternetAddress(m.group(1)!));
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
if (matchCount == 0) {
|
||||
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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
static Future<Set<InternetAddress>> _neighborsIPv6() async {
|
||||
final set = <InternetAddress>{};
|
||||
if (_isLinux) {
|
||||
final s = await _run('ip', ['-6', 'neigh']);
|
||||
if (s != null) {
|
||||
for (final line in const LineSplitter().convert(s)) {
|
||||
final ip = line.split(RegExp(r'\s+')).firstOrNull;
|
||||
if (ip != null && InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv6) {
|
||||
set.add(InternetAddress(ip));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_isMac) {
|
||||
final s = await _run('/usr/sbin/ndp', ['-a']);
|
||||
if (s != null) {
|
||||
for (final line in const LineSplitter().convert(s)) {
|
||||
final ip = line.trim().split(RegExp(r'\s+')).firstOrNull;
|
||||
if (ip != null && InternetAddress.tryParse(ip)?.type == InternetAddressType.IPv6) {
|
||||
set.add(InternetAddress(ip));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
static Future<List<_Cidr>> _localIPv4Cidrs() async {
|
||||
final res = <_Cidr>[];
|
||||
if (_isLinux) {
|
||||
final s = await _run('ip', ['-o', '-4', 'addr', 'show', 'scope', 'global']);
|
||||
if (s != null) {
|
||||
for (final line in const LineSplitter().convert(s)) {
|
||||
final m = RegExp(r'inet\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)').firstMatch(line);
|
||||
if (m != null) {
|
||||
final ip = InternetAddress(m.group(1)!);
|
||||
final prefix = int.parse(m.group(2)!);
|
||||
final mask = _prefixToMask(prefix);
|
||||
final net = _networkAddress(ip, mask);
|
||||
final brd = _broadcastAddress(ip, mask);
|
||||
res.add(_Cidr(ip, prefix, mask, net, brd));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_isMac) {
|
||||
final s = await _run('/sbin/ifconfig', []);
|
||||
if (s != null) {
|
||||
for (final raw in const LineSplitter().convert(s)) {
|
||||
final line = raw.trimRight();
|
||||
final ifMatch = RegExp(r'^([a-z0-9]+):').firstMatch(line);
|
||||
if (ifMatch != null) {
|
||||
continue;
|
||||
}
|
||||
if (line.contains('inet ') && !line.contains('127.0.0.1')) {
|
||||
try {
|
||||
final ipm = RegExp(
|
||||
r'inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+0x([0-9a-fA-F]+)(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))?',
|
||||
).firstMatch(line);
|
||||
if (ipm == null) {
|
||||
Loggers.app.warning('[ssh_discovery] Warning: Unexpected ifconfig line format: $line');
|
||||
continue;
|
||||
}
|
||||
final ip = InternetAddress(ipm.group(1)!);
|
||||
final hexMask = int.parse(ipm.group(2)!, radix: 16);
|
||||
final dotted =
|
||||
'${(hexMask >> 24) & 0xff}.${(hexMask >> 16) & 0xff}.${(hexMask >> 8) & 0xff}.${hexMask & 0xff}';
|
||||
final mask = InternetAddress(dotted);
|
||||
final prefix = _maskToPrefix(mask.address);
|
||||
final net = _networkAddress(ip, mask);
|
||||
final brd = InternetAddress(ipm.group(3) ?? _broadcastAddress(ip, mask).address);
|
||||
res.add(_Cidr(ip, prefix, mask, net, brd));
|
||||
} catch (e) {
|
||||
Loggers.app.warning('[ssh_discovery] Error parsing ifconfig output: $e, line: $line');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool _isBroadcastOrMulticast(InternetAddress a) {
|
||||
// IPv4 broadcast: ends with .255 or is 255.255.255.255
|
||||
if (a.type == InternetAddressType.IPv4) {
|
||||
if (a.address == '255.255.255.255') return true;
|
||||
if (a.address.split('.').last == '255') return true;
|
||||
// Multicast: 224.0.0.0 - 239.255.255.255
|
||||
final firstOctet = int.tryParse(a.address.split('.').first) ?? 0;
|
||||
if (firstOctet >= 224 && firstOctet <= 239) return true;
|
||||
} else if (a.type == InternetAddressType.IPv6) {
|
||||
// IPv6 multicast: starts with ff
|
||||
if (a.address.toLowerCase().startsWith('ff')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<Set<InternetAddress>> _mdnsSshCandidates() async {
|
||||
final set = <InternetAddress>{};
|
||||
if (_isMac) {
|
||||
try {
|
||||
final proc = await Process.start('/usr/bin/dns-sd', ['-B', '_ssh._tcp']);
|
||||
final lines = <String>[];
|
||||
final subscription = proc.stdout
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.listen(lines.add);
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
proc.kill();
|
||||
await subscription.cancel();
|
||||
|
||||
for (final l in lines) {
|
||||
final m = RegExp(r'Add\s+\d+\s+(\S+)\.\s+_ssh\._tcp\.').firstMatch(l);
|
||||
if (m != null) {
|
||||
final name = m.group(1)!;
|
||||
final det = await _run('/usr/bin/dns-sd', [
|
||||
'-L',
|
||||
name,
|
||||
'_ssh._tcp',
|
||||
'local.',
|
||||
], timeout: const Duration(seconds: 3));
|
||||
if (det != null) {
|
||||
for (final ip in RegExp(
|
||||
r'Address\s*=\s*([0-9a-fA-F:\.]+)',
|
||||
).allMatches(det).map((e) => e.group(1)!)) {
|
||||
final parsed = InternetAddress.tryParse(ip);
|
||||
if (parsed != null) set.add(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to discover mDNS SSH candidates on macOS', e, s);
|
||||
}
|
||||
} else if (_isLinux) {
|
||||
final s = await _run('/usr/bin/avahi-browse', ['-rat', '_ssh._tcp']);
|
||||
if (s != null) {
|
||||
for (final ip in RegExp(
|
||||
r'address = \[(.*?)\]',
|
||||
).allMatches(s).map((m) => m.group(1)!).where((e) => e.isNotEmpty)) {
|
||||
final parsed = InternetAddress.tryParse(ip);
|
||||
if (parsed != null) set.add(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
||||
|
||||
class _Cidr {
|
||||
final InternetAddress ip;
|
||||
final int prefix;
|
||||
final InternetAddress netmask;
|
||||
final InternetAddress network;
|
||||
final InternetAddress broadcast;
|
||||
|
||||
_Cidr(this.ip, this.prefix, this.netmask, this.network, this.broadcast);
|
||||
|
||||
Iterable<InternetAddress> enumerateHosts({int? limit}) sync* {
|
||||
final n = _ipv4ToInt(network.address);
|
||||
final b = _ipv4ToInt(broadcast.address);
|
||||
int emitted = 0;
|
||||
for (int v = n + 1; v <= b - 1; v++) {
|
||||
if (limit != null && emitted >= limit) break;
|
||||
emitted++;
|
||||
yield InternetAddress(_intToIPv4(v));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '${network.address}/$prefix';
|
||||
}
|
||||
|
||||
class _ScanResult {
|
||||
final InternetAddress addr;
|
||||
final String? banner;
|
||||
_ScanResult(this.addr, this.banner);
|
||||
}
|
||||
|
||||
class _Scanner {
|
||||
final Duration timeout;
|
||||
final int maxConcurrency;
|
||||
_Scanner({required this.timeout, required this.maxConcurrency});
|
||||
|
||||
Future<List<_ScanResult>> scan(List<InternetAddress> addrs) async {
|
||||
final sem = _Semaphore(maxConcurrency);
|
||||
final futures = <Future<_ScanResult?>>[];
|
||||
for (final a in addrs) {
|
||||
futures.add(_guarded(sem, () => _probeSsh(a)));
|
||||
}
|
||||
final out = await Future.wait(futures);
|
||||
return out.whereType<_ScanResult>().toList();
|
||||
}
|
||||
|
||||
Future<_ScanResult?> _probeSsh(InternetAddress ip) async {
|
||||
Socket? socket;
|
||||
StreamSubscription? sub;
|
||||
try {
|
||||
socket = await Socket.connect(ip, SshDiscoveryService._sshPort, timeout: timeout);
|
||||
socket.timeout(timeout);
|
||||
final c = Completer<String?>();
|
||||
sub = socket.listen(
|
||||
(data) {
|
||||
final s = utf8.decode(data, allowMalformed: true);
|
||||
final line = s.split('\n').firstWhere((_) => true, orElse: () => s);
|
||||
if (!c.isCompleted) {
|
||||
c.complete(line.trim());
|
||||
sub?.cancel();
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (!c.isCompleted) c.complete(null);
|
||||
},
|
||||
onError: (_) {
|
||||
if (!c.isCompleted) c.complete(null);
|
||||
},
|
||||
);
|
||||
final banner = await c.future.timeout(timeout, onTimeout: () => null);
|
||||
return _ScanResult(ip, banner);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to probe SSH at ${ip.address}', e, s);
|
||||
return null;
|
||||
} finally {
|
||||
sub?.cancel();
|
||||
socket?.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Semaphore {
|
||||
int _permits;
|
||||
final Queue<Completer<void>> _q = Queue();
|
||||
_Semaphore(this._permits);
|
||||
|
||||
Future<T> withPermit<T>(Future<T> Function() fn) async {
|
||||
if (_permits > 0) {
|
||||
_permits--;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
_permits++;
|
||||
if (_q.isNotEmpty) _q.removeFirst().complete();
|
||||
}
|
||||
} else {
|
||||
final c = Completer<void>();
|
||||
_q.add(c);
|
||||
await c.future;
|
||||
return withPermit(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _guarded<T>(_Semaphore sem, Future<T> Function() fn) => sem.withPermit(fn);
|
||||
|
||||
// IPv4 utilities
|
||||
|
||||
int _ipv4ToInt(String ip) {
|
||||
final p = ip.split('.').map(int.parse).toList();
|
||||
return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
String _intToIPv4(int v) => '${(v >> 24) & 0xff}.${(v >> 16) & 0xff}.${(v >> 8) & 0xff}.${v & 0xff}';
|
||||
|
||||
InternetAddress _prefixToMask(int prefix) {
|
||||
final mask = prefix == 0 ? 0 : 0xffffffff << (32 - prefix);
|
||||
return InternetAddress(_intToIPv4(mask & 0xffffffff));
|
||||
}
|
||||
|
||||
int _maskToPrefix(String mask) {
|
||||
final v = _ipv4ToInt(mask);
|
||||
int c = 0;
|
||||
for (int i = 31; i >= 0; i--) {
|
||||
if ((v & (1 << i)) != 0) {
|
||||
c++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
InternetAddress _networkAddress(InternetAddress ip, InternetAddress mask) {
|
||||
final v = _ipv4ToInt(ip.address) & _ipv4ToInt(mask.address);
|
||||
return InternetAddress(_intToIPv4(v));
|
||||
}
|
||||
|
||||
InternetAddress _broadcastAddress(InternetAddress ip, InternetAddress mask) {
|
||||
final n = _ipv4ToInt(ip.address) & _ipv4ToInt(mask.address);
|
||||
final b = n | (~_ipv4ToInt(mask.address) & 0xffffffff);
|
||||
return InternetAddress(_intToIPv4(b));
|
||||
}
|
||||
@@ -14,11 +14,7 @@ final class BakSyncer extends SyncIface {
|
||||
@override
|
||||
Future<void> saveToFile() async {
|
||||
final pwd = await SecureStoreProps.bakPwd.read();
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
// Enforce password for non-clipboard backups
|
||||
throw Exception('Backup password not set');
|
||||
}
|
||||
await BackupV2.backup(null, pwd);
|
||||
await BackupV2.backup(null, pwd?.isEmpty == true ? null : pwd);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -30,7 +26,8 @@ final class BakSyncer extends SyncIface {
|
||||
return MergeableUtils.fromJsonString(content, pwd).$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
|
||||
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:convert';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.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/server/server_private_info.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
@@ -29,7 +33,7 @@ enum GenSSHClientStatus { socket, key, pwd }
|
||||
String getPrivateKey(String id) {
|
||||
final pki = Stores.key.fetchOne(id);
|
||||
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;
|
||||
}
|
||||
@@ -52,9 +56,16 @@ Future<SSHClient> genClient(
|
||||
|
||||
/// Handle keyboard-interactive authentication
|
||||
SSHUserInfoRequestHandler? onKeyboardInteractive,
|
||||
Map<String, String>? knownHostFingerprints,
|
||||
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
|
||||
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
|
||||
}) async {
|
||||
onStatus?.call(GenSSHClientStatus.socket);
|
||||
|
||||
final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints());
|
||||
final hostKeyPersist = onHostKeyAccepted ?? _persistHostKeyFingerprint;
|
||||
final hostKeyPrompt = onHostKeyPrompt ?? _defaultHostKeyPrompt;
|
||||
|
||||
String? alterUser;
|
||||
|
||||
final socket = await () async {
|
||||
@@ -66,7 +77,14 @@ Future<SSHClient> genClient(
|
||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
||||
}();
|
||||
if (jumpSpi_ != null) {
|
||||
final jumpClient = await genClient(jumpSpi_, privateKey: jumpPrivateKey, timeout: timeout);
|
||||
final jumpClient = await genClient(
|
||||
jumpSpi_,
|
||||
privateKey: jumpPrivateKey,
|
||||
timeout: timeout,
|
||||
knownHostFingerprints: hostKeyCache,
|
||||
onHostKeyAccepted: hostKeyPersist,
|
||||
onHostKeyPrompt: onHostKeyPrompt,
|
||||
);
|
||||
|
||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
||||
}
|
||||
@@ -78,7 +96,7 @@ Future<SSHClient> genClient(
|
||||
Loggers.app.warning('genClient', e);
|
||||
if (spi.alterUrl == null) rethrow;
|
||||
try {
|
||||
final res = spi.fromStringUrl();
|
||||
final res = spi.parseAlterUrl();
|
||||
alterUser = res.$2;
|
||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||
} catch (e) {
|
||||
@@ -88,6 +106,13 @@ Future<SSHClient> genClient(
|
||||
}
|
||||
}();
|
||||
|
||||
final hostKeyVerifier = _HostKeyVerifier(
|
||||
spi: spi,
|
||||
cache: hostKeyCache,
|
||||
persistCallback: hostKeyPersist,
|
||||
prompt: hostKeyPrompt,
|
||||
);
|
||||
|
||||
final keyId = spi.keyId;
|
||||
if (keyId == null) {
|
||||
onStatus?.call(GenSSHClientStatus.pwd);
|
||||
@@ -96,6 +121,7 @@ Future<SSHClient> genClient(
|
||||
username: alterUser ?? spi.user,
|
||||
onPasswordRequest: () => spi.pwd,
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
onVerifyHostKey: hostKeyVerifier.call,
|
||||
// printDebug: debugPrint,
|
||||
// printTrace: debugPrint,
|
||||
);
|
||||
@@ -109,7 +135,220 @@ Future<SSHClient> genClient(
|
||||
// Must use [compute] here, instead of [Computer.shared.start]
|
||||
identities: await compute(loadIndentity, privateKey),
|
||||
onUserInfoRequest: onKeyboardInteractive,
|
||||
onVerifyHostKey: hostKeyVerifier.call,
|
||||
// printDebug: debugPrint,
|
||||
// printTrace: debugPrint,
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
final cache = _loadKnownHostFingerprints();
|
||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final jumpSpi = spi.jumpId != null ? Stores.server.box.get(spi.jumpId) : null;
|
||||
if (jumpSpi != null && !_hasKnownHostFingerprintForSpi(jumpSpi, cache)) {
|
||||
await ensureKnownHostKey(
|
||||
jumpSpi,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
);
|
||||
cache.addAll(_loadKnownHostFingerprints());
|
||||
if (_hasKnownHostFingerprintForSpi(spi, cache)) return;
|
||||
}
|
||||
|
||||
final client = await genClient(
|
||||
spi,
|
||||
timeout: timeout,
|
||||
onKeyboardInteractive: onKeyboardInteractive,
|
||||
knownHostFingerprints: cache,
|
||||
);
|
||||
|
||||
try {
|
||||
await client.authenticated;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
84
lib/core/utils/server_dedup.dart
Normal file
84
lib/core/utils/server_dedup.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
|
||||
class ServerDeduplication {
|
||||
/// Remove duplicate servers from the import list based on existing servers
|
||||
/// Returns the deduplicated list
|
||||
static List<Spi> deduplicateServers(List<Spi> importedServers) {
|
||||
final existingServers = ServerStore.instance.fetch();
|
||||
final deduplicated = <Spi>[];
|
||||
|
||||
for (final imported in importedServers) {
|
||||
if (!_isDuplicate(imported, existingServers)) {
|
||||
deduplicated.add(imported);
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicated;
|
||||
}
|
||||
|
||||
/// Check if an imported server is a duplicate of an existing server
|
||||
static bool _isDuplicate(Spi imported, List<Spi> existing) {
|
||||
for (final existingSpi in existing) {
|
||||
if (imported.isSameAs(existingSpi)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Resolve name conflicts by appending suffixes
|
||||
static List<Spi> resolveNameConflicts(List<Spi> importedServers) {
|
||||
final existingServers = ServerStore.instance.fetch();
|
||||
final existingNames = existingServers.map((s) => s.name).toSet();
|
||||
final processedNames = <String>{};
|
||||
final result = <Spi>[];
|
||||
|
||||
for (final server in importedServers) {
|
||||
String newName = server.name;
|
||||
int suffix = 1;
|
||||
|
||||
// Check against both existing servers and already processed servers
|
||||
while (existingNames.contains(newName) || processedNames.contains(newName)) {
|
||||
newName = '${server.name} ($suffix)';
|
||||
suffix++;
|
||||
}
|
||||
|
||||
processedNames.add(newName);
|
||||
|
||||
if (newName != server.name) {
|
||||
result.add(server.copyWith(name: newName));
|
||||
} else {
|
||||
result.add(server);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get summary of import operation
|
||||
static ImportSummary getImportSummary(List<Spi> originalList, List<Spi> deduplicatedList) {
|
||||
final duplicateCount = originalList.length - deduplicatedList.length;
|
||||
return ImportSummary(
|
||||
total: originalList.length,
|
||||
duplicates: duplicateCount,
|
||||
toImport: deduplicatedList.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImportSummary {
|
||||
final int total;
|
||||
final int duplicates;
|
||||
final int toImport;
|
||||
|
||||
const ImportSummary({
|
||||
required this.total,
|
||||
required this.duplicates,
|
||||
required this.toImport,
|
||||
});
|
||||
|
||||
bool get hasDuplicates => duplicates > 0;
|
||||
bool get hasItemsToImport => toImport > 0;
|
||||
}
|
||||
190
lib/core/utils/ssh_config.dart
Normal file
190
lib/core/utils/ssh_config.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'dart:io';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
|
||||
/// Utility class to parse SSH config files under `~/.ssh/config`
|
||||
abstract final class SSHConfig {
|
||||
static const String _defaultPath = '~/.ssh/config';
|
||||
|
||||
static String? get _homePath {
|
||||
final homePath = isWindows ? Platform.environment['USERPROFILE'] : Platform.environment['HOME'];
|
||||
if (homePath == null || homePath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return homePath;
|
||||
}
|
||||
|
||||
/// Get possible SSH config file paths, with macOS-specific handling
|
||||
static List<String> get _possibleConfigPaths {
|
||||
final paths = <String>[];
|
||||
final homePath = _homePath;
|
||||
|
||||
if (homePath != null) {
|
||||
// Standard path
|
||||
paths.add('$homePath/.ssh/config');
|
||||
|
||||
// On macOS, also try the actual user home directory
|
||||
if (isMacOS) {
|
||||
// Try to get the real user home directory
|
||||
final username = Platform.environment['USER'];
|
||||
if (username != null) {
|
||||
paths.add('/Users/$username/.ssh/config');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/// Parse SSH config file and return a list of Spi objects
|
||||
static Future<List<Spi>> parseConfig([String? configPath]) async {
|
||||
final (file, exists) = configExists(configPath);
|
||||
if (!exists || file == null) {
|
||||
Loggers.app.info('SSH config file does not exist at path: ${configPath ?? _defaultPath}');
|
||||
return [];
|
||||
}
|
||||
|
||||
final content = await file.readAsString();
|
||||
return _parseSSHConfig(content);
|
||||
}
|
||||
|
||||
/// Parse SSH config content
|
||||
static List<Spi> _parseSSHConfig(String content) {
|
||||
final servers = <Spi>[];
|
||||
final lines = content.split('\n');
|
||||
|
||||
String? currentHost;
|
||||
String? hostname;
|
||||
String? user;
|
||||
int port = 22;
|
||||
String? identityFile;
|
||||
String? jumpHost;
|
||||
|
||||
void addServer() {
|
||||
if (currentHost != null && currentHost != '*' && hostname != null) {
|
||||
final spi = Spi(
|
||||
id: ShortId.generate(),
|
||||
name: currentHost,
|
||||
ip: hostname,
|
||||
port: port,
|
||||
user: user ?? 'root', // Default user is 'root'
|
||||
keyId: identityFile,
|
||||
jumpId: jumpHost,
|
||||
);
|
||||
servers.add(spi);
|
||||
}
|
||||
}
|
||||
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty || trimmed.startsWith('#')) continue;
|
||||
|
||||
// Handle inline comments
|
||||
final commentIndex = trimmed.indexOf('#');
|
||||
final cleanLine = commentIndex != -1 ? trimmed.substring(0, commentIndex).trim() : trimmed;
|
||||
if (cleanLine.isEmpty) continue;
|
||||
|
||||
final parts = cleanLine.split(RegExp(r'\s+'));
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
final key = parts[0].toLowerCase();
|
||||
var value = parts.sublist(1).join(' ');
|
||||
|
||||
// Remove quotes from values
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.substring(1, value.length - 1);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'host':
|
||||
// Save previous host config
|
||||
addServer();
|
||||
|
||||
// Reset for new host
|
||||
final originalValue = parts.sublist(1).join(' ');
|
||||
final isQuoted =
|
||||
(originalValue.startsWith('"') && originalValue.endsWith('"')) ||
|
||||
(originalValue.startsWith("'") && originalValue.endsWith("'"));
|
||||
|
||||
currentHost = value;
|
||||
// Skip hosts with multiple patterns (contains spaces but not quoted)
|
||||
if (currentHost.contains(' ') && !isQuoted) {
|
||||
currentHost = null; // Mark as invalid to skip
|
||||
}
|
||||
hostname = null;
|
||||
user = null;
|
||||
port = 22;
|
||||
identityFile = null;
|
||||
jumpHost = null;
|
||||
break;
|
||||
|
||||
case 'hostname':
|
||||
hostname = value;
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user = value;
|
||||
break;
|
||||
|
||||
case 'port':
|
||||
port = int.tryParse(value) ?? 22;
|
||||
break;
|
||||
|
||||
case 'identityfile':
|
||||
identityFile = value; // Store the path directly
|
||||
break;
|
||||
|
||||
case 'proxyjump':
|
||||
case 'proxycommand':
|
||||
jumpHost = _extractJumpHost(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last server
|
||||
addServer();
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/// Extract jump host from ProxyJump or ProxyCommand
|
||||
static String? _extractJumpHost(String value) {
|
||||
if (value.isEmpty) return null;
|
||||
// For ProxyJump, the format is usually: user@host:port
|
||||
// For ProxyCommand, it's more complex and might need custom parsing
|
||||
if (value.contains('@')) {
|
||||
final parts = value.split(' ');
|
||||
return parts.isNotEmpty ? parts[0] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if SSH config file exists, trying multiple possible paths
|
||||
static (File?, bool) configExists([String? configPath]) {
|
||||
if (configPath != null) {
|
||||
// If specific path is provided, use it directly
|
||||
final homePath = _homePath;
|
||||
if (homePath == null) {
|
||||
Loggers.app.warning('Cannot determine home directory for SSH config parsing.');
|
||||
return (null, false);
|
||||
}
|
||||
final expandedPath = configPath.replaceFirst('~', homePath);
|
||||
dprint('Checking SSH config at path: $expandedPath');
|
||||
final file = File(expandedPath);
|
||||
return (file, file.existsSync());
|
||||
}
|
||||
|
||||
// Try multiple possible paths
|
||||
for (final path in _possibleConfigPaths) {
|
||||
dprint('Checking SSH config at path: $path');
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
dprint('Found SSH config at: $path');
|
||||
return (file, true);
|
||||
}
|
||||
}
|
||||
|
||||
dprint('SSH config file not found in any of the expected locations');
|
||||
return (null, false);
|
||||
}
|
||||
}
|
||||
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: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/system.dart';
|
||||
|
||||
@@ -9,8 +10,8 @@ class SystemDetector {
|
||||
///
|
||||
/// First checks if a custom system type is configured in [spi].
|
||||
/// If not, attempts to detect the system by running commands:
|
||||
/// 1. 'ver' command to detect Windows
|
||||
/// 2. 'uname -a' command to detect Linux/BSD/Darwin
|
||||
/// 1. 'uname -a' command to detect Linux/BSD/Darwin
|
||||
/// 2. 'ver' command to detect Windows (if uname fails)
|
||||
///
|
||||
/// Returns [SystemType.linux] as default if detection fails.
|
||||
static Future<SystemType> detect(SSHClient client, Spi spi) async {
|
||||
@@ -22,17 +23,11 @@ class SystemDetector {
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to detect Windows systems first (more reliable detection)
|
||||
final powershellResult = await client.run('ver 2>nul').string;
|
||||
if (powershellResult.isNotEmpty &&
|
||||
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
|
||||
detectedSystemType = SystemType.windows;
|
||||
dprint('Detected Windows system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
|
||||
// Try to detect Unix/Linux/BSD systems
|
||||
final unixResult = await client.run('uname -a').string;
|
||||
// Try to detect Unix/Linux/BSD systems first (more reliable and doesn't create files)
|
||||
final unixResult = await client.runSafe(
|
||||
'uname -a 2>/dev/null',
|
||||
context: 'uname detection for ${spi.oldId}',
|
||||
);
|
||||
if (unixResult.contains('Linux')) {
|
||||
detectedSystemType = SystemType.linux;
|
||||
dprint('Detected Linux system type for ${spi.oldId}');
|
||||
@@ -42,8 +37,21 @@ class SystemDetector {
|
||||
dprint('Detected BSD system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
} catch (e) {
|
||||
Loggers.app.warning('System detection failed for ${spi.oldId}: $e');
|
||||
|
||||
// If uname fails, try to detect Windows systems
|
||||
final powershellResult = await client.runSafe(
|
||||
'ver 2>nul',
|
||||
systemType: SystemType.windows,
|
||||
context: 'ver detection for ${spi.oldId}',
|
||||
);
|
||||
if (powershellResult.isNotEmpty &&
|
||||
(powershellResult.contains('Windows') || powershellResult.contains('NT'))) {
|
||||
detectedSystemType = SystemType.windows;
|
||||
dprint('Detected Windows system type for ${spi.oldId}');
|
||||
return detectedSystemType;
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
Loggers.app.warning('System detection failed for ${spi.oldId}: $e\n$stackTrace');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import 'dart:io';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/data/provider/snippet.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
@@ -44,17 +47,17 @@ abstract class BackupV2 with _$BackupV2 implements Mergeable {
|
||||
Future<void> merge({bool force = false}) async {
|
||||
_loggerV2.info('Merging...');
|
||||
|
||||
// Merge each store
|
||||
await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
|
||||
await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
|
||||
await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
|
||||
// Merge each store and check if changes were made
|
||||
final serverChanged = await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
|
||||
final snippetChanged = await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
|
||||
final keyChanged = await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
|
||||
await Mergeable.mergeStore(backupData: container, store: Stores.container, force: force);
|
||||
await Mergeable.mergeStore(backupData: history, store: Stores.history, force: force);
|
||||
await Mergeable.mergeStore(backupData: settings, store: Stores.setting, force: force);
|
||||
|
||||
// Reload providers and notify listeners
|
||||
Provider.reload();
|
||||
RNodes.app.notify();
|
||||
if (serverChanged) GlobalRef.gRef?.read(serversProvider.notifier).reload();
|
||||
if (snippetChanged) GlobalRef.gRef?.read(snippetProvider.notifier).reload();
|
||||
if (keyChanged) GlobalRef.gRef?.read(privateKeyProvider.notifier).reload();
|
||||
|
||||
_loggerV2.info('Merge completed');
|
||||
}
|
||||
|
||||
@@ -11,27 +11,10 @@ class BackupService {
|
||||
/// Perform backup operation with the given source
|
||||
static Future<void> backup(BuildContext context, BackupSource source) async {
|
||||
try {
|
||||
String? password;
|
||||
final saved = await SecureStoreProps.bakPwd.read();
|
||||
final password = saved?.isEmpty == true ? null : saved;
|
||||
|
||||
if (source is ClipboardBackupSource) {
|
||||
// Clipboard backup: allow optional password
|
||||
password = await _getClipboardPassword(context);
|
||||
if (password == null) return; // canceled
|
||||
} else {
|
||||
// All other backups require pre-set bakPwd (SecureStore)
|
||||
final saved = await SecureStoreProps.bakPwd.read();
|
||||
if (saved == null || saved.isEmpty) {
|
||||
// Prompt to set before proceeding
|
||||
password = await _showPasswordDialog(context, hint: l10n.backupPasswordTip);
|
||||
if (password == null || password.isEmpty) return; // Not set
|
||||
await SecureStoreProps.bakPwd.write(password);
|
||||
context.showSnackBar(l10n.backupPasswordSet);
|
||||
} else {
|
||||
password = saved;
|
||||
}
|
||||
}
|
||||
|
||||
final path = await BackupV2.backup(null, password.isEmpty ? null : password);
|
||||
final path = await BackupV2.backup(null, password?.isEmpty == true ? null : password);
|
||||
await source.saveContent(path);
|
||||
|
||||
if (source is ClipboardBackupSource) {
|
||||
@@ -56,34 +39,6 @@ class BackupService {
|
||||
await restoreFromText(context, text);
|
||||
}
|
||||
|
||||
/// Handle password dialog for backup operations
|
||||
static Future<String?> _getClipboardPassword(BuildContext context) async {
|
||||
// Use saved bakPwd as default for clipboard flow if exists, but allow empty/custom
|
||||
final savedPassword = await SecureStoreProps.bakPwd.read();
|
||||
String? password;
|
||||
|
||||
if (savedPassword != null && savedPassword.isNotEmpty) {
|
||||
final useCustom = await context.showRoundDialog<bool>(
|
||||
title: l10n.backupPassword,
|
||||
child: Text(l10n.backupPasswordTip),
|
||||
actions: [
|
||||
Btn.cancel(),
|
||||
TextButton(onPressed: () => context.pop(false), child: Text(l10n.backupPasswordSet)),
|
||||
TextButton(onPressed: () => context.pop(true), child: Text(libL10n.custom)),
|
||||
],
|
||||
);
|
||||
if (useCustom == null) return null;
|
||||
if (useCustom) {
|
||||
password = await _showPasswordDialog(context, initial: savedPassword);
|
||||
} else {
|
||||
password = savedPassword;
|
||||
}
|
||||
} else {
|
||||
password = await _showPasswordDialog(context);
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
/// Handle restore from text with decryption support
|
||||
static Future<void> restoreFromText(BuildContext context, String text) async {
|
||||
// Check if backup is encrypted
|
||||
@@ -119,8 +74,8 @@ class BackupService {
|
||||
await _confirmAndRestore(context, backup);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Saved password failed, will prompt for manual input
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to restore with saved password, will prompt for manual input', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:server_box/core/extension/context/locale.dart';
|
||||
enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus }
|
||||
|
||||
class SSHErr extends Err<SSHErrType> {
|
||||
SSHErr({required super.type, super.message});
|
||||
const SSHErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => switch (type) {
|
||||
@@ -29,7 +29,7 @@ enum ContainerErrType {
|
||||
}
|
||||
|
||||
class ContainerErr extends Err<ContainerErrType> {
|
||||
ContainerErr({required super.type, super.message});
|
||||
const ContainerErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => null;
|
||||
@@ -38,7 +38,7 @@ class ContainerErr extends Err<ContainerErrType> {
|
||||
enum ICloudErrType { generic, notFound, multipleFiles }
|
||||
|
||||
class ICloudErr extends Err<ICloudErrType> {
|
||||
ICloudErr({required super.type, super.message});
|
||||
const ICloudErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => null;
|
||||
@@ -47,7 +47,7 @@ class ICloudErr extends Err<ICloudErrType> {
|
||||
enum WebdavErrType { generic, notFound }
|
||||
|
||||
class WebdavErr extends Err<WebdavErrType> {
|
||||
WebdavErr({required super.type, super.message});
|
||||
const WebdavErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => null;
|
||||
@@ -56,7 +56,7 @@ class WebdavErr extends Err<WebdavErrType> {
|
||||
enum PveErrType { unknown, net, loginFailed }
|
||||
|
||||
class PveErr extends Err<PveErrType> {
|
||||
PveErr({required super.type, super.message});
|
||||
const PveErr({required super.type, super.message});
|
||||
|
||||
@override
|
||||
String? get solution => null;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,8 @@ enum StatusCmdType implements ShellCmdType {
|
||||
uptime('uptime'),
|
||||
conn('cat /proc/net/snmp'),
|
||||
disk(
|
||||
'lsblk --bytes --json --output '
|
||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
'(lsblk --bytes --json --output '
|
||||
'FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID 2>/dev/null && echo "LSBLK_SUCCESS") || df -k'
|
||||
),
|
||||
mem("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
@@ -166,25 +166,34 @@ enum WindowsStatusCmdType implements ShellCmdType {
|
||||
echo('echo ${SystemType.windowsSign}'),
|
||||
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:
|
||||
/// - Collects bytes received and sent per second for all network interfaces
|
||||
/// Uses WMI Win32_PerfRawData_Tcpip_NetworkInterface for cross-language compatibility:
|
||||
/// - 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(
|
||||
r'Get-Counter -Counter '
|
||||
r'"\\NetworkInterface(*)\\Bytes Received/sec", '
|
||||
r'"\\NetworkInterface(*)\\Bytes Sent/sec" '
|
||||
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
|
||||
r'$s1 = @(Get-WmiObject Win32_PerfRawData_Tcpip_NetworkInterface | '
|
||||
r'Select-Object Name, BytesReceivedPersec, BytesSentPersec, Timestamp_Sys100NS); '
|
||||
r'Start-Sleep -Seconds 1; '
|
||||
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'),
|
||||
cpu(
|
||||
'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'),
|
||||
disk(
|
||||
'Get-WmiObject -Class Win32_LogicalDisk | '
|
||||
@@ -213,19 +222,19 @@ enum WindowsStatusCmdType implements ShellCmdType {
|
||||
),
|
||||
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
|
||||
/// - Takes 2 samples with 1 second interval to calculate I/O rates
|
||||
/// - Physical disk counters provide hardware-level I/O statistics
|
||||
/// - Outputs results in JSON format for parsing
|
||||
/// - Counter names use wildcard (*) to capture all disk instances
|
||||
/// - Takes 2 samples with 1 second interval to calculate rates
|
||||
/// - DiskReadBytesPersec and DiskWriteBytesPersec are cumulative counters
|
||||
diskio(
|
||||
r'Get-Counter -Counter '
|
||||
r'"\\PhysicalDisk(*)\\Disk Read Bytes/sec", '
|
||||
r'"\\PhysicalDisk(*)\\Disk Write Bytes/sec" '
|
||||
r'-SampleInterval 1 -MaxSamples 2 | ConvertTo-Json',
|
||||
r'$s1 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '
|
||||
r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '
|
||||
r'Start-Sleep -Seconds 1; '
|
||||
r'$s2 = @(Get-WmiObject Win32_PerfRawData_PerfDisk_PhysicalDisk | '
|
||||
r'Select-Object Name, DiskReadBytesPersec, DiskWriteBytesPersec, Timestamp_Sys100NS); '
|
||||
r'@($s1, $s2) | ConvertTo-Json -Depth 5',
|
||||
),
|
||||
battery(
|
||||
'Get-WmiObject -Class Win32_Battery | '
|
||||
@@ -287,7 +296,7 @@ enum WindowsStatusCmdType implements ShellCmdType {
|
||||
String get separator => ScriptConstants.getCmdSeparator(name);
|
||||
|
||||
@override
|
||||
String get divider => ScriptConstants.getCmdDivider(name);
|
||||
String get divider => ScriptConstants.getWindowsCmdDivider(name);
|
||||
|
||||
@override
|
||||
CmdTypeSys get sysType => CmdTypeSys.windows;
|
||||
|
||||
@@ -29,6 +29,9 @@ class ScriptConstants {
|
||||
/// Generate command-specific divider
|
||||
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
|
||||
static Map<String, String> parseScriptOutput(String raw) {
|
||||
final result = <String, String>{};
|
||||
@@ -102,6 +105,7 @@ exec 2>/dev/null
|
||||
# DO NOT delete this file while app is running
|
||||
|
||||
\$ErrorActionPreference = "SilentlyContinue"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
''';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:server_box/data/model/app/scripts/script_builders.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
|
||||
/// Shell functions available in the ServerBox application
|
||||
enum ShellFunc {
|
||||
@@ -26,8 +25,8 @@ enum ShellFunc {
|
||||
};
|
||||
|
||||
/// Execute this shell function on the specified server
|
||||
String exec(String id, {SystemType? systemType}) {
|
||||
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType);
|
||||
String exec(String id, {SystemType? systemType, required String? customDir}) {
|
||||
final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType, customDir: customDir);
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
|
||||
@@ -51,11 +50,10 @@ class ShellFuncManager {
|
||||
/// Get the script directory for the given [id].
|
||||
///
|
||||
/// Checks for custom script directory first, then falls back to default.
|
||||
static String getScriptDir(String id, {SystemType? systemType}) {
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
static String getScriptDir(String id, {SystemType? systemType, required String? customDir}) {
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
|
||||
if (customScriptDir != null) return _normalizeDir(customScriptDir, isWindows);
|
||||
if (customDir != null) return _normalizeDir(customDir, isWindows);
|
||||
return ScriptPaths.getScriptDir(id, isWindows: isWindows);
|
||||
}
|
||||
|
||||
@@ -66,11 +64,10 @@ class ShellFuncManager {
|
||||
}
|
||||
|
||||
/// Get the full script path for the given [id]
|
||||
static String getScriptPath(String id, {SystemType? systemType}) {
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
if (customScriptDir != null) {
|
||||
static String getScriptPath(String id, {SystemType? systemType, required String? customDir}) {
|
||||
if (customDir != null) {
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final normalizedDir = _normalizeDir(customScriptDir, isWindows);
|
||||
final normalizedDir = _normalizeDir(customDir, isWindows);
|
||||
final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile;
|
||||
final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator;
|
||||
return '$normalizedDir$separator$fileName';
|
||||
@@ -81,8 +78,8 @@ class ShellFuncManager {
|
||||
}
|
||||
|
||||
/// Get the installation shell command for the script
|
||||
static String getInstallShellCmd(String id, {SystemType? systemType}) {
|
||||
final scriptDir = getScriptDir(id, systemType: systemType);
|
||||
static String getInstallShellCmd(String id, {SystemType? systemType, required String? customDir}) {
|
||||
final scriptDir = getScriptDir(id, systemType: systemType, customDir: customDir);
|
||||
final isWindows = systemType == SystemType.windows;
|
||||
final normalizedDir = _normalizeDir(scriptDir, isWindows);
|
||||
final builder = ScriptBuilderFactory.getBuilder(isWindows);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_ce_flutter/adapters.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/view/page/server/tab/tab.dart';
|
||||
@@ -8,10 +9,17 @@ import 'package:server_box/view/page/snippet/list.dart';
|
||||
import 'package:server_box/view/page/ssh/tab.dart';
|
||||
import 'package:server_box/view/page/storage/local.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@HiveType(typeId: 103)
|
||||
enum AppTab {
|
||||
@HiveField(0)
|
||||
server,
|
||||
@HiveField(1)
|
||||
ssh,
|
||||
@HiveField(2)
|
||||
file,
|
||||
@HiveField(3)
|
||||
snippet
|
||||
//settings,
|
||||
;
|
||||
@@ -93,4 +101,35 @@ enum AppTab {
|
||||
static List<NavigationRailDestination> get navRailDestinations {
|
||||
return AppTab.values.map((e) => e.navRailDestination).toList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Helper function to parse AppTab list from stored object
|
||||
static List<AppTab> parseAppTabsFromObj(dynamic val) {
|
||||
if (val is List) {
|
||||
final tabs = <AppTab>[];
|
||||
for (final e in val) {
|
||||
final tab = _parseAppTabFromElement(e);
|
||||
if (tab != null) {
|
||||
tabs.add(tab);
|
||||
}
|
||||
}
|
||||
if (tabs.isNotEmpty) return tabs;
|
||||
}
|
||||
return AppTab.values;
|
||||
}
|
||||
|
||||
/// Helper function to parse a single AppTab from various element types
|
||||
static AppTab? _parseAppTabFromElement(dynamic e) {
|
||||
if (e is AppTab) {
|
||||
return e;
|
||||
} else if (e is String) {
|
||||
return AppTab.values.firstWhereOrNull((t) => t.name == e);
|
||||
} else if (e is int) {
|
||||
if (e >= 0 && e < AppTab.values.length) {
|
||||
return AppTab.values[e];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
52
lib/data/model/app/tab.g.dart
Normal file
52
lib/data/model/app/tab.g.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AppTabAdapter extends TypeAdapter<AppTab> {
|
||||
@override
|
||||
final typeId = 103;
|
||||
|
||||
@override
|
||||
AppTab read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return AppTab.server;
|
||||
case 1:
|
||||
return AppTab.ssh;
|
||||
case 2:
|
||||
return AppTab.file;
|
||||
case 3:
|
||||
return AppTab.snippet;
|
||||
default:
|
||||
return AppTab.server;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppTab obj) {
|
||||
switch (obj) {
|
||||
case AppTab.server:
|
||||
writer.writeByte(0);
|
||||
case AppTab.ssh:
|
||||
writer.writeByte(1);
|
||||
case AppTab.file:
|
||||
writer.writeByte(2);
|
||||
case AppTab.snippet:
|
||||
writer.writeByte(3);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AppTabAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -37,12 +37,12 @@ final class PodmanImg implements ContainerImg {
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
||||
repository: json['repository'],
|
||||
tag: json['tag'],
|
||||
id: json['Id'],
|
||||
created: json['Created'],
|
||||
size: json['Size'],
|
||||
containers: json['Containers'],
|
||||
repository: _asString(json['repository']),
|
||||
tag: _asString(json['tag']),
|
||||
id: _asString(json['Id']),
|
||||
created: _asInt(json['Created']),
|
||||
size: _asInt(json['Size']),
|
||||
containers: _asInt(json['Containers']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -119,3 +119,16 @@ final class DockerImg implements ContainerImg {
|
||||
'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());
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/container/status.dart';
|
||||
import 'package:server_box/data/model/container/type.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
|
||||
@@ -10,7 +11,7 @@ sealed class ContainerPs {
|
||||
final String? image = null;
|
||||
String? get name;
|
||||
String? get cmd;
|
||||
bool get running;
|
||||
ContainerStatus get status;
|
||||
|
||||
String? cpu;
|
||||
String? mem;
|
||||
@@ -19,7 +20,7 @@ sealed class ContainerPs {
|
||||
|
||||
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 {
|
||||
@@ -51,10 +52,10 @@ final class PodmanPs implements ContainerPs {
|
||||
String? get cmd => command?.firstOrNull;
|
||||
|
||||
@override
|
||||
bool get running => exited != true;
|
||||
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);
|
||||
|
||||
@override
|
||||
void parseStats(String s) {
|
||||
void parseStats(String s, [String? version]) {
|
||||
final stats = json.decode(s);
|
||||
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
|
||||
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
|
||||
@@ -62,12 +63,32 @@ final class PodmanPs implements ContainerPs {
|
||||
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
|
||||
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
|
||||
mem = '$memUsage / $memLimit';
|
||||
final netIn = (stats['NetInput'] as int? ?? 0).bytes2Str;
|
||||
final netOut = (stats['NetOutput'] as int? ?? 0).bytes2Str;
|
||||
net = '↓ $netIn / ↑ $netOut';
|
||||
|
||||
int netIn = 0;
|
||||
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 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));
|
||||
@@ -121,18 +142,21 @@ final class DockerPs implements ContainerPs {
|
||||
String? get cmd => null;
|
||||
|
||||
@override
|
||||
bool get running {
|
||||
if (state?.contains('Exited') == true) return false;
|
||||
return true;
|
||||
}
|
||||
ContainerStatus get status => ContainerStatus.fromDockerState(state);
|
||||
|
||||
@override
|
||||
void parseStats(String s) {
|
||||
void parseStats(String s, [String? version]) {
|
||||
final stats = json.decode(s);
|
||||
cpu = stats['CPUPerc'];
|
||||
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
|
||||
|
||||
70
lib/data/model/container/status.dart
Normal file
70
lib/data/model/container/status.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
|
||||
/// Represents the various states a container can be in.
|
||||
/// Supports both Docker and Podman container status parsing.
|
||||
enum ContainerStatus {
|
||||
running,
|
||||
exited,
|
||||
created,
|
||||
paused,
|
||||
restarting,
|
||||
removing,
|
||||
dead,
|
||||
unknown;
|
||||
|
||||
/// Check if the container is actively running
|
||||
bool get isRunning => this == ContainerStatus.running;
|
||||
|
||||
/// Check if the container can be started
|
||||
bool get canStart =>
|
||||
this == ContainerStatus.exited ||
|
||||
this == ContainerStatus.created ||
|
||||
this == ContainerStatus.dead;
|
||||
|
||||
/// Check if the container can be stopped
|
||||
bool get canStop =>
|
||||
this == ContainerStatus.running || this == ContainerStatus.paused;
|
||||
|
||||
/// Check if the container can be restarted
|
||||
bool get canRestart =>
|
||||
this != ContainerStatus.removing && this != ContainerStatus.unknown;
|
||||
|
||||
/// Parse Docker container status string to ContainerStatus
|
||||
static ContainerStatus fromDockerState(String? state) {
|
||||
if (state == null || state.isEmpty) return ContainerStatus.unknown;
|
||||
|
||||
final lowerState = state.toLowerCase();
|
||||
|
||||
if (lowerState.startsWith('up')) return ContainerStatus.running;
|
||||
if (lowerState.contains('exited')) return ContainerStatus.exited;
|
||||
if (lowerState.contains('created')) return ContainerStatus.created;
|
||||
if (lowerState.contains('paused')) return ContainerStatus.paused;
|
||||
if (lowerState.contains('restarting')) return ContainerStatus.restarting;
|
||||
if (lowerState.contains('removing')) return ContainerStatus.removing;
|
||||
if (lowerState.contains('dead')) return ContainerStatus.dead;
|
||||
|
||||
return ContainerStatus.unknown;
|
||||
}
|
||||
|
||||
/// Parse Podman container status from exited boolean
|
||||
static ContainerStatus fromPodmanExited(bool? exited) {
|
||||
if (exited == true) return ContainerStatus.exited;
|
||||
if (exited == false) return ContainerStatus.running;
|
||||
return ContainerStatus.unknown;
|
||||
}
|
||||
|
||||
/// Get display string for the status
|
||||
String get displayName {
|
||||
return switch (this) {
|
||||
ContainerStatus.running => l10n.running,
|
||||
ContainerStatus.exited => libL10n.exit,
|
||||
ContainerStatus.created => 'Created',
|
||||
ContainerStatus.paused => 'Paused',
|
||||
ContainerStatus.restarting => 'Restarting',
|
||||
ContainerStatus.removing => 'Removing',
|
||||
ContainerStatus.dead => 'Dead',
|
||||
ContainerStatus.unknown => libL10n.unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ class UpgradePkgInfo {
|
||||
|
||||
void _parsePacman(String raw) {
|
||||
final parts = raw.split(' ');
|
||||
if (parts.length < 4) throw Exception('Invalid pacman output format');
|
||||
package = parts[0];
|
||||
nowVersion = parts[1];
|
||||
newVersion = parts[3];
|
||||
@@ -70,6 +71,7 @@ class UpgradePkgInfo {
|
||||
|
||||
void _parseOpkg(String raw) {
|
||||
final parts = raw.split(' - ');
|
||||
if (parts.length < 3) throw Exception('Invalid opkg output format');
|
||||
package = parts[0];
|
||||
nowVersion = parts[1];
|
||||
newVersion = parts[2];
|
||||
@@ -80,6 +82,7 @@ class UpgradePkgInfo {
|
||||
void _parseApk(String raw) {
|
||||
final parts = raw.split(' ');
|
||||
final len = parts.length;
|
||||
if (len < 2) throw Exception('Invalid apk output format');
|
||||
newVersion = parts[len - 1];
|
||||
nowVersion = parts[0];
|
||||
newVersion = newVersion.substring(0, newVersion.length - 1);
|
||||
|
||||
79
lib/data/model/server/connection_stat.dart
Normal file
79
lib/data/model/server/connection_stat.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
part 'connection_stat.freezed.dart';
|
||||
part 'connection_stat.g.dart';
|
||||
|
||||
@freezed
|
||||
@HiveType(typeId: 100)
|
||||
abstract class ConnectionStat with _$ConnectionStat {
|
||||
const factory ConnectionStat({
|
||||
@HiveField(0) required String serverId,
|
||||
@HiveField(1) required String serverName,
|
||||
@HiveField(2) required DateTime timestamp,
|
||||
@HiveField(3) required ConnectionResult result,
|
||||
@HiveField(4) @Default('') String errorMessage,
|
||||
@HiveField(5) required int durationMs,
|
||||
}) = _ConnectionStat;
|
||||
|
||||
factory ConnectionStat.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConnectionStatFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@HiveType(typeId: 101)
|
||||
abstract class ServerConnectionStats with _$ServerConnectionStats {
|
||||
const factory ServerConnectionStats({
|
||||
@HiveField(0) required String serverId,
|
||||
@HiveField(1) required String serverName,
|
||||
@HiveField(2) required int totalAttempts,
|
||||
@HiveField(3) required int successCount,
|
||||
@HiveField(4) required int failureCount,
|
||||
@HiveField(5) @Default(null) DateTime? lastSuccessTime,
|
||||
@HiveField(6) @Default(null) DateTime? lastFailureTime,
|
||||
@HiveField(7) @Default([]) List<ConnectionStat> recentConnections,
|
||||
@HiveField(8) required double successRate,
|
||||
}) = _ServerConnectionStats;
|
||||
|
||||
factory ServerConnectionStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerConnectionStatsFromJson(json);
|
||||
}
|
||||
|
||||
@HiveType(typeId: 102)
|
||||
enum ConnectionResult {
|
||||
@HiveField(0)
|
||||
@JsonValue('success')
|
||||
success,
|
||||
@HiveField(1)
|
||||
@JsonValue('timeout')
|
||||
timeout,
|
||||
@HiveField(2)
|
||||
@JsonValue('auth_failed')
|
||||
authFailed,
|
||||
@HiveField(3)
|
||||
@JsonValue('network_error')
|
||||
networkError,
|
||||
@HiveField(4)
|
||||
@JsonValue('unknown_error')
|
||||
unknownError,
|
||||
}
|
||||
|
||||
extension ConnectionResultExtension on ConnectionResult {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ConnectionResult.success:
|
||||
return libL10n.success;
|
||||
case ConnectionResult.timeout:
|
||||
return '${libL10n.error}(timeout)';
|
||||
case ConnectionResult.authFailed:
|
||||
return '${libL10n.error}(auth)';
|
||||
case ConnectionResult.networkError:
|
||||
return '${libL10n.error}(${libL10n.network})';
|
||||
case ConnectionResult.unknownError:
|
||||
return '${libL10n.error}(${libL10n.unknown})';
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSuccess => this == ConnectionResult.success;
|
||||
}
|
||||
585
lib/data/model/server/connection_stat.freezed.dart
Normal file
585
lib/data/model/server/connection_stat.freezed.dart
Normal file
@@ -0,0 +1,585 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'connection_stat.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConnectionStat {
|
||||
|
||||
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) DateTime get timestamp;@HiveField(3) ConnectionResult get result;@HiveField(4) String get errorMessage;@HiveField(5) int get durationMs;
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConnectionStatCopyWith<ConnectionStat> get copyWith => _$ConnectionStatCopyWithImpl<ConnectionStat>(this as ConnectionStat, _$identity);
|
||||
|
||||
/// Serializes this ConnectionStat to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ConnectionStatCopyWith<$Res> {
|
||||
factory $ConnectionStatCopyWith(ConnectionStat value, $Res Function(ConnectionStat) _then) = _$ConnectionStatCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ConnectionStatCopyWithImpl<$Res>
|
||||
implements $ConnectionStatCopyWith<$Res> {
|
||||
_$ConnectionStatCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ConnectionStat _self;
|
||||
final $Res Function(ConnectionStat) _then;
|
||||
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ConnectionStat].
|
||||
extension ConnectionStatPatterns on ConnectionStat {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ConnectionStat value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ConnectionStat value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ConnectionStat value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat():
|
||||
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ConnectionStat() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ConnectionStat implements ConnectionStat {
|
||||
const _ConnectionStat({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.timestamp, @HiveField(3) required this.result, @HiveField(4) this.errorMessage = '', @HiveField(5) required this.durationMs});
|
||||
factory _ConnectionStat.fromJson(Map<String, dynamic> json) => _$ConnectionStatFromJson(json);
|
||||
|
||||
@override@HiveField(0) final String serverId;
|
||||
@override@HiveField(1) final String serverName;
|
||||
@override@HiveField(2) final DateTime timestamp;
|
||||
@override@HiveField(3) final ConnectionResult result;
|
||||
@override@JsonKey()@HiveField(4) final String errorMessage;
|
||||
@override@HiveField(5) final int durationMs;
|
||||
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ConnectionStatCopyWith<_ConnectionStat> get copyWith => __$ConnectionStatCopyWithImpl<_ConnectionStat>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ConnectionStatToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ConnectionStatCopyWith<$Res> implements $ConnectionStatCopyWith<$Res> {
|
||||
factory _$ConnectionStatCopyWith(_ConnectionStat value, $Res Function(_ConnectionStat) _then) = __$ConnectionStatCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ConnectionStatCopyWithImpl<$Res>
|
||||
implements _$ConnectionStatCopyWith<$Res> {
|
||||
__$ConnectionStatCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ConnectionStat _self;
|
||||
final $Res Function(_ConnectionStat) _then;
|
||||
|
||||
/// Create a copy of ConnectionStat
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
|
||||
return _then(_ConnectionStat(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
|
||||
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ServerConnectionStats {
|
||||
|
||||
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) int get totalAttempts;@HiveField(3) int get successCount;@HiveField(4) int get failureCount;@HiveField(5) DateTime? get lastSuccessTime;@HiveField(6) DateTime? get lastFailureTime;@HiveField(7) List<ConnectionStat> get recentConnections;@HiveField(8) double get successRate;
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerConnectionStatsCopyWith<ServerConnectionStats> get copyWith => _$ServerConnectionStatsCopyWithImpl<ServerConnectionStats>(this as ServerConnectionStats, _$identity);
|
||||
|
||||
/// Serializes this ServerConnectionStats to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other.recentConnections, recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(recentConnections),successRate);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ServerConnectionStatsCopyWith<$Res> {
|
||||
factory $ServerConnectionStatsCopyWith(ServerConnectionStats value, $Res Function(ServerConnectionStats) _then) = _$ServerConnectionStatsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ServerConnectionStatsCopyWithImpl<$Res>
|
||||
implements $ServerConnectionStatsCopyWith<$Res> {
|
||||
_$ServerConnectionStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ServerConnectionStats _self;
|
||||
final $Res Function(ServerConnectionStats) _then;
|
||||
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,recentConnections: null == recentConnections ? _self.recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
|
||||
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ServerConnectionStats].
|
||||
extension ServerConnectionStatsPatterns on ServerConnectionStats {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerConnectionStats value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerConnectionStats value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerConnectionStats value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats():
|
||||
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerConnectionStats() when $default != null:
|
||||
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ServerConnectionStats implements ServerConnectionStats {
|
||||
const _ServerConnectionStats({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.totalAttempts, @HiveField(3) required this.successCount, @HiveField(4) required this.failureCount, @HiveField(5) this.lastSuccessTime = null, @HiveField(6) this.lastFailureTime = null, @HiveField(7) final List<ConnectionStat> recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections;
|
||||
factory _ServerConnectionStats.fromJson(Map<String, dynamic> json) => _$ServerConnectionStatsFromJson(json);
|
||||
|
||||
@override@HiveField(0) final String serverId;
|
||||
@override@HiveField(1) final String serverName;
|
||||
@override@HiveField(2) final int totalAttempts;
|
||||
@override@HiveField(3) final int successCount;
|
||||
@override@HiveField(4) final int failureCount;
|
||||
@override@JsonKey()@HiveField(5) final DateTime? lastSuccessTime;
|
||||
@override@JsonKey()@HiveField(6) final DateTime? lastFailureTime;
|
||||
final List<ConnectionStat> _recentConnections;
|
||||
@override@JsonKey()@HiveField(7) List<ConnectionStat> get recentConnections {
|
||||
if (_recentConnections is EqualUnmodifiableListView) return _recentConnections;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_recentConnections);
|
||||
}
|
||||
|
||||
@override@HiveField(8) final double successRate;
|
||||
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ServerConnectionStatsCopyWith<_ServerConnectionStats> get copyWith => __$ServerConnectionStatsCopyWithImpl<_ServerConnectionStats>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ServerConnectionStatsToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other._recentConnections, _recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(_recentConnections),successRate);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ServerConnectionStatsCopyWith<$Res> implements $ServerConnectionStatsCopyWith<$Res> {
|
||||
factory _$ServerConnectionStatsCopyWith(_ServerConnectionStats value, $Res Function(_ServerConnectionStats) _then) = __$ServerConnectionStatsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ServerConnectionStatsCopyWithImpl<$Res>
|
||||
implements _$ServerConnectionStatsCopyWith<$Res> {
|
||||
__$ServerConnectionStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ServerConnectionStats _self;
|
||||
final $Res Function(_ServerConnectionStats) _then;
|
||||
|
||||
/// Create a copy of ServerConnectionStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
|
||||
return _then(_ServerConnectionStats(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
|
||||
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,recentConnections: null == recentConnections ? _self._recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
|
||||
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
233
lib/data/model/server/connection_stat.g.dart
Normal file
233
lib/data/model/server/connection_stat.g.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'connection_stat.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ConnectionStatAdapter extends TypeAdapter<ConnectionStat> {
|
||||
@override
|
||||
final typeId = 100;
|
||||
|
||||
@override
|
||||
ConnectionStat read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ConnectionStat(
|
||||
serverId: fields[0] as String,
|
||||
serverName: fields[1] as String,
|
||||
timestamp: fields[2] as DateTime,
|
||||
result: fields[3] as ConnectionResult,
|
||||
errorMessage: fields[4] == null ? '' : fields[4] as String,
|
||||
durationMs: (fields[5] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ConnectionStat obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.serverId)
|
||||
..writeByte(1)
|
||||
..write(obj.serverName)
|
||||
..writeByte(2)
|
||||
..write(obj.timestamp)
|
||||
..writeByte(3)
|
||||
..write(obj.result)
|
||||
..writeByte(4)
|
||||
..write(obj.errorMessage)
|
||||
..writeByte(5)
|
||||
..write(obj.durationMs);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ConnectionStatAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class ServerConnectionStatsAdapter extends TypeAdapter<ServerConnectionStats> {
|
||||
@override
|
||||
final typeId = 101;
|
||||
|
||||
@override
|
||||
ServerConnectionStats read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ServerConnectionStats(
|
||||
serverId: fields[0] as String,
|
||||
serverName: fields[1] as String,
|
||||
totalAttempts: (fields[2] as num).toInt(),
|
||||
successCount: (fields[3] as num).toInt(),
|
||||
failureCount: (fields[4] as num).toInt(),
|
||||
lastSuccessTime: fields[5] == null ? null : fields[5] as DateTime?,
|
||||
lastFailureTime: fields[6] == null ? null : fields[6] as DateTime?,
|
||||
recentConnections: fields[7] == null
|
||||
? []
|
||||
: (fields[7] as List).cast<ConnectionStat>(),
|
||||
successRate: (fields[8] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerConnectionStats obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.serverId)
|
||||
..writeByte(1)
|
||||
..write(obj.serverName)
|
||||
..writeByte(2)
|
||||
..write(obj.totalAttempts)
|
||||
..writeByte(3)
|
||||
..write(obj.successCount)
|
||||
..writeByte(4)
|
||||
..write(obj.failureCount)
|
||||
..writeByte(5)
|
||||
..write(obj.lastSuccessTime)
|
||||
..writeByte(6)
|
||||
..write(obj.lastFailureTime)
|
||||
..writeByte(7)
|
||||
..write(obj.recentConnections)
|
||||
..writeByte(8)
|
||||
..write(obj.successRate);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ServerConnectionStatsAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class ConnectionResultAdapter extends TypeAdapter<ConnectionResult> {
|
||||
@override
|
||||
final typeId = 102;
|
||||
|
||||
@override
|
||||
ConnectionResult read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return ConnectionResult.success;
|
||||
case 1:
|
||||
return ConnectionResult.timeout;
|
||||
case 2:
|
||||
return ConnectionResult.authFailed;
|
||||
case 3:
|
||||
return ConnectionResult.networkError;
|
||||
case 4:
|
||||
return ConnectionResult.unknownError;
|
||||
default:
|
||||
return ConnectionResult.success;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ConnectionResult obj) {
|
||||
switch (obj) {
|
||||
case ConnectionResult.success:
|
||||
writer.writeByte(0);
|
||||
case ConnectionResult.timeout:
|
||||
writer.writeByte(1);
|
||||
case ConnectionResult.authFailed:
|
||||
writer.writeByte(2);
|
||||
case ConnectionResult.networkError:
|
||||
writer.writeByte(3);
|
||||
case ConnectionResult.unknownError:
|
||||
writer.writeByte(4);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ConnectionResultAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ConnectionStat _$ConnectionStatFromJson(Map<String, dynamic> json) =>
|
||||
_ConnectionStat(
|
||||
serverId: json['serverId'] as String,
|
||||
serverName: json['serverName'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
result: $enumDecode(_$ConnectionResultEnumMap, json['result']),
|
||||
errorMessage: json['errorMessage'] as String? ?? '',
|
||||
durationMs: (json['durationMs'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConnectionStatToJson(_ConnectionStat instance) =>
|
||||
<String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'serverName': instance.serverName,
|
||||
'timestamp': instance.timestamp.toIso8601String(),
|
||||
'result': _$ConnectionResultEnumMap[instance.result]!,
|
||||
'errorMessage': instance.errorMessage,
|
||||
'durationMs': instance.durationMs,
|
||||
};
|
||||
|
||||
const _$ConnectionResultEnumMap = {
|
||||
ConnectionResult.success: 'success',
|
||||
ConnectionResult.timeout: 'timeout',
|
||||
ConnectionResult.authFailed: 'auth_failed',
|
||||
ConnectionResult.networkError: 'network_error',
|
||||
ConnectionResult.unknownError: 'unknown_error',
|
||||
};
|
||||
|
||||
_ServerConnectionStats _$ServerConnectionStatsFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ServerConnectionStats(
|
||||
serverId: json['serverId'] as String,
|
||||
serverName: json['serverName'] as String,
|
||||
totalAttempts: (json['totalAttempts'] as num).toInt(),
|
||||
successCount: (json['successCount'] as num).toInt(),
|
||||
failureCount: (json['failureCount'] as num).toInt(),
|
||||
lastSuccessTime: json['lastSuccessTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastSuccessTime'] as String),
|
||||
lastFailureTime: json['lastFailureTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastFailureTime'] as String),
|
||||
recentConnections:
|
||||
(json['recentConnections'] as List<dynamic>?)
|
||||
?.map((e) => ConnectionStat.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
successRate: (json['successRate'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ServerConnectionStatsToJson(
|
||||
_ServerConnectionStats instance,
|
||||
) => <String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'serverName': instance.serverName,
|
||||
'totalAttempts': instance.totalAttempts,
|
||||
'successCount': instance.successCount,
|
||||
'failureCount': instance.failureCount,
|
||||
'lastSuccessTime': instance.lastSuccessTime?.toIso8601String(),
|
||||
'lastFailureTime': instance.lastFailureTime?.toIso8601String(),
|
||||
'recentConnections': instance.recentConnections,
|
||||
'successRate': instance.successRate,
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import 'package:server_box/data/res/status.dart';
|
||||
/// Capacity of the FIFO queue
|
||||
const _kCap = 30;
|
||||
|
||||
class Cpus extends TimeSeq<List<SingleCpuCore>> {
|
||||
class Cpus extends TimeSeq<SingleCpuCore> {
|
||||
Cpus(super.init1, super.init2);
|
||||
|
||||
final Map<String, int> brand = {};
|
||||
@@ -14,21 +14,30 @@ class Cpus extends TimeSeq<List<SingleCpuCore>> {
|
||||
@override
|
||||
void onUpdate() {
|
||||
_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;
|
||||
_user = _getUser();
|
||||
_sys = _getSys();
|
||||
_iowait = _getIowait();
|
||||
_idle = _getIdle();
|
||||
_updateSpots();
|
||||
//_updateRange();
|
||||
}
|
||||
|
||||
double usedPercent({int coreIdx = 0}) {
|
||||
if (now.length != pre.length) return 0;
|
||||
if (now.isEmpty) return 0;
|
||||
if (coreIdx >= now.length) return 0;
|
||||
try {
|
||||
final idleDelta = now[coreIdx].idle - pre[coreIdx].idle;
|
||||
final totalDelta = now[coreIdx].total - pre[coreIdx].total;
|
||||
if (totalDelta == 0) return 0;
|
||||
final used = idleDelta / totalDelta;
|
||||
return used.isNaN ? 0 : 100 - used * 100;
|
||||
} catch (e, s) {
|
||||
@@ -157,6 +166,7 @@ class SingleCpuCore extends TimeSeqIface<SingleCpuCore> {
|
||||
final id = item.split(' ').firstOrNull;
|
||||
if (id == null) continue;
|
||||
final matches = item.replaceFirst(id, '').trim().split(' ');
|
||||
if (matches.length < 7) continue;
|
||||
cpus.add(
|
||||
SingleCpuCore(
|
||||
id,
|
||||
|
||||
49
lib/data/model/server/discovery_result.dart
Normal file
49
lib/data/model/server/discovery_result.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'discovery_result.freezed.dart';
|
||||
part 'discovery_result.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class SshDiscoveryResult with _$SshDiscoveryResult {
|
||||
const factory SshDiscoveryResult({
|
||||
required String ip,
|
||||
required int port,
|
||||
String? banner,
|
||||
@Default(false) bool isSelected,
|
||||
}) = _SshDiscoveryResult;
|
||||
|
||||
factory SshDiscoveryResult.fromJson(Map<String, dynamic> json) => _$SshDiscoveryResultFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SshDiscoveryReport with _$SshDiscoveryReport {
|
||||
const factory SshDiscoveryReport({
|
||||
required String generatedAt,
|
||||
required int durationMs,
|
||||
required int count,
|
||||
required List<SshDiscoveryResult> items,
|
||||
}) = _SshDiscoveryReport;
|
||||
|
||||
factory SshDiscoveryReport.fromJson(Map<String, dynamic> json) => _$SshDiscoveryReportFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SshDiscoveryConfig with _$SshDiscoveryConfig {
|
||||
const factory SshDiscoveryConfig({
|
||||
@Default(700) int timeoutMs,
|
||||
@Default(128) int maxConcurrency,
|
||||
@Default(false) bool enableMdns,
|
||||
@Default(4096) int hostEnumerationLimit,
|
||||
}) = _SshDiscoveryConfig;
|
||||
}
|
||||
|
||||
extension SshDiscoveryConfigX on SshDiscoveryConfig {
|
||||
List<String> toArgs() {
|
||||
final args = <String>[];
|
||||
args.add('--timeout-ms=$timeoutMs');
|
||||
args.add('--max-concurrency=$maxConcurrency');
|
||||
args.add('--host-enumeration-limit=$hostEnumerationLimit');
|
||||
if (enableMdns) args.add('--enable-mdns');
|
||||
return args;
|
||||
}
|
||||
}
|
||||
830
lib/data/model/server/discovery_result.freezed.dart
Normal file
830
lib/data/model/server/discovery_result.freezed.dart
Normal file
@@ -0,0 +1,830 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'discovery_result.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SshDiscoveryResult {
|
||||
|
||||
String get ip; int get port; String? get banner; bool get isSelected;
|
||||
/// Create a copy of SshDiscoveryResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SshDiscoveryResultCopyWith<SshDiscoveryResult> get copyWith => _$SshDiscoveryResultCopyWithImpl<SshDiscoveryResult>(this as SshDiscoveryResult, _$identity);
|
||||
|
||||
/// Serializes this SshDiscoveryResult to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryResult&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.banner, banner) || other.banner == banner)&&(identical(other.isSelected, isSelected) || other.isSelected == isSelected));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,ip,port,banner,isSelected);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SshDiscoveryResult(ip: $ip, port: $port, banner: $banner, isSelected: $isSelected)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SshDiscoveryResultCopyWith<$Res> {
|
||||
factory $SshDiscoveryResultCopyWith(SshDiscoveryResult value, $Res Function(SshDiscoveryResult) _then) = _$SshDiscoveryResultCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String ip, int port, String? banner, bool isSelected
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SshDiscoveryResultCopyWithImpl<$Res>
|
||||
implements $SshDiscoveryResultCopyWith<$Res> {
|
||||
_$SshDiscoveryResultCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SshDiscoveryResult _self;
|
||||
final $Res Function(SshDiscoveryResult) _then;
|
||||
|
||||
/// Create a copy of SshDiscoveryResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? ip = null,Object? port = null,Object? banner = freezed,Object? isSelected = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
|
||||
as int,banner: freezed == banner ? _self.banner : banner // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isSelected: null == isSelected ? _self.isSelected : isSelected // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SshDiscoveryResult].
|
||||
extension SshDiscoveryResultPatterns on SshDiscoveryResult {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryResult value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryResult() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryResult value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryResult():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryResult value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryResult() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String ip, int port, String? banner, bool isSelected)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryResult() when $default != null:
|
||||
return $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String ip, int port, String? banner, bool isSelected) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryResult():
|
||||
return $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String ip, int port, String? banner, bool isSelected)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryResult() when $default != null:
|
||||
return $default(_that.ip,_that.port,_that.banner,_that.isSelected);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SshDiscoveryResult implements SshDiscoveryResult {
|
||||
const _SshDiscoveryResult({required this.ip, required this.port, this.banner, this.isSelected = false});
|
||||
factory _SshDiscoveryResult.fromJson(Map<String, dynamic> json) => _$SshDiscoveryResultFromJson(json);
|
||||
|
||||
@override final String ip;
|
||||
@override final int port;
|
||||
@override final String? banner;
|
||||
@override@JsonKey() final bool isSelected;
|
||||
|
||||
/// Create a copy of SshDiscoveryResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SshDiscoveryResultCopyWith<_SshDiscoveryResult> get copyWith => __$SshDiscoveryResultCopyWithImpl<_SshDiscoveryResult>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SshDiscoveryResultToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryResult&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.banner, banner) || other.banner == banner)&&(identical(other.isSelected, isSelected) || other.isSelected == isSelected));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,ip,port,banner,isSelected);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SshDiscoveryResult(ip: $ip, port: $port, banner: $banner, isSelected: $isSelected)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SshDiscoveryResultCopyWith<$Res> implements $SshDiscoveryResultCopyWith<$Res> {
|
||||
factory _$SshDiscoveryResultCopyWith(_SshDiscoveryResult value, $Res Function(_SshDiscoveryResult) _then) = __$SshDiscoveryResultCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String ip, int port, String? banner, bool isSelected
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SshDiscoveryResultCopyWithImpl<$Res>
|
||||
implements _$SshDiscoveryResultCopyWith<$Res> {
|
||||
__$SshDiscoveryResultCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SshDiscoveryResult _self;
|
||||
final $Res Function(_SshDiscoveryResult) _then;
|
||||
|
||||
/// Create a copy of SshDiscoveryResult
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? ip = null,Object? port = null,Object? banner = freezed,Object? isSelected = null,}) {
|
||||
return _then(_SshDiscoveryResult(
|
||||
ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||
as String,port: null == port ? _self.port : port // ignore: cast_nullable_to_non_nullable
|
||||
as int,banner: freezed == banner ? _self.banner : banner // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isSelected: null == isSelected ? _self.isSelected : isSelected // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SshDiscoveryReport {
|
||||
|
||||
String get generatedAt; int get durationMs; int get count; List<SshDiscoveryResult> get items;
|
||||
/// Create a copy of SshDiscoveryReport
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SshDiscoveryReportCopyWith<SshDiscoveryReport> get copyWith => _$SshDiscoveryReportCopyWithImpl<SshDiscoveryReport>(this as SshDiscoveryReport, _$identity);
|
||||
|
||||
/// Serializes this SshDiscoveryReport to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryReport&&(identical(other.generatedAt, generatedAt) || other.generatedAt == generatedAt)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs)&&(identical(other.count, count) || other.count == count)&&const DeepCollectionEquality().equals(other.items, items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,generatedAt,durationMs,count,const DeepCollectionEquality().hash(items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SshDiscoveryReport(generatedAt: $generatedAt, durationMs: $durationMs, count: $count, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SshDiscoveryReportCopyWith<$Res> {
|
||||
factory $SshDiscoveryReportCopyWith(SshDiscoveryReport value, $Res Function(SshDiscoveryReport) _then) = _$SshDiscoveryReportCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SshDiscoveryReportCopyWithImpl<$Res>
|
||||
implements $SshDiscoveryReportCopyWith<$Res> {
|
||||
_$SshDiscoveryReportCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SshDiscoveryReport _self;
|
||||
final $Res Function(SshDiscoveryReport) _then;
|
||||
|
||||
/// Create a copy of SshDiscoveryReport
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? generatedAt = null,Object? durationMs = null,Object? count = null,Object? items = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
generatedAt: null == generatedAt ? _self.generatedAt : generatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
|
||||
as int,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<SshDiscoveryResult>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SshDiscoveryReport].
|
||||
extension SshDiscoveryReportPatterns on SshDiscoveryReport {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryReport value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryReport() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryReport value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryReport():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryReport value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryReport() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryReport() when $default != null:
|
||||
return $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryReport():
|
||||
return $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryReport() when $default != null:
|
||||
return $default(_that.generatedAt,_that.durationMs,_that.count,_that.items);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SshDiscoveryReport implements SshDiscoveryReport {
|
||||
const _SshDiscoveryReport({required this.generatedAt, required this.durationMs, required this.count, required final List<SshDiscoveryResult> items}): _items = items;
|
||||
factory _SshDiscoveryReport.fromJson(Map<String, dynamic> json) => _$SshDiscoveryReportFromJson(json);
|
||||
|
||||
@override final String generatedAt;
|
||||
@override final int durationMs;
|
||||
@override final int count;
|
||||
final List<SshDiscoveryResult> _items;
|
||||
@override List<SshDiscoveryResult> get items {
|
||||
if (_items is EqualUnmodifiableListView) return _items;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_items);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of SshDiscoveryReport
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SshDiscoveryReportCopyWith<_SshDiscoveryReport> get copyWith => __$SshDiscoveryReportCopyWithImpl<_SshDiscoveryReport>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SshDiscoveryReportToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryReport&&(identical(other.generatedAt, generatedAt) || other.generatedAt == generatedAt)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs)&&(identical(other.count, count) || other.count == count)&&const DeepCollectionEquality().equals(other._items, _items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,generatedAt,durationMs,count,const DeepCollectionEquality().hash(_items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SshDiscoveryReport(generatedAt: $generatedAt, durationMs: $durationMs, count: $count, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SshDiscoveryReportCopyWith<$Res> implements $SshDiscoveryReportCopyWith<$Res> {
|
||||
factory _$SshDiscoveryReportCopyWith(_SshDiscoveryReport value, $Res Function(_SshDiscoveryReport) _then) = __$SshDiscoveryReportCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String generatedAt, int durationMs, int count, List<SshDiscoveryResult> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SshDiscoveryReportCopyWithImpl<$Res>
|
||||
implements _$SshDiscoveryReportCopyWith<$Res> {
|
||||
__$SshDiscoveryReportCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SshDiscoveryReport _self;
|
||||
final $Res Function(_SshDiscoveryReport) _then;
|
||||
|
||||
/// Create a copy of SshDiscoveryReport
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? generatedAt = null,Object? durationMs = null,Object? count = null,Object? items = null,}) {
|
||||
return _then(_SshDiscoveryReport(
|
||||
generatedAt: null == generatedAt ? _self.generatedAt : generatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
|
||||
as int,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<SshDiscoveryResult>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SshDiscoveryConfig {
|
||||
|
||||
int get timeoutMs; int get maxConcurrency; bool get enableMdns; int get hostEnumerationLimit;
|
||||
/// Create a copy of SshDiscoveryConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SshDiscoveryConfigCopyWith<SshDiscoveryConfig> get copyWith => _$SshDiscoveryConfigCopyWithImpl<SshDiscoveryConfig>(this as SshDiscoveryConfig, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SshDiscoveryConfig&&(identical(other.timeoutMs, timeoutMs) || other.timeoutMs == timeoutMs)&&(identical(other.maxConcurrency, maxConcurrency) || other.maxConcurrency == maxConcurrency)&&(identical(other.enableMdns, enableMdns) || other.enableMdns == enableMdns)&&(identical(other.hostEnumerationLimit, hostEnumerationLimit) || other.hostEnumerationLimit == hostEnumerationLimit));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,timeoutMs,maxConcurrency,enableMdns,hostEnumerationLimit);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SshDiscoveryConfig(timeoutMs: $timeoutMs, maxConcurrency: $maxConcurrency, enableMdns: $enableMdns, hostEnumerationLimit: $hostEnumerationLimit)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SshDiscoveryConfigCopyWith<$Res> {
|
||||
factory $SshDiscoveryConfigCopyWith(SshDiscoveryConfig value, $Res Function(SshDiscoveryConfig) _then) = _$SshDiscoveryConfigCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SshDiscoveryConfigCopyWithImpl<$Res>
|
||||
implements $SshDiscoveryConfigCopyWith<$Res> {
|
||||
_$SshDiscoveryConfigCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SshDiscoveryConfig _self;
|
||||
final $Res Function(SshDiscoveryConfig) _then;
|
||||
|
||||
/// Create a copy of SshDiscoveryConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? timeoutMs = null,Object? maxConcurrency = null,Object? enableMdns = null,Object? hostEnumerationLimit = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
timeoutMs: null == timeoutMs ? _self.timeoutMs : timeoutMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,maxConcurrency: null == maxConcurrency ? _self.maxConcurrency : maxConcurrency // ignore: cast_nullable_to_non_nullable
|
||||
as int,enableMdns: null == enableMdns ? _self.enableMdns : enableMdns // ignore: cast_nullable_to_non_nullable
|
||||
as bool,hostEnumerationLimit: null == hostEnumerationLimit ? _self.hostEnumerationLimit : hostEnumerationLimit // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SshDiscoveryConfig].
|
||||
extension SshDiscoveryConfigPatterns on SshDiscoveryConfig {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SshDiscoveryConfig value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryConfig() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SshDiscoveryConfig value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryConfig():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SshDiscoveryConfig value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryConfig() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryConfig() when $default != null:
|
||||
return $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryConfig():
|
||||
return $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SshDiscoveryConfig() when $default != null:
|
||||
return $default(_that.timeoutMs,_that.maxConcurrency,_that.enableMdns,_that.hostEnumerationLimit);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _SshDiscoveryConfig implements SshDiscoveryConfig {
|
||||
const _SshDiscoveryConfig({this.timeoutMs = 700, this.maxConcurrency = 128, this.enableMdns = false, this.hostEnumerationLimit = 4096});
|
||||
|
||||
|
||||
@override@JsonKey() final int timeoutMs;
|
||||
@override@JsonKey() final int maxConcurrency;
|
||||
@override@JsonKey() final bool enableMdns;
|
||||
@override@JsonKey() final int hostEnumerationLimit;
|
||||
|
||||
/// Create a copy of SshDiscoveryConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SshDiscoveryConfigCopyWith<_SshDiscoveryConfig> get copyWith => __$SshDiscoveryConfigCopyWithImpl<_SshDiscoveryConfig>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SshDiscoveryConfig&&(identical(other.timeoutMs, timeoutMs) || other.timeoutMs == timeoutMs)&&(identical(other.maxConcurrency, maxConcurrency) || other.maxConcurrency == maxConcurrency)&&(identical(other.enableMdns, enableMdns) || other.enableMdns == enableMdns)&&(identical(other.hostEnumerationLimit, hostEnumerationLimit) || other.hostEnumerationLimit == hostEnumerationLimit));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,timeoutMs,maxConcurrency,enableMdns,hostEnumerationLimit);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SshDiscoveryConfig(timeoutMs: $timeoutMs, maxConcurrency: $maxConcurrency, enableMdns: $enableMdns, hostEnumerationLimit: $hostEnumerationLimit)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SshDiscoveryConfigCopyWith<$Res> implements $SshDiscoveryConfigCopyWith<$Res> {
|
||||
factory _$SshDiscoveryConfigCopyWith(_SshDiscoveryConfig value, $Res Function(_SshDiscoveryConfig) _then) = __$SshDiscoveryConfigCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int timeoutMs, int maxConcurrency, bool enableMdns, int hostEnumerationLimit
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SshDiscoveryConfigCopyWithImpl<$Res>
|
||||
implements _$SshDiscoveryConfigCopyWith<$Res> {
|
||||
__$SshDiscoveryConfigCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SshDiscoveryConfig _self;
|
||||
final $Res Function(_SshDiscoveryConfig) _then;
|
||||
|
||||
/// Create a copy of SshDiscoveryConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? timeoutMs = null,Object? maxConcurrency = null,Object? enableMdns = null,Object? hostEnumerationLimit = null,}) {
|
||||
return _then(_SshDiscoveryConfig(
|
||||
timeoutMs: null == timeoutMs ? _self.timeoutMs : timeoutMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,maxConcurrency: null == maxConcurrency ? _self.maxConcurrency : maxConcurrency // ignore: cast_nullable_to_non_nullable
|
||||
as int,enableMdns: null == enableMdns ? _self.enableMdns : enableMdns // ignore: cast_nullable_to_non_nullable
|
||||
as bool,hostEnumerationLimit: null == hostEnumerationLimit ? _self.hostEnumerationLimit : hostEnumerationLimit // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
41
lib/data/model/server/discovery_result.g.dart
Normal file
41
lib/data/model/server/discovery_result.g.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'discovery_result.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SshDiscoveryResult _$SshDiscoveryResultFromJson(Map<String, dynamic> json) =>
|
||||
_SshDiscoveryResult(
|
||||
ip: json['ip'] as String,
|
||||
port: (json['port'] as num).toInt(),
|
||||
banner: json['banner'] as String?,
|
||||
isSelected: json['isSelected'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SshDiscoveryResultToJson(_SshDiscoveryResult instance) =>
|
||||
<String, dynamic>{
|
||||
'ip': instance.ip,
|
||||
'port': instance.port,
|
||||
'banner': instance.banner,
|
||||
'isSelected': instance.isSelected,
|
||||
};
|
||||
|
||||
_SshDiscoveryReport _$SshDiscoveryReportFromJson(Map<String, dynamic> json) =>
|
||||
_SshDiscoveryReport(
|
||||
generatedAt: json['generatedAt'] as String,
|
||||
durationMs: (json['durationMs'] as num).toInt(),
|
||||
count: (json['count'] as num).toInt(),
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((e) => SshDiscoveryResult.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SshDiscoveryReportToJson(_SshDiscoveryReport instance) =>
|
||||
<String, dynamic>{
|
||||
'generatedAt': instance.generatedAt,
|
||||
'durationMs': instance.durationMs,
|
||||
'count': instance.count,
|
||||
'items': instance.items,
|
||||
};
|
||||
@@ -44,22 +44,49 @@ class Disk with EquatableMixin {
|
||||
static List<Disk> parse(String raw) {
|
||||
final list = <Disk>[];
|
||||
raw = raw.trim();
|
||||
try {
|
||||
if (raw.startsWith('{')) {
|
||||
// Parse JSON output from lsblk command
|
||||
final Map<String, dynamic> jsonData = json.decode(raw);
|
||||
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
|
||||
|
||||
if (raw.isEmpty) {
|
||||
dprint('Empty disk info data received');
|
||||
return list;
|
||||
}
|
||||
|
||||
for (final device in blockdevices) {
|
||||
// Process each device
|
||||
_processTopLevelDevice(device, list);
|
||||
try {
|
||||
// Check if we have lsblk JSON output with success marker
|
||||
if (raw.startsWith('{')) {
|
||||
// Extract JSON part (excluding the success marker if present)
|
||||
final jsonEnd = raw.indexOf('\nLSBLK_SUCCESS');
|
||||
final jsonPart = jsonEnd > 0 ? raw.substring(0, jsonEnd) : raw;
|
||||
|
||||
try {
|
||||
final Map<String, dynamic> jsonData = json.decode(jsonPart);
|
||||
final List<dynamic> blockdevices = jsonData['blockdevices'] ?? [];
|
||||
|
||||
for (final device in blockdevices) {
|
||||
// Process each device
|
||||
_processTopLevelDevice(device, list);
|
||||
}
|
||||
|
||||
// If we successfully parsed JSON and have valid disks, return them
|
||||
if (list.isNotEmpty) {
|
||||
return list;
|
||||
}
|
||||
} on FormatException catch (e) {
|
||||
Loggers.app.warning('JSON parsing failed, falling back to df -k output: $e');
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Error processing JSON disk data, falling back to df -k output: $e', e);
|
||||
}
|
||||
} else {
|
||||
// Fallback to the old parsing method in case of non-JSON output
|
||||
}
|
||||
|
||||
// Check if we have df -k output (fallback case)
|
||||
if (raw.contains('Filesystem') && raw.contains('Mounted on')) {
|
||||
return _parseWithOldMethod(raw);
|
||||
}
|
||||
|
||||
// If we reach here, both parsing methods failed
|
||||
Loggers.app.warning('Unable to parse disk info with any method');
|
||||
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Failed to parse disk info: $e', e);
|
||||
Loggers.app.warning('Failed to parse disk info with both methods: $e', e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -88,6 +115,32 @@ class Disk with EquatableMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse filesystem fields from device data
|
||||
static ({BigInt size, BigInt used, BigInt avail, int usedPercent}) _parseFilesystemFields(Map<String, dynamic> device) {
|
||||
// Helper function to parse size strings safely
|
||||
BigInt parseSize(String? sizeStr) {
|
||||
if (sizeStr == null || sizeStr.isEmpty || sizeStr == 'null' || sizeStr == '0') {
|
||||
return BigInt.zero;
|
||||
}
|
||||
return (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
}
|
||||
|
||||
// Helper function to parse percentage strings
|
||||
int parsePercent(String? percentStr) {
|
||||
if (percentStr == null || percentStr.isEmpty || percentStr == 'null') {
|
||||
return 0;
|
||||
}
|
||||
return int.tryParse(percentStr.replaceAll('%', '')) ?? 0;
|
||||
}
|
||||
|
||||
return (
|
||||
size: parseSize(device['fssize']?.toString()),
|
||||
used: parseSize(device['fsused']?.toString()),
|
||||
avail: parseSize(device['fsavail']?.toString()),
|
||||
usedPercent: parsePercent(device['fsuse%']?.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Process a single device without recursively processing its children
|
||||
static Disk? _processSingleDevice(Map<String, dynamic> device) {
|
||||
final fstype = device['fstype']?.toString();
|
||||
@@ -102,20 +155,7 @@ class Disk with EquatableMixin {
|
||||
return null;
|
||||
}
|
||||
|
||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
||||
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final usedStr = device['fsused']?.toString() ?? '0';
|
||||
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final availStr = device['fsavail']?.toString() ?? '0';
|
||||
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
// Parse fsuse% which is usually in the format "45%"
|
||||
String usePercentStr = device['fsuse%']?.toString() ?? '0';
|
||||
usePercentStr = usePercentStr.replaceAll('%', '');
|
||||
final usedPercent = int.tryParse(usePercentStr) ?? 0;
|
||||
|
||||
final fsFields = _parseFilesystemFields(device);
|
||||
final name = device['name']?.toString();
|
||||
final kname = device['kname']?.toString();
|
||||
final uuid = device['uuid']?.toString();
|
||||
@@ -124,10 +164,10 @@ class Disk with EquatableMixin {
|
||||
path: path,
|
||||
fsTyp: fstype,
|
||||
mount: mountpoint,
|
||||
usedPercent: usedPercent,
|
||||
used: used,
|
||||
size: size,
|
||||
avail: avail,
|
||||
usedPercent: fsFields.usedPercent,
|
||||
used: fsFields.used,
|
||||
size: fsFields.size,
|
||||
avail: fsFields.avail,
|
||||
name: name,
|
||||
kname: kname,
|
||||
uuid: uuid,
|
||||
@@ -155,20 +195,7 @@ class Disk with EquatableMixin {
|
||||
|
||||
// Handle common filesystem cases or parent devices with children
|
||||
if ((fstype != null && _shouldCalc(fstype, mount)) || (childDisks.isNotEmpty && path.isNotEmpty)) {
|
||||
final sizeStr = device['fssize']?.toString() ?? '0';
|
||||
final size = (BigInt.tryParse(sizeStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final usedStr = device['fsused']?.toString() ?? '0';
|
||||
final used = (BigInt.tryParse(usedStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
final availStr = device['fsavail']?.toString() ?? '0';
|
||||
final avail = (BigInt.tryParse(availStr) ?? BigInt.zero) ~/ BigInt.from(1024);
|
||||
|
||||
// Parse fsuse% which is usually in the format "45%"
|
||||
String usePercentStr = device['fsuse%']?.toString() ?? '0';
|
||||
usePercentStr = usePercentStr.replaceAll('%', '');
|
||||
final usedPercent = int.tryParse(usePercentStr) ?? 0;
|
||||
|
||||
final fsFields = _parseFilesystemFields(device);
|
||||
final name = device['name']?.toString();
|
||||
final kname = device['kname']?.toString();
|
||||
final uuid = device['uuid']?.toString();
|
||||
@@ -177,10 +204,10 @@ class Disk with EquatableMixin {
|
||||
path: path,
|
||||
fsTyp: fstype,
|
||||
mount: mount,
|
||||
usedPercent: usedPercent,
|
||||
used: used,
|
||||
size: size,
|
||||
avail: avail,
|
||||
usedPercent: fsFields.usedPercent,
|
||||
used: fsFields.used,
|
||||
size: fsFields.size,
|
||||
avail: fsFields.avail,
|
||||
name: name,
|
||||
kname: kname,
|
||||
uuid: uuid,
|
||||
@@ -253,7 +280,7 @@ class Disk with EquatableMixin {
|
||||
];
|
||||
}
|
||||
|
||||
class DiskIO extends TimeSeq<List<DiskIOPiece>> {
|
||||
class DiskIO extends TimeSeq<DiskIOPiece> {
|
||||
DiskIO(super.init1, super.init2);
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,7 +18,7 @@ class NetSpeedPart extends TimeSeqIface<NetSpeedPart> {
|
||||
|
||||
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);
|
||||
|
||||
@override
|
||||
@@ -164,7 +164,8 @@ class NetSpeed extends TimeSeq<List<NetSpeedPart>> {
|
||||
final bytesIn = BigInt.parse(bytes.first);
|
||||
final bytesOut = BigInt.parse(bytes[8]);
|
||||
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
||||
} catch (_) {
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to parse net speed data: $item', e, s);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ class Proc {
|
||||
}
|
||||
|
||||
String get binary {
|
||||
final parts = command.split(' ');
|
||||
return parts[0];
|
||||
final parts = command.trim().split(' ').where((e) => e.isNotEmpty).toList();
|
||||
return parts.isNotEmpty ? parts[0] : '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||
import 'package:server_box/data/model/server/amd.dart';
|
||||
@@ -11,25 +10,9 @@ import 'package:server_box/data/model/server/memory.dart';
|
||||
import 'package:server_box/data/model/server/net_speed.dart';
|
||||
import 'package:server_box/data/model/server/nvdia.dart';
|
||||
import 'package:server_box/data/model/server/sensors.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/temp.dart';
|
||||
|
||||
class Server {
|
||||
Spi spi;
|
||||
ServerStatus status;
|
||||
SSHClient? client;
|
||||
ServerConn conn;
|
||||
|
||||
Server(this.spi, this.status, this.conn, {this.client});
|
||||
|
||||
bool get needGenClient => conn < ServerConn.connecting;
|
||||
|
||||
bool get canViewDetails => conn == ServerConn.finished;
|
||||
|
||||
String get id => spi.id;
|
||||
}
|
||||
|
||||
class ServerStatus {
|
||||
Cpus cpu;
|
||||
Memory mem;
|
||||
|
||||
@@ -4,10 +4,8 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
|
||||
part 'server_private_info.freezed.dart';
|
||||
@@ -58,6 +56,7 @@ abstract class Spi with _$Spi {
|
||||
@override
|
||||
String toString() => 'Spi<$oldId>';
|
||||
|
||||
/// Parse the [id], if it's null or empty, generate a new one.
|
||||
static String parseId(Object? id) {
|
||||
if (id == null || id is! String || id.isEmpty) return ShortId.generate();
|
||||
return id;
|
||||
@@ -85,23 +84,26 @@ extension Spix on Spi {
|
||||
return newSpi.id;
|
||||
}
|
||||
|
||||
/// Json encode to string.
|
||||
String toJsonString() => json.encode(toJson());
|
||||
|
||||
VNode<Server>? get server => ServerProvider.pick(spi: this);
|
||||
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
|
||||
|
||||
bool shouldReconnect(Spi old) {
|
||||
return user != old.user ||
|
||||
ip != old.ip ||
|
||||
port != old.port ||
|
||||
pwd != old.pwd ||
|
||||
keyId != old.keyId ||
|
||||
alterUrl != old.alterUrl ||
|
||||
jumpId != old.jumpId ||
|
||||
custom?.cmds != old.custom?.cmds;
|
||||
/// Returns true if the connection info is the same as [other].
|
||||
bool isSameAs(Spi other) {
|
||||
return user == other.user &&
|
||||
ip == other.ip &&
|
||||
port == other.port &&
|
||||
pwd == other.pwd &&
|
||||
keyId == other.keyId &&
|
||||
jumpId == other.jumpId;
|
||||
}
|
||||
|
||||
(String ip, String usr, int port) fromStringUrl() {
|
||||
/// Returns true if the connection should be re-established.
|
||||
bool shouldReconnect(Spi old) {
|
||||
return !isSameAs(old) || alterUrl != old.alterUrl || custom?.cmds != old.custom?.cmds;
|
||||
}
|
||||
|
||||
/// Parse the [alterUrl] to (ip, user, port).
|
||||
(String ip, String usr, int port) parseAlterUrl() {
|
||||
if (alterUrl == null) {
|
||||
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
|
||||
}
|
||||
@@ -146,5 +148,6 @@ extension Spix on Spi {
|
||||
id: 'id',
|
||||
);
|
||||
|
||||
/// Returns true if the user is 'root'.
|
||||
bool get isRoot => user == 'root';
|
||||
}
|
||||
|
||||
@@ -104,6 +104,11 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
|
||||
try {
|
||||
req.ss.disk = Disk.parse(StatusCmdType.disk.findInMap(parsedOutput));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.diskUsage = DiskUsage.parse(req.ss.disk);
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
@@ -294,7 +299,9 @@ String? _parseSysVer(String raw) {
|
||||
String? _parseHostName(String raw) {
|
||||
if (raw.isEmpty) return null;
|
||||
if (raw.contains(ScriptConstants.scriptFile)) return null;
|
||||
return raw;
|
||||
final trimmed = raw.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// Windows status parsing implementation
|
||||
@@ -371,18 +378,27 @@ void _parseWindowsCpuData(ServerStatusUpdateReq req, Map<String, String> parsedO
|
||||
// Windows CPU parsing - JSON format from PowerShell
|
||||
final cpuRaw = WindowsStatusCmdType.cpu.findInMap(parsedOutput);
|
||||
if (cpuRaw.isNotEmpty && cpuRaw != 'null' && !cpuRaw.contains('error') && !cpuRaw.contains('Exception')) {
|
||||
final cpus = WindowsParser.parseCpu(cpuRaw, req.ss);
|
||||
if (cpus.isNotEmpty) {
|
||||
req.ss.cpu.update(cpus);
|
||||
final cpuResult = WindowsParser.parseCpu(cpuRaw, req.ss);
|
||||
if (cpuResult.cores.isNotEmpty) {
|
||||
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) {
|
||||
Loggers.app.warning('Windows CPU parsing failed: $e', s);
|
||||
}
|
||||
@@ -420,8 +436,11 @@ void _parseWindowsDiskData(ServerStatusUpdateReq req, Map<String, String> parsed
|
||||
/// Parse Windows uptime data
|
||||
void _parseWindowsUptimeData(ServerStatusUpdateReq req, Map<String, String> parsedOutput) {
|
||||
try {
|
||||
final uptime = WindowsParser.parseUpTime(WindowsStatusCmdType.uptime.findInMap(parsedOutput));
|
||||
if (uptime != null) {
|
||||
final uptimeRaw = WindowsStatusCmdType.uptime.findInMap(parsedOutput);
|
||||
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;
|
||||
}
|
||||
} catch (e, s) {
|
||||
@@ -534,38 +553,36 @@ List<NetSpeedPart> _parseWindowsNetwork(String raw, int currentTime) {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<NetSpeedPart> netParts = [];
|
||||
|
||||
// PowerShell Get-Counter returns a structure with CounterSamples
|
||||
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
|
||||
final samples = jsonData['CounterSamples'] as List?;
|
||||
if (samples != null && samples.length >= 2) {
|
||||
// We need 2 samples to calculate speed (interval between them)
|
||||
final Map<String, double> interfaceRx = {};
|
||||
final Map<String, double> interfaceTx = {};
|
||||
|
||||
for (final sample in samples) {
|
||||
final path = sample['Path']?.toString() ?? '';
|
||||
final cookedValue = sample['CookedValue'] as num? ?? 0;
|
||||
|
||||
if (path.contains('Bytes Received/sec')) {
|
||||
final interfaceName = _extractInterfaceName(path);
|
||||
if (interfaceName.isNotEmpty) {
|
||||
interfaceRx[interfaceName] = cookedValue.toDouble();
|
||||
}
|
||||
} else if (path.contains('Bytes Sent/sec')) {
|
||||
final interfaceName = _extractInterfaceName(path);
|
||||
if (interfaceName.isNotEmpty) {
|
||||
interfaceTx[interfaceName] = cookedValue.toDouble();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create NetSpeedPart for each interface
|
||||
for (final interfaceName in interfaceRx.keys) {
|
||||
final rx = interfaceRx[interfaceName] ?? 0;
|
||||
final tx = interfaceTx[interfaceName] ?? 0;
|
||||
|
||||
if (jsonData is List && jsonData.length >= 2) {
|
||||
var sample1 = jsonData[jsonData.length - 2];
|
||||
var sample2 = jsonData[jsonData.length - 1];
|
||||
if (sample1 is Map && sample1.containsKey('value')) {
|
||||
sample1 = sample1['value'];
|
||||
}
|
||||
if (sample2 is Map && sample2.containsKey('value')) {
|
||||
sample2 = sample2['value'];
|
||||
}
|
||||
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
|
||||
for (int i = 0; i < sample1.length; i++) {
|
||||
final s1 = sample1[i];
|
||||
final s2 = sample2[i];
|
||||
final name = s1['Name']?.toString() ?? '';
|
||||
if (name.isEmpty || name == '_Total') continue;
|
||||
final rx1 = (s1['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
|
||||
final rx2 = (s2['BytesReceivedPersec'] as num?)?.toDouble() ?? 0;
|
||||
final tx1 = (s1['BytesSentPersec'] as num?)?.toDouble() ?? 0;
|
||||
final tx2 = (s2['BytesSentPersec'] as num?)?.toDouble() ?? 0;
|
||||
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 rxDelta = rx2 - rx1;
|
||||
final txDelta = tx2 - tx1;
|
||||
if (rxDelta < 0 || txDelta < 0) continue;
|
||||
final rxSpeed = rxDelta / timeDelta;
|
||||
final txSpeed = txDelta / timeDelta;
|
||||
netParts.add(
|
||||
NetSpeedPart(interfaceName, BigInt.from(rx.toInt()), BigInt.from(tx.toInt()), currentTime),
|
||||
NetSpeedPart(name, BigInt.from(rxSpeed.toInt()), BigInt.from(txSpeed.toInt()), currentTime),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -577,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) {
|
||||
try {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<DiskIOPiece> diskParts = [];
|
||||
|
||||
// PowerShell Get-Counter returns a structure with CounterSamples
|
||||
if (jsonData is Map && jsonData.containsKey('CounterSamples')) {
|
||||
final samples = jsonData['CounterSamples'] as List?;
|
||||
if (samples != null) {
|
||||
final Map<String, double> diskReads = {};
|
||||
final Map<String, double> diskWrites = {};
|
||||
|
||||
for (final sample in samples) {
|
||||
final path = sample['Path']?.toString() ?? '';
|
||||
final cookedValue = sample['CookedValue'] as num? ?? 0;
|
||||
|
||||
if (path.contains('Disk Read Bytes/sec')) {
|
||||
final diskName = _extractDiskName(path);
|
||||
if (diskName.isNotEmpty) {
|
||||
diskReads[diskName] = cookedValue.toDouble();
|
||||
}
|
||||
} else if (path.contains('Disk Write Bytes/sec')) {
|
||||
final diskName = _extractDiskName(path);
|
||||
if (diskName.isNotEmpty) {
|
||||
diskWrites[diskName] = cookedValue.toDouble();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create DiskIOPiece for each disk - convert bytes to sectors
|
||||
// (assuming 512 bytes per sector)
|
||||
for (final diskName in diskReads.keys) {
|
||||
final readBytes = diskReads[diskName] ?? 0;
|
||||
final writeBytes = diskWrites[diskName] ?? 0;
|
||||
final sectorsRead = (readBytes / 512).round();
|
||||
final sectorsWrite = (writeBytes / 512).round();
|
||||
if (jsonData is List && jsonData.length >= 2) {
|
||||
var sample1 = jsonData[jsonData.length - 2];
|
||||
var sample2 = jsonData[jsonData.length - 1];
|
||||
if (sample1 is Map && sample1.containsKey('value')) {
|
||||
sample1 = sample1['value'];
|
||||
}
|
||||
if (sample2 is Map && sample2.containsKey('value')) {
|
||||
sample2 = sample2['value'];
|
||||
}
|
||||
if (sample1 is List && sample2 is List && sample1.length == sample2.length) {
|
||||
for (int i = 0; i < sample1.length; i++) {
|
||||
final s1 = sample1[i];
|
||||
final s2 = sample2[i];
|
||||
final name = s1['Name']?.toString() ?? '';
|
||||
if (name.isEmpty || name == '_Total') continue;
|
||||
final read1 = (s1['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||
final read2 = (s2['DiskReadBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||
final write1 = (s1['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||
final write2 = (s2['DiskWriteBytesPersec'] as num?)?.toDouble() ?? 0;
|
||||
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;
|
||||
final writeDelta = write2 - write1;
|
||||
if (readDelta < 0 || writeDelta < 0) continue;
|
||||
final readSpeed = readDelta / timeDelta;
|
||||
final writeSpeed = writeDelta / timeDelta;
|
||||
final sectorsRead = (readSpeed / 512).round();
|
||||
final sectorsWrite = (writeSpeed / 512).round();
|
||||
|
||||
diskParts.add(
|
||||
DiskIOPiece(
|
||||
dev: diskName,
|
||||
dev: name,
|
||||
sectorsRead: sectorsRead,
|
||||
sectorsWrite: sectorsWrite,
|
||||
time: currentTime,
|
||||
@@ -639,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) {
|
||||
try {
|
||||
// Handle error output
|
||||
@@ -677,7 +679,7 @@ void _parseWindowsTemperatures(Temperatures temps, String raw) {
|
||||
if (typeLines.isNotEmpty && valueLines.isNotEmpty) {
|
||||
temps.parse(typeLines.join('\n'), valueLines.join('\n'));
|
||||
}
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, ignore temperature data
|
||||
} catch (e, s) {
|
||||
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] /
|
||||
/// [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];
|
||||
}
|
||||
|
||||
T get now {
|
||||
List<T> get now {
|
||||
return _list[length - 1];
|
||||
}
|
||||
|
||||
void onUpdate();
|
||||
|
||||
void update(T new_) {
|
||||
void update(List<T> new_) {
|
||||
add(new_);
|
||||
|
||||
if (pre.length != now.length) {
|
||||
pre.removeWhere((e) => now.any((el) => e.same(el)));
|
||||
pre.addAll(now.where((e) => pre.every((el) => !e.same(el))));
|
||||
final previous = pre.toList(growable: false);
|
||||
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();
|
||||
|
||||
@@ -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/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
|
||||
///
|
||||
/// This module handles parsing of Windows PowerShell command outputs
|
||||
@@ -94,30 +101,75 @@ class WindowsParser {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
final dynamic jsonData = json.decode(raw);
|
||||
final List<SingleCpuCore> cpus = [];
|
||||
int totalCoreCount = 1;
|
||||
|
||||
if (jsonData is List) {
|
||||
for (int i = 0; i < jsonData.length; i++) {
|
||||
final cpu = jsonData[i];
|
||||
final loadPercentage = cpu['LoadPercentage'] ?? 0;
|
||||
final usage = loadPercentage as int;
|
||||
// Multiple physical processors
|
||||
totalCoreCount = 0; // Reset to sum up
|
||||
var logicalProcessorOffset = 0;
|
||||
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;
|
||||
|
||||
// Get previous CPU data to calculate cumulative values
|
||||
final prevCpus = serverStatus.cpu.now;
|
||||
final prevCpu = i < prevCpus.length ? prevCpus[i] : null;
|
||||
// Create a SingleCpuCore entry for each logical processor
|
||||
// Windows only reports overall CPU load, so we distribute it evenly
|
||||
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
|
||||
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
|
||||
// We simulate cumulative counters by adding current percentages to previous totals.
|
||||
// This approach has limitations:
|
||||
// 1. Not as accurate as true cumulative time counters (Linux /proc/stat)
|
||||
// 2. May drift over time with variable polling intervals
|
||||
// 3. Results depend on consistent polling frequency
|
||||
// However, this allows compatibility with existing delta-based CPU calculation logic.
|
||||
// LIMITATION: Windows CPU counters approach
|
||||
// PowerShell provides LoadPercentage as instantaneous percentage, not cumulative time.
|
||||
// We simulate cumulative counters by adding current percentages to previous totals.
|
||||
// Additionally, Windows only provides overall CPU load, not per-core load.
|
||||
// We distribute the load evenly across all logical processors.
|
||||
final newUser = (prevCpu?.user ?? 0) + usage;
|
||||
final newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||
|
||||
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 newIdle = (prevCpu?.idle ?? 0) + idle;
|
||||
|
||||
@@ -125,46 +177,43 @@ class WindowsParser {
|
||||
SingleCpuCore(
|
||||
'cpu$i',
|
||||
newUser, // cumulative user time
|
||||
0, // sys (not available)
|
||||
0, // nice (not available)
|
||||
0, // sys
|
||||
0, // nice
|
||||
newIdle, // cumulative idle time
|
||||
0, // iowait (not available)
|
||||
0, // irq (not available)
|
||||
0, // softirq (not available)
|
||||
0, // iowait
|
||||
0, // irq
|
||||
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;
|
||||
} catch (e) {
|
||||
return [];
|
||||
// Add a summary entry at the beginning (like Linux 'cpu' line)
|
||||
// This is the aggregate of all logical processors
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ class SftpReq {
|
||||
String? privateKey;
|
||||
Spi? jumpSpi;
|
||||
String? jumpPrivateKey;
|
||||
Map<String, String>? knownHostFingerprints;
|
||||
|
||||
SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
|
||||
final keyId = spi.keyId;
|
||||
@@ -18,6 +19,12 @@ class SftpReq {
|
||||
jumpSpi = Stores.server.box.get(spi.jumpId);
|
||||
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key;
|
||||
}
|
||||
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 +37,7 @@ class SftpReqStatus {
|
||||
late SftpWorker worker;
|
||||
final Completer? completer;
|
||||
|
||||
String get fileName => req.localPath.split('/').last;
|
||||
String get fileName => req.localPath.split(Pfs.seperator).last;
|
||||
|
||||
// status of the download
|
||||
double? progress;
|
||||
|
||||
@@ -65,11 +65,12 @@ Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sen
|
||||
privateKey: req.privateKey,
|
||||
jumpSpi: req.jumpSpi,
|
||||
jumpPrivateKey: req.jumpPrivateKey,
|
||||
knownHostFingerprints: req.knownHostFingerprints,
|
||||
);
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Use [FileMode.write] to overwrite the file
|
||||
@@ -121,6 +122,7 @@ Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendE
|
||||
privateKey: req.privateKey,
|
||||
jumpSpi: req.jumpSpi,
|
||||
jumpPrivateKey: req.jumpPrivateKey,
|
||||
knownHostFingerprints: req.knownHostFingerprints,
|
||||
);
|
||||
mainSendPort.send(SftpWorkerStatus.sshConnectted);
|
||||
|
||||
|
||||
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)';
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app.g.dart';
|
||||
part 'app.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class AppState with _$AppState {
|
||||
const factory AppState() = _AppState;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppStates extends _$AppStates {
|
||||
static BuildContext? ctx;
|
||||
|
||||
@override
|
||||
AppState build() {
|
||||
return const AppState();
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'app.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$AppState {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class $AppStateCopyWith<$Res> {
|
||||
$AppStateCopyWith(AppState _, $Res Function(AppState) __);
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AppState].
|
||||
extension AppStatePatterns on AppState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AppState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AppState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AppState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function()? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default();case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function() $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState():
|
||||
return $default();case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function()? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default();case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _AppState implements AppState {
|
||||
const _AppState();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// dart format on
|
||||
@@ -1,62 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
@ProviderFor(AppStates)
|
||||
const appStatesProvider = AppStatesProvider._();
|
||||
|
||||
final class AppStatesProvider extends $NotifierProvider<AppStates, AppState> {
|
||||
const AppStatesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'appStatesProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$appStatesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AppStates create() => AppStates();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AppState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AppState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$appStatesHash() => r'ef96f10f6fff0f3dd6d3128ebf070ad79cbc8bc9';
|
||||
|
||||
abstract class _$AppStates extends $Notifier<AppState> {
|
||||
AppState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AppState, AppState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AppState, AppState>,
|
||||
AppState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -4,6 +4,8 @@ import 'dart:convert';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
@@ -12,63 +14,54 @@ import 'package:server_box/data/model/container/ps.dart';
|
||||
import 'package:server_box/data/model/container/type.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
part 'container.freezed.dart';
|
||||
part 'container.g.dart';
|
||||
|
||||
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
|
||||
class ContainerProvider extends ChangeNotifier {
|
||||
final SSHClient? client;
|
||||
final String userName;
|
||||
final String hostId;
|
||||
final BuildContext context;
|
||||
List<ContainerPs>? items;
|
||||
List<ContainerImg>? images;
|
||||
String? version;
|
||||
ContainerErr? error;
|
||||
String? runLog;
|
||||
ContainerType type;
|
||||
var sudoCompleter = Completer<bool>();
|
||||
bool isBusy = false;
|
||||
@freezed
|
||||
abstract class ContainerState with _$ContainerState {
|
||||
const factory ContainerState({
|
||||
@Default(null) List<ContainerPs>? items,
|
||||
@Default(null) List<ContainerImg>? images,
|
||||
@Default(null) String? version,
|
||||
@Default(null) ContainerErr? error,
|
||||
@Default(null) String? runLog,
|
||||
@Default(ContainerType.docker) ContainerType type,
|
||||
@Default(false) bool isBusy,
|
||||
}) = _ContainerState;
|
||||
}
|
||||
|
||||
ContainerProvider({
|
||||
required this.client,
|
||||
required this.userName,
|
||||
required this.hostId,
|
||||
required this.context,
|
||||
}) : type = Stores.container.getType(hostId) {
|
||||
refresh();
|
||||
@riverpod
|
||||
class ContainerNotifier extends _$ContainerNotifier {
|
||||
var sudoCompleter = Completer<bool>();
|
||||
|
||||
@override
|
||||
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
|
||||
final type = Stores.container.getType(hostId);
|
||||
final initialState = ContainerState(type: type);
|
||||
|
||||
// Async initialization
|
||||
Future.microtask(() => refresh());
|
||||
|
||||
return initialState;
|
||||
}
|
||||
|
||||
Future<void> setType(ContainerType type) async {
|
||||
this.type = type;
|
||||
state = state.copyWith(type: type, error: null, runLog: null, items: null, images: null, version: null);
|
||||
Stores.container.setType(type, hostId);
|
||||
error = runLog = items = images = version = null;
|
||||
sudoCompleter = Completer<bool>();
|
||||
notifyListeners();
|
||||
await refresh();
|
||||
}
|
||||
|
||||
// Future<bool> _checkDockerInstalled(SSHClient client) async {
|
||||
// final session = await client.execute("docker");
|
||||
// await session.done;
|
||||
// // debugPrint('docker code: ${session.exitCode}');
|
||||
// return session.exitCode == 0;
|
||||
// }
|
||||
|
||||
// String _removeSudoPrompts(String value) {
|
||||
// final regex = RegExp(r"\[sudo\] password for \w+:");
|
||||
// if (value.startsWith(regex)) {
|
||||
// return value.replaceFirstMapped(regex, (match) => "");
|
||||
// }
|
||||
// return value;
|
||||
// }
|
||||
|
||||
void _requiresSudo() async {
|
||||
/// Podman is rootless
|
||||
if (type == ContainerType.podman) return sudoCompleter.complete(false);
|
||||
if (state.type == ContainerType.podman) return sudoCompleter.complete(false);
|
||||
if (!Stores.setting.containerTrySudo.fetch()) {
|
||||
return sudoCompleter.complete(false);
|
||||
}
|
||||
|
||||
final res = await client?.run(_wrap(ContainerCmdType.images.exec(type)));
|
||||
final res = await client?.run(_wrap(ContainerCmdType.images.exec(state.type)));
|
||||
if (res?.string.toLowerCase().contains('permission denied') ?? false) {
|
||||
return sudoCompleter.complete(true);
|
||||
}
|
||||
@@ -76,8 +69,8 @@ class ContainerProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> refresh({bool isAuto = false}) async {
|
||||
if (isBusy) return;
|
||||
isBusy = true;
|
||||
if (state.isBusy) return;
|
||||
state = state.copyWith(isBusy: true);
|
||||
|
||||
if (!sudoCompleter.isCompleted) _requiresSudo();
|
||||
|
||||
@@ -85,11 +78,14 @@ class ContainerProvider extends ChangeNotifier {
|
||||
|
||||
/// If sudo is required and auto refresh is enabled, skip the refresh.
|
||||
/// Or this will ask for pwd again and again.
|
||||
if (sudo && isAuto) return;
|
||||
if (sudo && isAuto) {
|
||||
state = state.copyWith(isBusy: false);
|
||||
return;
|
||||
}
|
||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||
|
||||
var raw = '';
|
||||
final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats));
|
||||
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats));
|
||||
final code = await client?.execWithPwd(
|
||||
cmd,
|
||||
context: context,
|
||||
@@ -97,75 +93,79 @@ class ContainerProvider extends ChangeNotifier {
|
||||
id: hostId,
|
||||
);
|
||||
|
||||
isBusy = false;
|
||||
state = state.copyWith(isBusy: false);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
/// Code 127 means command not found
|
||||
if (code == 127 || raw.contains(_dockerNotFound)) {
|
||||
error = ContainerErr(type: ContainerErrType.notInstalled);
|
||||
notifyListeners();
|
||||
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check result segments count
|
||||
final segments = raw.split(ScriptConstants.separator);
|
||||
if (segments.length != ContainerCmdType.values.length) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.segmentsNotMatch,
|
||||
message: 'Container segments: ${segments.length}',
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(
|
||||
type: ContainerErrType.segmentsNotMatch,
|
||||
message: 'Container segments: ${segments.length}',
|
||||
),
|
||||
);
|
||||
Loggers.app.warning('Container segments: ${segments.length}\n$raw');
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse version
|
||||
final verRaw = ContainerCmdType.version.find(segments);
|
||||
try {
|
||||
version = json.decode(verRaw)['Client']['Version'];
|
||||
final version = json.decode(verRaw)['Client']['Version'];
|
||||
state = state.copyWith(version: version, error: null);
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e');
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),
|
||||
);
|
||||
Loggers.app.warning('Container version failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse ps
|
||||
final psRaw = ContainerCmdType.ps.find(segments);
|
||||
try {
|
||||
final lines = psRaw.split('\n');
|
||||
if (type == ContainerType.docker) {
|
||||
if (state.type == ContainerType.docker) {
|
||||
/// Due to the fetched data is not in json format, skip table header
|
||||
lines.removeWhere((element) => element.contains('CONTAINER ID'));
|
||||
}
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList();
|
||||
final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList();
|
||||
state = state.copyWith(items: items);
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(type: ContainerErrType.parsePs, message: '$e');
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),
|
||||
);
|
||||
Loggers.app.warning('Container ps failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse images
|
||||
final imageRaw = ContainerCmdType.images.find(segments).trim();
|
||||
final isEntireJson = imageRaw.startsWith('[') && imageRaw.endsWith(']');
|
||||
try {
|
||||
List<ContainerImg> images;
|
||||
if (isEntireJson) {
|
||||
images = (json.decode(imageRaw) as List)
|
||||
.map((e) => ContainerImg.fromRawJson(json.encode(e), type))
|
||||
.map((e) => ContainerImg.fromRawJson(json.encode(e), state.type))
|
||||
.toList();
|
||||
} else {
|
||||
final lines = imageRaw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
|
||||
images = lines.map((e) => ContainerImg.fromRawJson(e, state.type)).toList();
|
||||
}
|
||||
state = state.copyWith(images: images);
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(type: ContainerErrType.parseImages, message: '$e');
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),
|
||||
);
|
||||
Loggers.app.warning('Container images failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse stats
|
||||
@@ -173,22 +173,26 @@ class ContainerProvider extends ChangeNotifier {
|
||||
try {
|
||||
final statsLines = statsRaw.split('\n');
|
||||
statsLines.removeWhere((element) => element.isEmpty);
|
||||
for (var item in items!) {
|
||||
final items = state.items;
|
||||
if (items == null) return;
|
||||
|
||||
for (var item in items) {
|
||||
final id = item.id;
|
||||
if (id == null) continue;
|
||||
if (id.length < 5) continue;
|
||||
final statsLine = statsLines.firstWhereOrNull(
|
||||
/// Use 5 characters to match the container id, possibility of mismatch
|
||||
/// is very low.
|
||||
(element) => element.contains(id.substring(0, 5)),
|
||||
);
|
||||
if (statsLine == null) continue;
|
||||
item.parseStats(statsLine);
|
||||
item.parseStats(statsLine, state.version);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(type: ContainerErrType.parseStats, message: '$e');
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),
|
||||
);
|
||||
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,25 +227,23 @@ class ContainerProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
|
||||
cmd = switch (type) {
|
||||
cmd = switch (state.type) {
|
||||
ContainerType.docker => 'docker $cmd',
|
||||
ContainerType.podman => 'podman $cmd',
|
||||
};
|
||||
|
||||
runLog = '';
|
||||
state = state.copyWith(runLog: '');
|
||||
final errs = <String>[];
|
||||
final code = await client?.execWithPwd(
|
||||
_wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
|
||||
context: context,
|
||||
onStdout: (data, _) {
|
||||
runLog = '$runLog$data';
|
||||
notifyListeners();
|
||||
state = state.copyWith(runLog: '${state.runLog}$data');
|
||||
},
|
||||
onStderr: (data, _) => errs.add(data),
|
||||
id: hostId,
|
||||
);
|
||||
runLog = null;
|
||||
notifyListeners();
|
||||
state = state.copyWith(runLog: null);
|
||||
|
||||
if (code != 0) {
|
||||
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
|
||||
@@ -278,7 +280,7 @@ enum ContainerCmdType {
|
||||
return switch (this) {
|
||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||
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.
|
||||
ContainerType.docker =>
|
||||
'$prefix ps -a --format "table {{printf \\"'
|
||||
|
||||
305
lib/data/provider/container.freezed.dart
Normal file
305
lib/data/provider/container.freezed.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'container.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ContainerState {
|
||||
|
||||
List<ContainerPs>? get items; List<ContainerImg>? get images; String? get version; ContainerErr? get error; String? get runLog; ContainerType get type; bool get isBusy;
|
||||
/// Create a copy of ContainerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ContainerStateCopyWith<ContainerState> get copyWith => _$ContainerStateCopyWithImpl<ContainerState>(this as ContainerState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,error,runLog,type,isBusy);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ContainerStateCopyWith<$Res> {
|
||||
factory $ContainerStateCopyWith(ContainerState value, $Res Function(ContainerState) _then) = _$ContainerStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ContainerStateCopyWithImpl<$Res>
|
||||
implements $ContainerStateCopyWith<$Res> {
|
||||
_$ContainerStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ContainerState _self;
|
||||
final $Res Function(ContainerState) _then;
|
||||
|
||||
/// Create a copy of ContainerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
items: freezed == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContainerPs>?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContainerImg>?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable
|
||||
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ContainerState].
|
||||
extension ContainerStatePatterns on ContainerState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ContainerState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ContainerState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ContainerState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ContainerState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ContainerState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ContainerState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContainerState() when $default != null:
|
||||
return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContainerState():
|
||||
return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ContainerState() when $default != null:
|
||||
return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ContainerState implements ContainerState {
|
||||
const _ContainerState({final List<ContainerPs>? items = null, final List<ContainerImg>? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images;
|
||||
|
||||
|
||||
final List<ContainerPs>? _items;
|
||||
@override@JsonKey() List<ContainerPs>? get items {
|
||||
final value = _items;
|
||||
if (value == null) return null;
|
||||
if (_items is EqualUnmodifiableListView) return _items;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
final List<ContainerImg>? _images;
|
||||
@override@JsonKey() List<ContainerImg>? get images {
|
||||
final value = _images;
|
||||
if (value == null) return null;
|
||||
if (_images is EqualUnmodifiableListView) return _images;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override@JsonKey() final String? version;
|
||||
@override@JsonKey() final ContainerErr? error;
|
||||
@override@JsonKey() final String? runLog;
|
||||
@override@JsonKey() final ContainerType type;
|
||||
@override@JsonKey() final bool isBusy;
|
||||
|
||||
/// Create a copy of ContainerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ContainerStateCopyWith<_ContainerState> get copyWith => __$ContainerStateCopyWithImpl<_ContainerState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,error,runLog,type,isBusy);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ContainerStateCopyWith<$Res> implements $ContainerStateCopyWith<$Res> {
|
||||
factory _$ContainerStateCopyWith(_ContainerState value, $Res Function(_ContainerState) _then) = __$ContainerStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<ContainerPs>? items, List<ContainerImg>? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ContainerStateCopyWithImpl<$Res>
|
||||
implements _$ContainerStateCopyWith<$Res> {
|
||||
__$ContainerStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ContainerState _self;
|
||||
final $Res Function(_ContainerState) _then;
|
||||
|
||||
/// Create a copy of ContainerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) {
|
||||
return _then(_ContainerState(
|
||||
items: freezed == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContainerPs>?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable
|
||||
as List<ContainerImg>?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable
|
||||
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
123
lib/data/provider/container.g.dart
Normal file
123
lib/data/provider/container.g.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'container.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ContainerNotifier)
|
||||
const containerProvider = ContainerNotifierFamily._();
|
||||
|
||||
final class ContainerNotifierProvider
|
||||
extends $NotifierProvider<ContainerNotifier, ContainerState> {
|
||||
const ContainerNotifierProvider._({
|
||||
required ContainerNotifierFamily super.from,
|
||||
required (SSHClient?, String, String, BuildContext) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'containerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$containerNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'containerProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ContainerNotifier create() => ContainerNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ContainerState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ContainerState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ContainerNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
|
||||
|
||||
final class ContainerNotifierFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
ContainerNotifier,
|
||||
ContainerState,
|
||||
ContainerState,
|
||||
ContainerState,
|
||||
(SSHClient?, String, String, BuildContext)
|
||||
> {
|
||||
const ContainerNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'containerProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
ContainerNotifierProvider call(
|
||||
SSHClient? client,
|
||||
String userName,
|
||||
String hostId,
|
||||
BuildContext context,
|
||||
) => ContainerNotifierProvider._(
|
||||
argument: (client, userName, hostId, context),
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'containerProvider';
|
||||
}
|
||||
|
||||
abstract class _$ContainerNotifier extends $Notifier<ContainerState> {
|
||||
late final _$args = ref.$arg as (SSHClient?, String, String, BuildContext);
|
||||
SSHClient? get client => _$args.$1;
|
||||
String get userName => _$args.$2;
|
||||
String get hostId => _$args.$3;
|
||||
BuildContext get context => _$args.$4;
|
||||
|
||||
ContainerState build(
|
||||
SSHClient? client,
|
||||
String userName,
|
||||
String hostId,
|
||||
BuildContext context,
|
||||
);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args.$1, _$args.$2, _$args.$3, _$args.$4);
|
||||
final ref = this.ref as $Ref<ContainerState, ContainerState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ContainerState, ContainerState>,
|
||||
ContainerState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,61 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class PrivateKeyProvider extends Provider {
|
||||
const PrivateKeyProvider._();
|
||||
static const instance = PrivateKeyProvider._();
|
||||
part 'private_key.freezed.dart';
|
||||
part 'private_key.g.dart';
|
||||
|
||||
static final pkis = <PrivateKeyInfo>[].vn;
|
||||
@freezed
|
||||
abstract class PrivateKeyState with _$PrivateKeyState {
|
||||
const factory PrivateKeyState({@Default(<PrivateKeyInfo>[]) List<PrivateKeyInfo> keys}) = _PrivateKeyState;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class PrivateKeyNotifier extends _$PrivateKeyNotifier {
|
||||
@override
|
||||
void load() {
|
||||
super.load();
|
||||
pkis.value = Stores.key.fetch();
|
||||
PrivateKeyState build() {
|
||||
return _load();
|
||||
}
|
||||
|
||||
static void add(PrivateKeyInfo info) {
|
||||
pkis.value.add(info);
|
||||
pkis.notify();
|
||||
void reload() {
|
||||
final newState = _load();
|
||||
if (newState == state) return;
|
||||
state = newState;
|
||||
}
|
||||
|
||||
PrivateKeyState _load() {
|
||||
final keys = Stores.key.fetch();
|
||||
return stateOrNull?.copyWith(keys: keys) ?? PrivateKeyState(keys: keys);
|
||||
}
|
||||
|
||||
void add(PrivateKeyInfo info) {
|
||||
final newKeys = [...state.keys, info];
|
||||
state = state.copyWith(keys: newKeys);
|
||||
Stores.key.put(info);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void delete(PrivateKeyInfo info) {
|
||||
pkis.value.removeWhere((e) => e.id == info.id);
|
||||
pkis.notify();
|
||||
void delete(PrivateKeyInfo info) {
|
||||
final newKeys = state.keys.where((e) => e.id != info.id).toList();
|
||||
state = state.copyWith(keys: newKeys);
|
||||
Stores.key.delete(info);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
||||
final idx = pkis.value.indexWhere((e) => e.id == old.id);
|
||||
void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
||||
final keys = [...state.keys];
|
||||
final idx = keys.indexWhere((e) => e.id == old.id);
|
||||
if (idx == -1) {
|
||||
pkis.value.add(newInfo);
|
||||
keys.add(newInfo);
|
||||
Stores.key.put(newInfo);
|
||||
Stores.key.delete(old);
|
||||
} else {
|
||||
pkis.value[idx] = newInfo;
|
||||
keys[idx] = newInfo;
|
||||
Stores.key.put(newInfo);
|
||||
}
|
||||
pkis.notify();
|
||||
state = state.copyWith(keys: keys);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
}
|
||||
|
||||
277
lib/data/provider/private_key.freezed.dart
Normal file
277
lib/data/provider/private_key.freezed.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'private_key.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$PrivateKeyState {
|
||||
|
||||
List<PrivateKeyInfo> get keys;
|
||||
/// Create a copy of PrivateKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PrivateKeyStateCopyWith<PrivateKeyState> get copyWith => _$PrivateKeyStateCopyWithImpl<PrivateKeyState>(this as PrivateKeyState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PrivateKeyState&&const DeepCollectionEquality().equals(other.keys, keys));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(keys));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PrivateKeyState(keys: $keys)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PrivateKeyStateCopyWith<$Res> {
|
||||
factory $PrivateKeyStateCopyWith(PrivateKeyState value, $Res Function(PrivateKeyState) _then) = _$PrivateKeyStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<PrivateKeyInfo> keys
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PrivateKeyStateCopyWithImpl<$Res>
|
||||
implements $PrivateKeyStateCopyWith<$Res> {
|
||||
_$PrivateKeyStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PrivateKeyState _self;
|
||||
final $Res Function(PrivateKeyState) _then;
|
||||
|
||||
/// Create a copy of PrivateKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? keys = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
keys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable
|
||||
as List<PrivateKeyInfo>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PrivateKeyState].
|
||||
extension PrivateKeyStatePatterns on PrivateKeyState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PrivateKeyState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PrivateKeyState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PrivateKeyState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PrivateKeyState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PrivateKeyState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PrivateKeyState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<PrivateKeyInfo> keys)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PrivateKeyState() when $default != null:
|
||||
return $default(_that.keys);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<PrivateKeyInfo> keys) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PrivateKeyState():
|
||||
return $default(_that.keys);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<PrivateKeyInfo> keys)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PrivateKeyState() when $default != null:
|
||||
return $default(_that.keys);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _PrivateKeyState implements PrivateKeyState {
|
||||
const _PrivateKeyState({final List<PrivateKeyInfo> keys = const <PrivateKeyInfo>[]}): _keys = keys;
|
||||
|
||||
|
||||
final List<PrivateKeyInfo> _keys;
|
||||
@override@JsonKey() List<PrivateKeyInfo> get keys {
|
||||
if (_keys is EqualUnmodifiableListView) return _keys;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_keys);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of PrivateKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PrivateKeyStateCopyWith<_PrivateKeyState> get copyWith => __$PrivateKeyStateCopyWithImpl<_PrivateKeyState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PrivateKeyState&&const DeepCollectionEquality().equals(other._keys, _keys));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_keys));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PrivateKeyState(keys: $keys)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PrivateKeyStateCopyWith<$Res> implements $PrivateKeyStateCopyWith<$Res> {
|
||||
factory _$PrivateKeyStateCopyWith(_PrivateKeyState value, $Res Function(_PrivateKeyState) _then) = __$PrivateKeyStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<PrivateKeyInfo> keys
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PrivateKeyStateCopyWithImpl<$Res>
|
||||
implements _$PrivateKeyStateCopyWith<$Res> {
|
||||
__$PrivateKeyStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PrivateKeyState _self;
|
||||
final $Res Function(_PrivateKeyState) _then;
|
||||
|
||||
/// Create a copy of PrivateKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? keys = null,}) {
|
||||
return _then(_PrivateKeyState(
|
||||
keys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable
|
||||
as List<PrivateKeyInfo>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
64
lib/data/provider/private_key.g.dart
Normal file
64
lib/data/provider/private_key.g.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'private_key.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PrivateKeyNotifier)
|
||||
const privateKeyProvider = PrivateKeyNotifierProvider._();
|
||||
|
||||
final class PrivateKeyNotifierProvider
|
||||
extends $NotifierProvider<PrivateKeyNotifier, PrivateKeyState> {
|
||||
const PrivateKeyNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'privateKeyProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$privateKeyNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PrivateKeyNotifier create() => PrivateKeyNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PrivateKeyState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PrivateKeyState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$privateKeyNotifierHash() =>
|
||||
r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7';
|
||||
|
||||
abstract class _$PrivateKeyNotifier extends $Notifier<PrivateKeyState> {
|
||||
PrivateKeyState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<PrivateKeyState, PrivateKeyState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<PrivateKeyState, PrivateKeyState>,
|
||||
PrivateKeyState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
83
lib/data/provider/providers.dart
Normal file
83
lib/data/provider/providers.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/misc.dart';
|
||||
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/data/provider/sftp.dart';
|
||||
import 'package:server_box/data/provider/snippet.dart';
|
||||
|
||||
/// !library;
|
||||
/// ref.useNotifier, ref.readProvider, ref.watchProvider
|
||||
///
|
||||
/// Usage:
|
||||
/// - `providers.read.server` -> `ref.read(serversProvider)`
|
||||
/// - `providers.use.snippet` -> `ref.read(snippetsNotifierProvider.notifier)`
|
||||
|
||||
extension RiverpodNotifiers on ConsumerState {
|
||||
T useNotifier<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) {
|
||||
return ref.read(provider.notifier);
|
||||
}
|
||||
|
||||
T readProvider<T>(ProviderBase<T> provider) {
|
||||
return ref.read(provider);
|
||||
}
|
||||
|
||||
T watchProvider<T>(ProviderBase<T> provider) {
|
||||
return ref.watch(provider);
|
||||
}
|
||||
|
||||
MyProviders get providers => MyProviders(ref);
|
||||
}
|
||||
|
||||
final class MyProviders {
|
||||
final WidgetRef ref;
|
||||
const MyProviders(this.ref);
|
||||
|
||||
ReadMyProvider get read => ReadMyProvider(ref);
|
||||
WatchMyProvider get watch => WatchMyProvider(ref);
|
||||
UseNotifierMyProvider get use => UseNotifierMyProvider(ref);
|
||||
}
|
||||
|
||||
final class ReadMyProvider {
|
||||
final WidgetRef ref;
|
||||
const ReadMyProvider(this.ref);
|
||||
|
||||
T call<T>(ProviderBase<T> provider) => ref.read(provider);
|
||||
|
||||
// Specific provider getters
|
||||
ServersState get server => ref.read(serversProvider);
|
||||
SnippetState get snippet => ref.read(snippetProvider);
|
||||
AppState get app => ref.read(appStatesProvider);
|
||||
PrivateKeyState get privateKey => ref.read(privateKeyProvider);
|
||||
SftpState get sftp => ref.read(sftpProvider);
|
||||
}
|
||||
|
||||
final class WatchMyProvider {
|
||||
final WidgetRef ref;
|
||||
const WatchMyProvider(this.ref);
|
||||
|
||||
T call<T>(ProviderBase<T> provider) => ref.watch(provider);
|
||||
|
||||
// Specific provider getters
|
||||
ServersState get server => ref.watch(serversProvider);
|
||||
SnippetState get snippet => ref.watch(snippetProvider);
|
||||
AppState get app => ref.watch(appStatesProvider);
|
||||
PrivateKeyState get privateKey => ref.watch(privateKeyProvider);
|
||||
SftpState get sftp => ref.watch(sftpProvider);
|
||||
}
|
||||
|
||||
final class UseNotifierMyProvider {
|
||||
final WidgetRef ref;
|
||||
const UseNotifierMyProvider(this.ref);
|
||||
|
||||
T call<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) =>
|
||||
ref.read(provider.notifier);
|
||||
|
||||
// Specific provider notifier getters
|
||||
ServersNotifier get server => ref.read(serversProvider.notifier);
|
||||
SnippetNotifier get snippet => ref.read(snippetProvider.notifier);
|
||||
AppStates get app => ref.read(appStatesProvider.notifier);
|
||||
PrivateKeyNotifier get privateKey => ref.read(privateKeyProvider.notifier);
|
||||
SftpNotifier get sftp => ref.read(sftpProvider.notifier);
|
||||
}
|
||||
@@ -6,72 +6,90 @@ import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/server/pve.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
|
||||
part 'pve.freezed.dart';
|
||||
part 'pve.g.dart';
|
||||
|
||||
typedef PveCtrlFunc = Future<bool> Function(String node, String id);
|
||||
|
||||
final class PveProvider extends ChangeNotifier {
|
||||
final Spi spi;
|
||||
@freezed
|
||||
abstract class PveState with _$PveState {
|
||||
const factory PveState({
|
||||
@Default(null) PveErr? error,
|
||||
@Default(null) PveRes? data,
|
||||
@Default(null) String? release,
|
||||
@Default(false) bool isBusy,
|
||||
@Default(false) bool isConnected,
|
||||
}) = _PveState;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class PveNotifier extends _$PveNotifier {
|
||||
late final Spi spi;
|
||||
late String addr;
|
||||
late final SSHClient _client;
|
||||
late final ServerSocket _serverSocket;
|
||||
final List<SSHForwardChannel> _forwards = [];
|
||||
int _localPort = 0;
|
||||
late final Dio session;
|
||||
late final bool _ignoreCert;
|
||||
|
||||
PveProvider({required this.spi}) {
|
||||
final client = spi.server?.value.client;
|
||||
@override
|
||||
PveState build(Spi spiParam) {
|
||||
spi = spiParam;
|
||||
final serverState = ref.watch(serverProvider(spi.id));
|
||||
final client = serverState.client;
|
||||
if (client == null) {
|
||||
throw Exception('Server client is null');
|
||||
return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null'));
|
||||
}
|
||||
_client = client;
|
||||
final addr = spi.custom?.pveAddr;
|
||||
if (addr == null) {
|
||||
err.value = 'PVE address is null';
|
||||
return;
|
||||
return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null'));
|
||||
}
|
||||
this.addr = addr;
|
||||
_init();
|
||||
_ignoreCert = spi.custom?.pveIgnoreCert ?? false;
|
||||
_initSession();
|
||||
// Async initialization
|
||||
Future.microtask(() => _init());
|
||||
return const PveState();
|
||||
}
|
||||
|
||||
final err = ValueNotifier<String?>(null);
|
||||
final connected = Completer<void>();
|
||||
void _initSession() {
|
||||
session = Dio()
|
||||
..httpClientAdapter = IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
client.connectionFactory = cf;
|
||||
if (_ignoreCert) {
|
||||
client.badCertificateCallback = (_, _, _) => true;
|
||||
}
|
||||
return client;
|
||||
},
|
||||
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
|
||||
);
|
||||
}
|
||||
|
||||
late final _ignoreCert = spi.custom?.pveIgnoreCert ?? false;
|
||||
late final session = Dio()
|
||||
..httpClientAdapter = IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
client.connectionFactory = cf;
|
||||
if (_ignoreCert) {
|
||||
client.badCertificateCallback = (_, _, _) => true;
|
||||
}
|
||||
return client;
|
||||
},
|
||||
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
|
||||
);
|
||||
|
||||
final data = ValueNotifier<PveRes?>(null);
|
||||
|
||||
bool get onlyOneNode => data.value?.nodes.length == 1;
|
||||
String? release;
|
||||
bool isBusy = false;
|
||||
bool get onlyOneNode => state.data?.nodes.length == 1;
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
await _forward();
|
||||
await _login();
|
||||
await _getRelease();
|
||||
state = state.copyWith(isConnected: true);
|
||||
} on PveErr {
|
||||
err.value = l10n.pveLoginFailed;
|
||||
state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed));
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('PVE init failed', e, s);
|
||||
err.value = e.toString();
|
||||
} finally {
|
||||
connected.complete();
|
||||
state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +107,7 @@ final class PveProvider extends ChangeNotifier {
|
||||
final newUrl = Uri.parse(
|
||||
addr,
|
||||
).replace(host: 'localhost', port: _localPort).toString();
|
||||
debugPrint('Forwarding $newUrl to $addr');
|
||||
dprint('Forwarding $newUrl to $addr');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,72 +164,85 @@ final class PveProvider extends ChangeNotifier {
|
||||
final resp = await session.get('$addr/api2/extjs/version');
|
||||
final version = resp.data['data']['release'] as String?;
|
||||
if (version != null) {
|
||||
release = version;
|
||||
state = state.copyWith(release: version);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> list() async {
|
||||
await connected.future;
|
||||
if (isBusy) return;
|
||||
isBusy = true;
|
||||
if (!state.isConnected || state.isBusy) return;
|
||||
state = state.copyWith(isBusy: true);
|
||||
try {
|
||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||
final res = resp.data['data'] as List;
|
||||
final result = await Computer.shared.start(PveRes.parse, (
|
||||
res,
|
||||
data.value,
|
||||
state.data,
|
||||
));
|
||||
data.value = result;
|
||||
state = state.copyWith(data: result, error: null);
|
||||
} catch (e) {
|
||||
Loggers.app.warning('PVE list failed', e);
|
||||
err.value = e.toString();
|
||||
state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()));
|
||||
} finally {
|
||||
isBusy = false;
|
||||
state = state.copyWith(isBusy: false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> reboot(String node, String id) async {
|
||||
await connected.future;
|
||||
if (!state.isConnected) return false;
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/reboot',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
final success = _isCtrlSuc(resp);
|
||||
if (success) await list(); // Refresh data
|
||||
return success;
|
||||
}
|
||||
|
||||
Future<bool> start(String node, String id) async {
|
||||
await connected.future;
|
||||
if (!state.isConnected) return false;
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/start',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
final success = _isCtrlSuc(resp);
|
||||
if (success) await list(); // Refresh data
|
||||
return success;
|
||||
}
|
||||
|
||||
Future<bool> stop(String node, String id) async {
|
||||
await connected.future;
|
||||
if (!state.isConnected) return false;
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/stop',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
final success = _isCtrlSuc(resp);
|
||||
if (success) await list(); // Refresh data
|
||||
return success;
|
||||
}
|
||||
|
||||
Future<bool> shutdown(String node, String id) async {
|
||||
await connected.future;
|
||||
if (!state.isConnected) return false;
|
||||
final resp = await session.post(
|
||||
'$addr/api2/json/nodes/$node/$id/status/shutdown',
|
||||
);
|
||||
return _isCtrlSuc(resp);
|
||||
final success = _isCtrlSuc(resp);
|
||||
if (success) await list(); // Refresh data
|
||||
return success;
|
||||
}
|
||||
|
||||
bool _isCtrlSuc(Response resp) {
|
||||
return resp.statusCode == 200;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
super.dispose();
|
||||
await _serverSocket.close();
|
||||
try {
|
||||
await _serverSocket.close();
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to close server socket', e, s);
|
||||
}
|
||||
for (final forward in _forwards) {
|
||||
forward.close();
|
||||
try {
|
||||
forward.close();
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to close forward', e, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
283
lib/data/provider/pve.freezed.dart
Normal file
283
lib/data/provider/pve.freezed.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'pve.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$PveState {
|
||||
|
||||
PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected;
|
||||
/// Create a copy of PveState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PveStateCopyWith<PveState> get copyWith => _$PveStateCopyWithImpl<PveState>(this as PveState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PveStateCopyWith<$Res> {
|
||||
factory $PveStateCopyWith(PveState value, $Res Function(PveState) _then) = _$PveStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PveStateCopyWithImpl<$Res>
|
||||
implements $PveStateCopyWith<$Res> {
|
||||
_$PveStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PveState _self;
|
||||
final $Res Function(PveState) _then;
|
||||
|
||||
/// Create a copy of PveState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PveState].
|
||||
extension PveStatePatterns on PveState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PveState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PveState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PveState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PveState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PveState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PveState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PveState() when $default != null:
|
||||
return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PveState():
|
||||
return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PveState() when $default != null:
|
||||
return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _PveState implements PveState {
|
||||
const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false});
|
||||
|
||||
|
||||
@override@JsonKey() final PveErr? error;
|
||||
@override@JsonKey() final PveRes? data;
|
||||
@override@JsonKey() final String? release;
|
||||
@override@JsonKey() final bool isBusy;
|
||||
@override@JsonKey() final bool isConnected;
|
||||
|
||||
/// Create a copy of PveState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PveStateCopyWith<_PveState> get copyWith => __$PveStateCopyWithImpl<_PveState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PveStateCopyWith<$Res> implements $PveStateCopyWith<$Res> {
|
||||
factory _$PveStateCopyWith(_PveState value, $Res Function(_PveState) _then) = __$PveStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PveStateCopyWithImpl<$Res>
|
||||
implements _$PveStateCopyWith<$Res> {
|
||||
__$PveStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PveState _self;
|
||||
final $Res Function(_PveState) _then;
|
||||
|
||||
/// Create a copy of PveState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) {
|
||||
return _then(_PveState(
|
||||
error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
101
lib/data/provider/pve.g.dart
Normal file
101
lib/data/provider/pve.g.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'pve.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PveNotifier)
|
||||
const pveProvider = PveNotifierFamily._();
|
||||
|
||||
final class PveNotifierProvider
|
||||
extends $NotifierProvider<PveNotifier, PveState> {
|
||||
const PveNotifierProvider._({
|
||||
required PveNotifierFamily super.from,
|
||||
required Spi super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'pveProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$pveNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'pveProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PveNotifier create() => PveNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PveState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PveState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PveNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$pveNotifierHash() => r'ba5f2d6cb47c33735f7cc09b771b4a86501b86c6';
|
||||
|
||||
final class PveNotifierFamily extends $Family
|
||||
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
||||
const PveNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'pveProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
PveNotifierProvider call(Spi spiParam) =>
|
||||
PveNotifierProvider._(argument: spiParam, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'pveProvider';
|
||||
}
|
||||
|
||||
abstract class _$PveNotifier extends $Notifier<PveState> {
|
||||
late final _$args = ref.$arg as Spi;
|
||||
Spi get spiParam => _$args;
|
||||
|
||||
PveState build(Spi spiParam);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<PveState, PveState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<PveState, PveState>,
|
||||
PveState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,505 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
// import 'dart:io';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
import 'package:server_box/core/utils/ssh_auth.dart';
|
||||
import 'package:server_box/data/helper/system_detector.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||
import 'package:server_box/data/res/status.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/ssh/session_manager.dart';
|
||||
|
||||
class ServerProvider extends Provider {
|
||||
const ServerProvider._();
|
||||
static const instance = ServerProvider._();
|
||||
|
||||
static final Map<String, VNode<Server>> servers = {};
|
||||
static final serverOrder = <String>[].vn;
|
||||
static final _tags = <String>{}.vn;
|
||||
static VNode<Set<String>> get tags => _tags;
|
||||
|
||||
static Timer? _timer;
|
||||
|
||||
static final _manualDisconnectedIds = <String>{};
|
||||
|
||||
static final _serverIdsUpdating = <String, Future<void>?>{};
|
||||
|
||||
@override
|
||||
Future<void> load() async {
|
||||
super.load();
|
||||
// #147
|
||||
// Clear all servers because of restarting app will cause duplicate servers
|
||||
final oldServers = Map<String, VNode<Server>>.from(servers);
|
||||
servers.clear();
|
||||
serverOrder.value.clear();
|
||||
|
||||
final spis = Stores.server.fetch();
|
||||
for (int idx = 0; idx < spis.length; idx++) {
|
||||
final spi = spis[idx];
|
||||
final originServer = oldServers[spi.id];
|
||||
|
||||
/// #258
|
||||
/// If not [shouldReconnect], then keep the old state.
|
||||
if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) {
|
||||
originServer.value.spi = spi;
|
||||
servers[spi.id] = originServer;
|
||||
} else {
|
||||
final newServer = genServer(spi);
|
||||
servers[spi.id] = newServer.vn;
|
||||
}
|
||||
}
|
||||
final serverOrder_ = Stores.setting.serverOrder.fetch();
|
||||
if (serverOrder_.isNotEmpty) {
|
||||
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
|
||||
serverOrder.value.addAll(spis.map((e) => e.id));
|
||||
} else {
|
||||
serverOrder.value.addAll(servers.keys);
|
||||
}
|
||||
// Must use [equals] to compare [Order] here.
|
||||
if (!serverOrder.value.equals(serverOrder_)) {
|
||||
Stores.setting.serverOrder.put(serverOrder.value);
|
||||
}
|
||||
_updateTags();
|
||||
// Must notify here, or the UI will not be updated.
|
||||
serverOrder.notify();
|
||||
}
|
||||
|
||||
/// Get a [Server] by [spi] or [id].
|
||||
///
|
||||
/// Priority: [spi] > [id]
|
||||
static VNode<Server>? pick({Spi? spi, String? id}) {
|
||||
if (spi != null) {
|
||||
return servers[spi.id];
|
||||
}
|
||||
if (id != null) {
|
||||
return servers[id];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void _updateTags() {
|
||||
final tags = <String>{};
|
||||
for (final s in servers.values) {
|
||||
final spiTags = s.value.spi.tags;
|
||||
if (spiTags == null) continue;
|
||||
for (final t in spiTags) {
|
||||
tags.add(t);
|
||||
}
|
||||
}
|
||||
_tags.value = tags;
|
||||
}
|
||||
|
||||
static Server genServer(Spi spi) {
|
||||
return Server(spi, InitStatus.status, ServerConn.disconnected);
|
||||
}
|
||||
|
||||
/// if [spi] is specificed then only refresh this server
|
||||
/// [onlyFailed] only refresh failed servers
|
||||
static Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
|
||||
if (spi != null) {
|
||||
_manualDisconnectedIds.remove(spi.id);
|
||||
await _getData(spi);
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(
|
||||
servers.values.map((val) async {
|
||||
final s = val.value;
|
||||
if (onlyFailed) {
|
||||
if (s.conn != ServerConn.failed) return;
|
||||
TryLimiter.reset(s.spi.id);
|
||||
}
|
||||
|
||||
if (_manualDisconnectedIds.contains(s.spi.id)) return;
|
||||
|
||||
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already updating, and if so, wait for it to complete
|
||||
final existingUpdate = _serverIdsUpdating[s.spi.id];
|
||||
if (existingUpdate != null) {
|
||||
// Already updating, wait for the existing update to complete
|
||||
try {
|
||||
await existingUpdate;
|
||||
} catch (e) {
|
||||
// Ignore errors from the existing update, we'll try our own
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a new update operation
|
||||
final updateFuture = _updateServer(s.spi);
|
||||
_serverIdsUpdating[s.spi.id] = updateFuture;
|
||||
|
||||
try {
|
||||
await updateFuture;
|
||||
} finally {
|
||||
_serverIdsUpdating.remove(s.spi.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _updateServer(Spi spi) async {
|
||||
await _getData(spi);
|
||||
}
|
||||
|
||||
static Future<void> startAutoRefresh() async {
|
||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||
stopAutoRefresh();
|
||||
if (duration == 0) return;
|
||||
if (duration < 0 || duration > 10 || duration == 1) {
|
||||
duration = 3;
|
||||
Loggers.app.warning('Invalid duration: $duration, use default 3');
|
||||
}
|
||||
_timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
||||
await refresh();
|
||||
});
|
||||
}
|
||||
|
||||
static void stopAutoRefresh() {
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
static bool get isAutoRefreshOn => _timer != null;
|
||||
|
||||
static void setDisconnected() {
|
||||
for (final s in servers.values) {
|
||||
s.value.conn = ServerConn.disconnected;
|
||||
s.notify();
|
||||
|
||||
// Update SSH session status to disconnected
|
||||
final sessionId = 'ssh_${s.value.spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
}
|
||||
//TryLimiter.clear();
|
||||
}
|
||||
|
||||
static void closeServer({String? id}) {
|
||||
if (id == null) {
|
||||
for (final s in servers.values) {
|
||||
_closeOneServer(s.value.spi.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_closeOneServer(id);
|
||||
}
|
||||
|
||||
static void _closeOneServer(String id) {
|
||||
final s = servers[id];
|
||||
if (s == null) {
|
||||
Loggers.app.warning('Server with id $id not found');
|
||||
return;
|
||||
}
|
||||
final item = s.value;
|
||||
item.client?.close();
|
||||
item.client = null;
|
||||
item.conn = ServerConn.disconnected;
|
||||
_manualDisconnectedIds.add(id);
|
||||
s.notify();
|
||||
|
||||
// Remove SSH session when server is manually closed
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
}
|
||||
|
||||
static void addServer(Spi spi) {
|
||||
servers[spi.id] = genServer(spi).vn;
|
||||
Stores.server.put(spi);
|
||||
serverOrder.value.add(spi.id);
|
||||
serverOrder.notify();
|
||||
Stores.setting.serverOrder.put(serverOrder.value);
|
||||
_updateTags();
|
||||
refresh(spi: spi);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void delServer(String id) {
|
||||
servers.remove(id);
|
||||
serverOrder.value.remove(id);
|
||||
serverOrder.notify();
|
||||
Stores.setting.serverOrder.put(serverOrder.value);
|
||||
Stores.server.delete(id);
|
||||
_updateTags();
|
||||
|
||||
// Remove SSH session when server is deleted
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void deleteAll() {
|
||||
// Remove all SSH sessions before clearing servers
|
||||
for (final id in servers.keys) {
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
}
|
||||
|
||||
servers.clear();
|
||||
serverOrder.value.clear();
|
||||
serverOrder.notify();
|
||||
Stores.setting.serverOrder.put(serverOrder.value);
|
||||
Stores.server.clear();
|
||||
_updateTags();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static Future<void> updateServer(Spi old, Spi newSpi) async {
|
||||
if (old != newSpi) {
|
||||
Stores.server.update(old, newSpi);
|
||||
servers[old.id]?.value.spi = newSpi;
|
||||
|
||||
if (newSpi.id != old.id) {
|
||||
servers[newSpi.id] = servers[old.id]!;
|
||||
servers.remove(old.id);
|
||||
serverOrder.value.update(old.id, newSpi.id);
|
||||
Stores.setting.serverOrder.put(serverOrder.value);
|
||||
serverOrder.notify();
|
||||
|
||||
// Update SSH session ID when server ID changes
|
||||
final oldSessionId = 'ssh_${old.id}';
|
||||
TermSessionManager.remove(oldSessionId);
|
||||
// Session will be re-added when reconnecting if necessary
|
||||
}
|
||||
|
||||
// Only reconnect if neccessary
|
||||
if (newSpi.shouldReconnect(old)) {
|
||||
// Use [newSpi.id] instead of [old.id] because [old.id] may be changed
|
||||
TryLimiter.reset(newSpi.id);
|
||||
refresh(spi: newSpi);
|
||||
}
|
||||
}
|
||||
_updateTags();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void _setServerState(VNode<Server> s, ServerConn ss) {
|
||||
s.value.conn = ss;
|
||||
s.notify();
|
||||
}
|
||||
|
||||
static Future<void> _getData(Spi spi) async {
|
||||
final sid = spi.id;
|
||||
final s = servers[sid];
|
||||
|
||||
if (s == null) return;
|
||||
|
||||
final sv = s.value;
|
||||
if (!TryLimiter.canTry(sid)) {
|
||||
if (sv.conn != ServerConn.failed) {
|
||||
_setServerState(s, ServerConn.failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sv.status.err = null;
|
||||
|
||||
if (sv.needGenClient || (sv.client?.isClosed ?? true)) {
|
||||
_setServerState(s, ServerConn.connecting);
|
||||
|
||||
final wol = spi.wolCfg;
|
||||
if (wol != null) {
|
||||
try {
|
||||
await wol.wake();
|
||||
} catch (e) {
|
||||
// TryLimiter.inc(sid);
|
||||
// s.status.err = SSHErr(
|
||||
// type: SSHErrType.connect,
|
||||
// message: 'Wake on lan failed: $e',
|
||||
// );
|
||||
// _setServerState(s, ServerConn.failed);
|
||||
Loggers.app.warning('Wake on lan failed', e);
|
||||
// return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final time1 = DateTime.now();
|
||||
sv.client = await genClient(
|
||||
spi,
|
||||
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
|
||||
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
|
||||
);
|
||||
final time2 = DateTime.now();
|
||||
final spentTime = time2.difference(time1).inMilliseconds;
|
||||
if (spi.jumpId == null) {
|
||||
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
||||
} else {
|
||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||
}
|
||||
|
||||
// Add SSH session to TermSessionManager
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.add(
|
||||
id: sessionId,
|
||||
spi: spi,
|
||||
startTimeMs: time1.millisecondsSinceEpoch,
|
||||
disconnect: () => _closeOneServer(spi.id),
|
||||
status: TermSessionStatus.connecting,
|
||||
);
|
||||
TermSessionManager.setActive(sessionId, hasTerminal: false);
|
||||
} catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
sv.status.err = SSHErr(type: SSHErrType.connect, message: e.toString());
|
||||
_setServerState(s, ServerConn.failed);
|
||||
|
||||
// Remove SSH session on connection failure
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.remove(sessionId);
|
||||
|
||||
/// In order to keep privacy, print [spi.name] instead of [spi.id]
|
||||
Loggers.app.warning('Connect to ${spi.name} failed', e);
|
||||
return;
|
||||
}
|
||||
|
||||
_setServerState(s, ServerConn.connected);
|
||||
|
||||
// Update SSH session status to connected
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);
|
||||
|
||||
try {
|
||||
// Detect system type using helper
|
||||
final detectedSystemType = await SystemDetector.detect(sv.client!, spi);
|
||||
sv.status.system = detectedSystemType;
|
||||
|
||||
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
||||
final scriptRaw = ShellFuncManager.allScript(
|
||||
spi.custom?.cmds,
|
||||
systemType: detectedSystemType,
|
||||
disabledCmdTypes: spi.disabledCmdTypes,
|
||||
).uint8List;
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
|
||||
if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) {
|
||||
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
|
||||
throw writeScriptResult;
|
||||
}
|
||||
} on SSHAuthAbortError catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
|
||||
sv.status.err = err;
|
||||
Loggers.app.warning(err);
|
||||
_setServerState(s, ServerConn.failed);
|
||||
|
||||
// Update SSH session status to disconnected
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
} on SSHAuthFailError catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
|
||||
sv.status.err = err;
|
||||
Loggers.app.warning(err);
|
||||
_setServerState(s, ServerConn.failed);
|
||||
|
||||
// Update SSH session status to disconnected
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
} catch (e) {
|
||||
// If max try times < 2 and can't write script, this will stop the status getting and etc.
|
||||
// TryLimiter.inc(sid);
|
||||
final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
|
||||
sv.status.err = err;
|
||||
Loggers.app.warning(err);
|
||||
_setServerState(s, ServerConn.failed);
|
||||
|
||||
// Update SSH session status to disconnected
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
if (sv.conn == ServerConn.connecting) return;
|
||||
|
||||
/// Keep [finished] state, or the UI will be refreshed to [loading] state
|
||||
/// instead of the '$Temp | $Uptime'.
|
||||
/// eg: '32C | 7 days'
|
||||
if (sv.conn != ServerConn.finished) {
|
||||
_setServerState(s, ServerConn.loading);
|
||||
}
|
||||
|
||||
List<String>? segments;
|
||||
String? raw;
|
||||
|
||||
try {
|
||||
raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
|
||||
//dprint('Get status from ${spi.name}:\n$raw');
|
||||
segments = raw?.split(ScriptConstants.separator).map((e) => e.trim()).toList();
|
||||
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||
// Keep previous server status when err occurs
|
||||
if (sv.conn != ServerConn.failed && sv.status.more.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
TryLimiter.inc(sid);
|
||||
sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
|
||||
_setServerState(s, ServerConn.failed);
|
||||
|
||||
// Update SSH session status to disconnected on segments error
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
|
||||
_setServerState(s, ServerConn.failed);
|
||||
Loggers.app.warning('Get status from ${spi.name} failed', e);
|
||||
|
||||
// Update SSH session status to disconnected on status error
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse script output into command-specific map
|
||||
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
ss: sv.status,
|
||||
parsedOutput: parsedOutput,
|
||||
system: sv.status.system,
|
||||
customCmds: spi.custom?.cmds ?? {},
|
||||
);
|
||||
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');
|
||||
} catch (e, trace) {
|
||||
TryLimiter.inc(sid);
|
||||
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
|
||||
_setServerState(s, ServerConn.failed);
|
||||
Loggers.app.warning('Server status', e, trace);
|
||||
|
||||
// Update SSH session status to disconnected on parse error
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Call this every time for setting [Server.isBusy] to false
|
||||
_setServerState(s, ServerConn.finished);
|
||||
// reset try times only after prepared successfully
|
||||
TryLimiter.reset(sid);
|
||||
}
|
||||
}
|
||||
322
lib/data/provider/server/all.dart
Normal file
322
lib/data/provider/server/all.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/ssh/session_manager.dart';
|
||||
|
||||
part 'all.freezed.dart';
|
||||
part 'all.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ServersState with _$ServersState {
|
||||
const factory ServersState({
|
||||
@Default({}) Map<String, Spi> servers,
|
||||
@Default([]) List<String> serverOrder,
|
||||
@Default(<String>{}) Set<String> tags,
|
||||
@Default(<String>{}) Set<String> manualDisconnectedIds,
|
||||
Timer? autoRefreshTimer,
|
||||
}) = _ServersState;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ServersNotifier extends _$ServersNotifier {
|
||||
@override
|
||||
ServersState build() {
|
||||
return _load();
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
final newState = _load();
|
||||
if (newState == state) return;
|
||||
state = newState;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
ServersState _load() {
|
||||
final spis = Stores.server.fetch();
|
||||
final newServers = <String, Spi>{};
|
||||
final newServerOrder = <String>[];
|
||||
|
||||
for (final spi in spis) {
|
||||
newServers[spi.id] = spi;
|
||||
}
|
||||
|
||||
final serverOrder_ = Stores.setting.serverOrder.fetch();
|
||||
if (serverOrder_.isNotEmpty) {
|
||||
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
|
||||
newServerOrder.addAll(spis.map((e) => e.id));
|
||||
} else {
|
||||
newServerOrder.addAll(newServers.keys);
|
||||
}
|
||||
|
||||
// Must use [equals] to compare [Order] here.
|
||||
if (!newServerOrder.equals(serverOrder_)) {
|
||||
Stores.setting.serverOrder.put(newServerOrder);
|
||||
}
|
||||
|
||||
final newTags = _calculateTags(newServers);
|
||||
|
||||
return stateOrNull?.copyWith(servers: newServers, serverOrder: newServerOrder, tags: newTags) ??
|
||||
ServersState(servers: newServers, serverOrder: newServerOrder, tags: newTags);
|
||||
}
|
||||
|
||||
Set<String> _calculateTags(Map<String, Spi> servers) {
|
||||
final tags = <String>{};
|
||||
for (final spi in servers.values) {
|
||||
final spiTags = spi.tags;
|
||||
if (spiTags == null) continue;
|
||||
for (final t in spiTags) {
|
||||
tags.add(t);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
/// Get a [Spi] by [spi] or [id].
|
||||
///
|
||||
/// Priority: [spi] > [id]
|
||||
Spi? pick({Spi? spi, String? id}) {
|
||||
if (spi != null) {
|
||||
return state.servers[spi.id];
|
||||
}
|
||||
if (id != null) {
|
||||
return state.servers[id];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// if [spi] is specificed then only refresh this server
|
||||
/// [onlyFailed] only refresh failed servers
|
||||
Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
|
||||
if (spi != null) {
|
||||
final newManualDisconnected = Set<String>.from(state.manualDisconnectedIds)..remove(spi.id);
|
||||
state = state.copyWith(manualDisconnectedIds: newManualDisconnected);
|
||||
final serverNotifier = ref.read(serverProvider(spi.id).notifier);
|
||||
await serverNotifier.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(
|
||||
state.servers.entries.map((entry) async {
|
||||
final serverId = entry.key;
|
||||
final spi = entry.value;
|
||||
|
||||
if (onlyFailed) {
|
||||
final serverState = ref.read(serverProvider(serverId));
|
||||
if (serverState.conn != ServerConn.failed) return;
|
||||
TryLimiter.reset(serverId);
|
||||
}
|
||||
|
||||
if (state.manualDisconnectedIds.contains(serverId)) return;
|
||||
|
||||
final serverState = ref.read(serverProvider(serverId));
|
||||
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
final serverNotifier = ref.read(serverProvider(serverId).notifier);
|
||||
await serverNotifier.refresh();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> startAutoRefresh() async {
|
||||
var duration = Stores.setting.serverStatusUpdateInterval.fetch();
|
||||
stopAutoRefresh();
|
||||
if (duration == 0) return;
|
||||
if (duration < 0 || duration > 10 || duration == 1) {
|
||||
duration = 3;
|
||||
Loggers.app.warning('Invalid duration: $duration, use default 3');
|
||||
}
|
||||
final timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
||||
await refresh();
|
||||
});
|
||||
state = state.copyWith(autoRefreshTimer: timer);
|
||||
}
|
||||
|
||||
void stopAutoRefresh() {
|
||||
final timer = state.autoRefreshTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
state = state.copyWith(autoRefreshTimer: null);
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAutoRefreshOn => state.autoRefreshTimer != null;
|
||||
|
||||
void setDisconnected() {
|
||||
for (final serverId in state.servers.keys) {
|
||||
final serverNotifier = ref.read(serverProvider(serverId).notifier);
|
||||
serverNotifier.updateConnection(ServerConn.disconnected);
|
||||
|
||||
// Update SSH session status to disconnected
|
||||
final sessionId = 'ssh_$serverId';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
}
|
||||
//TryLimiter.clear();
|
||||
}
|
||||
|
||||
void closeServer({String? id}) {
|
||||
if (id == null) {
|
||||
for (final serverId in state.servers.keys) {
|
||||
closeOneServer(serverId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
closeOneServer(id);
|
||||
}
|
||||
|
||||
void closeOneServer(String id) {
|
||||
final spi = state.servers[id];
|
||||
if (spi == null) {
|
||||
Loggers.app.warning('Server with id $id not found');
|
||||
return;
|
||||
}
|
||||
|
||||
final serverNotifier = ref.read(serverProvider(id).notifier);
|
||||
serverNotifier.closeConnection();
|
||||
|
||||
final newManualDisconnected = Set<String>.from(state.manualDisconnectedIds)..add(id);
|
||||
state = state.copyWith(manualDisconnectedIds: newManualDisconnected);
|
||||
|
||||
// Remove SSH session when server is manually closed
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
}
|
||||
|
||||
void addServer(Spi spi) {
|
||||
final newServers = Map<String, Spi>.from(state.servers);
|
||||
newServers[spi.id] = spi;
|
||||
|
||||
final newOrder = List<String>.from(state.serverOrder)..add(spi.id);
|
||||
final newTags = _calculateTags(newServers);
|
||||
|
||||
state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);
|
||||
|
||||
Stores.server.put(spi);
|
||||
Stores.setting.serverOrder.put(newOrder);
|
||||
refresh(spi: spi);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
void delServer(String id) {
|
||||
final newServers = Map<String, Spi>.from(state.servers);
|
||||
newServers.remove(id);
|
||||
|
||||
final newOrder = List<String>.from(state.serverOrder)..remove(id);
|
||||
final newTags = _calculateTags(newServers);
|
||||
|
||||
state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);
|
||||
|
||||
Stores.setting.serverOrder.put(newOrder);
|
||||
Stores.server.delete(id);
|
||||
|
||||
// Remove SSH session when server is deleted
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
void deleteAll() {
|
||||
// Remove all SSH sessions before clearing servers
|
||||
for (final id in state.servers.keys) {
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
}
|
||||
|
||||
state = const ServersState();
|
||||
|
||||
Stores.setting.serverOrder.put([]);
|
||||
Stores.server.clear();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
void updateServerOrder(List<String> order) {
|
||||
final seen = <String>{};
|
||||
final newOrder = <String>[];
|
||||
|
||||
for (final id in order) {
|
||||
if (!state.servers.containsKey(id)) {
|
||||
continue;
|
||||
}
|
||||
if (!seen.add(id)) {
|
||||
continue;
|
||||
}
|
||||
newOrder.add(id);
|
||||
}
|
||||
|
||||
for (final id in state.servers.keys) {
|
||||
if (seen.add(id)) {
|
||||
newOrder.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (_isSameOrder(newOrder, state.serverOrder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(serverOrder: newOrder);
|
||||
Stores.setting.serverOrder.put(newOrder);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
bool _isSameOrder(List<String> a, List<String> b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> updateServer(Spi old, Spi newSpi) async {
|
||||
if (old != newSpi) {
|
||||
Stores.server.update(old, newSpi);
|
||||
|
||||
final newServers = Map<String, Spi>.from(state.servers);
|
||||
final newOrder = List<String>.from(state.serverOrder);
|
||||
|
||||
if (newSpi.id != old.id) {
|
||||
newServers[newSpi.id] = newSpi;
|
||||
newServers.remove(old.id);
|
||||
newOrder.update(old.id, newSpi.id);
|
||||
Stores.setting.serverOrder.put(newOrder);
|
||||
|
||||
// Update SSH session ID when server ID changes
|
||||
final oldSessionId = 'ssh_${old.id}';
|
||||
TermSessionManager.remove(oldSessionId);
|
||||
// Session will be re-added when reconnecting if necessary
|
||||
} else {
|
||||
newServers[old.id] = newSpi;
|
||||
// Update SPI in the corresponding IndividualServerNotifier
|
||||
final serverNotifier = ref.read(serverProvider(old.id).notifier);
|
||||
serverNotifier.updateSpi(newSpi);
|
||||
}
|
||||
|
||||
final newTags = _calculateTags(newServers);
|
||||
state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags);
|
||||
|
||||
// Only reconnect if neccessary
|
||||
if (newSpi.shouldReconnect(old)) {
|
||||
// Use [newSpi.id] instead of [old.id] because [old.id] may be changed
|
||||
TryLimiter.reset(newSpi.id);
|
||||
refresh(spi: newSpi);
|
||||
}
|
||||
}
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
}
|
||||
307
lib/data/provider/server/all.freezed.dart
Normal file
307
lib/data/provider/server/all.freezed.dart
Normal file
@@ -0,0 +1,307 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'all.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ServersState {
|
||||
|
||||
Map<String, Spi> get servers; List<String> get serverOrder; Set<String> get tags; Set<String> get manualDisconnectedIds; Timer? get autoRefreshTimer;
|
||||
/// Create a copy of ServersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServersStateCopyWith<ServersState> get copyWith => _$ServersStateCopyWithImpl<ServersState>(this as ServersState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServersState&&const DeepCollectionEquality().equals(other.servers, servers)&&const DeepCollectionEquality().equals(other.serverOrder, serverOrder)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.manualDisconnectedIds, manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(servers),const DeepCollectionEquality().hash(serverOrder),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(manualDisconnectedIds),autoRefreshTimer);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ServersStateCopyWith<$Res> {
|
||||
factory $ServersStateCopyWith(ServersState value, $Res Function(ServersState) _then) = _$ServersStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ServersStateCopyWithImpl<$Res>
|
||||
implements $ServersStateCopyWith<$Res> {
|
||||
_$ServersStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ServersState _self;
|
||||
final $Res Function(ServersState) _then;
|
||||
|
||||
/// Create a copy of ServersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
servers: null == servers ? _self.servers : servers // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Spi>,serverOrder: null == serverOrder ? _self.serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self.manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable
|
||||
as Set<String>,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable
|
||||
as Timer?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ServersState].
|
||||
extension ServersStatePatterns on ServersState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServersState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServersState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServersState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServersState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServersState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServersState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServersState() when $default != null:
|
||||
return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServersState():
|
||||
return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServersState() when $default != null:
|
||||
return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ServersState implements ServersState {
|
||||
const _ServersState({final Map<String, Spi> servers = const {}, final List<String> serverOrder = const [], final Set<String> tags = const <String>{}, final Set<String> manualDisconnectedIds = const <String>{}, this.autoRefreshTimer}): _servers = servers,_serverOrder = serverOrder,_tags = tags,_manualDisconnectedIds = manualDisconnectedIds;
|
||||
|
||||
|
||||
final Map<String, Spi> _servers;
|
||||
@override@JsonKey() Map<String, Spi> get servers {
|
||||
if (_servers is EqualUnmodifiableMapView) return _servers;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_servers);
|
||||
}
|
||||
|
||||
final List<String> _serverOrder;
|
||||
@override@JsonKey() List<String> get serverOrder {
|
||||
if (_serverOrder is EqualUnmodifiableListView) return _serverOrder;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_serverOrder);
|
||||
}
|
||||
|
||||
final Set<String> _tags;
|
||||
@override@JsonKey() Set<String> get tags {
|
||||
if (_tags is EqualUnmodifiableSetView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableSetView(_tags);
|
||||
}
|
||||
|
||||
final Set<String> _manualDisconnectedIds;
|
||||
@override@JsonKey() Set<String> get manualDisconnectedIds {
|
||||
if (_manualDisconnectedIds is EqualUnmodifiableSetView) return _manualDisconnectedIds;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableSetView(_manualDisconnectedIds);
|
||||
}
|
||||
|
||||
@override final Timer? autoRefreshTimer;
|
||||
|
||||
/// Create a copy of ServersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ServersStateCopyWith<_ServersState> get copyWith => __$ServersStateCopyWithImpl<_ServersState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServersState&&const DeepCollectionEquality().equals(other._servers, _servers)&&const DeepCollectionEquality().equals(other._serverOrder, _serverOrder)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._manualDisconnectedIds, _manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_servers),const DeepCollectionEquality().hash(_serverOrder),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_manualDisconnectedIds),autoRefreshTimer);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ServersStateCopyWith<$Res> implements $ServersStateCopyWith<$Res> {
|
||||
factory _$ServersStateCopyWith(_ServersState value, $Res Function(_ServersState) _then) = __$ServersStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> manualDisconnectedIds, Timer? autoRefreshTimer
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ServersStateCopyWithImpl<$Res>
|
||||
implements _$ServersStateCopyWith<$Res> {
|
||||
__$ServersStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ServersState _self;
|
||||
final $Res Function(_ServersState) _then;
|
||||
|
||||
/// Create a copy of ServersState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) {
|
||||
return _then(_ServersState(
|
||||
servers: null == servers ? _self._servers : servers // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, Spi>,serverOrder: null == serverOrder ? _self._serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self._manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable
|
||||
as Set<String>,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable
|
||||
as Timer?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
63
lib/data/provider/server/all.g.dart
Normal file
63
lib/data/provider/server/all.g.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'all.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ServersNotifier)
|
||||
const serversProvider = ServersNotifierProvider._();
|
||||
|
||||
final class ServersNotifierProvider
|
||||
extends $NotifierProvider<ServersNotifier, ServersState> {
|
||||
const ServersNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'serversProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$serversNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ServersNotifier create() => ServersNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ServersState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ServersState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$serversNotifierHash() => r'3292bdce7d602ff64687b05ff81d120e71761ec2';
|
||||
|
||||
abstract class _$ServersNotifier extends $Notifier<ServersState> {
|
||||
ServersState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<ServersState, ServersState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ServersState, ServersState>,
|
||||
ServersState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
389
lib/data/provider/server/single.dart
Normal file
389
lib/data/provider/server/single.dart
Normal file
@@ -0,0 +1,389 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/core/utils/server.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/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||
import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/data/res/status.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/ssh/session_manager.dart';
|
||||
|
||||
part 'single.g.dart';
|
||||
part 'single.freezed.dart';
|
||||
|
||||
// Individual server state, including connection and status information
|
||||
@freezed
|
||||
abstract class ServerState with _$ServerState {
|
||||
const factory ServerState({
|
||||
required Spi spi,
|
||||
required ServerStatus status,
|
||||
@Default(ServerConn.disconnected) ServerConn conn,
|
||||
SSHClient? client,
|
||||
Future<void>? updateFuture,
|
||||
}) = _ServerState;
|
||||
}
|
||||
|
||||
// Individual server state management
|
||||
@Riverpod(keepAlive: true)
|
||||
class ServerNotifier extends _$ServerNotifier {
|
||||
@override
|
||||
ServerState build(String serverId) {
|
||||
final serverNotifier = ref.read(serversProvider);
|
||||
final spi = serverNotifier.servers[serverId];
|
||||
if (spi == null) {
|
||||
throw StateError('Server $serverId not found');
|
||||
}
|
||||
|
||||
return ServerState(spi: spi, status: InitStatus.status);
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
void updateConnection(ServerConn conn) {
|
||||
state = state.copyWith(conn: conn);
|
||||
}
|
||||
|
||||
// Update server status
|
||||
void updateStatus(ServerStatus status) {
|
||||
state = state.copyWith(status: status);
|
||||
}
|
||||
|
||||
// Update SSH client
|
||||
void updateClient(SSHClient? client) {
|
||||
state = state.copyWith(client: client);
|
||||
}
|
||||
|
||||
// Update SPI configuration
|
||||
void updateSpi(Spi spi) {
|
||||
state = state.copyWith(spi: spi);
|
||||
}
|
||||
|
||||
// Close connection
|
||||
void closeConnection() {
|
||||
final client = state.client;
|
||||
client?.close();
|
||||
state = state.copyWith(client: null, conn: ServerConn.disconnected);
|
||||
}
|
||||
|
||||
// Refresh server status
|
||||
Future<void> refresh() async {
|
||||
if (state.updateFuture != null) {
|
||||
await state.updateFuture;
|
||||
return;
|
||||
}
|
||||
|
||||
final updateFuture = _updateServer();
|
||||
state = state.copyWith(updateFuture: updateFuture);
|
||||
|
||||
try {
|
||||
await updateFuture;
|
||||
} finally {
|
||||
state = state.copyWith(updateFuture: null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateServer() async {
|
||||
await _getData();
|
||||
}
|
||||
|
||||
Future<void> _getData() async {
|
||||
final spi = state.spi;
|
||||
final sid = spi.id;
|
||||
|
||||
if (!TryLimiter.canTry(sid)) {
|
||||
if (state.conn != ServerConn.failed) {
|
||||
updateConnection(ServerConn.failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final newStatus = state.status..err = null; // Clear previous error
|
||||
updateStatus(newStatus);
|
||||
|
||||
if (state.conn < ServerConn.connecting || (state.client?.isClosed ?? true)) {
|
||||
updateConnection(ServerConn.connecting);
|
||||
|
||||
// Wake on LAN
|
||||
final wol = spi.wolCfg;
|
||||
if (wol != null) {
|
||||
try {
|
||||
await wol.wake();
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Wake on lan failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final time1 = DateTime.now();
|
||||
final client = await genClient(
|
||||
spi,
|
||||
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
|
||||
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
|
||||
);
|
||||
updateClient(client);
|
||||
|
||||
final time2 = DateTime.now();
|
||||
final spentTime = time2.difference(time1).inMilliseconds;
|
||||
if (spi.jumpId == null) {
|
||||
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
|
||||
} else {
|
||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||
}
|
||||
|
||||
// Record successful connection
|
||||
Stores.connectionStats.recordConnection(ConnectionStat(
|
||||
serverId: spi.id,
|
||||
serverName: spi.name,
|
||||
timestamp: time1,
|
||||
result: ConnectionResult.success,
|
||||
durationMs: spentTime,
|
||||
));
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.add(
|
||||
id: sessionId,
|
||||
spi: spi,
|
||||
startTimeMs: time1.millisecondsSinceEpoch,
|
||||
disconnect: () => ref.read(serversProvider.notifier).closeOneServer(spi.id),
|
||||
status: TermSessionStatus.connecting,
|
||||
);
|
||||
TermSessionManager.setActive(sessionId, hasTerminal: false);
|
||||
} catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
|
||||
// Determine connection failure type
|
||||
ConnectionResult failureResult;
|
||||
if (e.toString().contains('timeout') || e.toString().contains('Timeout')) {
|
||||
failureResult = ConnectionResult.timeout;
|
||||
} else if (e.toString().contains('auth') || e.toString().contains('Authentication')) {
|
||||
failureResult = ConnectionResult.authFailed;
|
||||
} else if (e.toString().contains('network') || e.toString().contains('Network')) {
|
||||
failureResult = ConnectionResult.networkError;
|
||||
} else {
|
||||
failureResult = ConnectionResult.unknownError;
|
||||
}
|
||||
|
||||
// Record failed connection
|
||||
Stores.connectionStats.recordConnection(ConnectionStat(
|
||||
serverId: spi.id,
|
||||
serverName: spi.name,
|
||||
timestamp: DateTime.now(),
|
||||
result: failureResult,
|
||||
errorMessage: e.toString(),
|
||||
durationMs: 0,
|
||||
));
|
||||
|
||||
final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
|
||||
updateStatus(newStatus);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
// Remove SSH session when connection fails
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.remove(sessionId);
|
||||
|
||||
Loggers.app.warning('Connect to ${spi.name} failed', e);
|
||||
return;
|
||||
}
|
||||
|
||||
updateConnection(ServerConn.connected);
|
||||
|
||||
// Update SSH session status to connected
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);
|
||||
|
||||
try {
|
||||
// Detect system type
|
||||
final detectedSystemType = await SystemDetector.detect(state.client!, spi);
|
||||
final newStatus = state.status..system = detectedSystemType;
|
||||
updateStatus(newStatus);
|
||||
|
||||
Loggers.app.info('Writing script for ${spi.name} (${detectedSystemType.name})');
|
||||
|
||||
final (stdoutResult, writeScriptResult) = await state.client!.execSafe(
|
||||
(session) async {
|
||||
final scriptRaw = ShellFuncManager.allScript(
|
||||
spi.custom?.cmds,
|
||||
systemType: detectedSystemType,
|
||||
disabledCmdTypes: spi.disabledCmdTypes,
|
||||
).uint8List;
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
},
|
||||
entry: ShellFuncManager.getInstallShellCmd(
|
||||
spi.id,
|
||||
systemType: detectedSystemType,
|
||||
customDir: spi.custom?.scriptDir,
|
||||
),
|
||||
systemType: detectedSystemType,
|
||||
context: 'WriteScript<${spi.name}>',
|
||||
);
|
||||
|
||||
if (stdoutResult.isNotEmpty) {
|
||||
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) {
|
||||
TryLimiter.inc(sid);
|
||||
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
|
||||
final newStatus = state.status..err = err;
|
||||
updateStatus(newStatus);
|
||||
Loggers.app.warning(err);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
} on SSHAuthFailError catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
|
||||
final newStatus = state.status..err = err;
|
||||
updateStatus(newStatus);
|
||||
Loggers.app.warning(err);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
} catch (e) {
|
||||
final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
|
||||
final newStatus = state.status..err = err;
|
||||
updateStatus(newStatus);
|
||||
Loggers.app.warning(err);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.conn == ServerConn.connecting) return;
|
||||
|
||||
// Keep finished status to prevent UI from refreshing to loading state
|
||||
if (state.conn != ServerConn.finished) {
|
||||
updateConnection(ServerConn.loading);
|
||||
}
|
||||
|
||||
List<String>? segments;
|
||||
String? raw;
|
||||
|
||||
try {
|
||||
final statusCmd = 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) {
|
||||
raw = SSHDecoder.decode(
|
||||
execResult,
|
||||
isWindows: state.status.system == SystemType.windows,
|
||||
context: 'GetStatus<${spi.name}>',
|
||||
);
|
||||
// Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');
|
||||
} else {
|
||||
raw = '';
|
||||
Loggers.app.warning('No status result from ${spi.name}');
|
||||
}
|
||||
|
||||
if (raw.isEmpty) {
|
||||
TryLimiter.inc(sid);
|
||||
final newStatus = state.status
|
||||
..err = SSHErr(type: SSHErrType.segements, message: 'Empty response from server');
|
||||
updateStatus(newStatus);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
|
||||
if (segments.isEmpty) {
|
||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||
// Keep previous server status when error occurs
|
||||
if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
TryLimiter.inc(sid);
|
||||
final newStatus = state.status
|
||||
..err = SSHErr(type: SSHErrType.segements, message: 'Separate segments failed, raw:\n$raw');
|
||||
updateStatus(newStatus);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
final newStatus = state.status..err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
|
||||
updateStatus(newStatus);
|
||||
updateConnection(ServerConn.failed);
|
||||
Loggers.app.warning('Get status from ${spi.name} failed', e);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse script output into command-specific mappings
|
||||
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
ss: state.status,
|
||||
parsedOutput: parsedOutput,
|
||||
system: state.status.system,
|
||||
customCmds: spi.custom?.cmds ?? {},
|
||||
);
|
||||
final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>');
|
||||
updateStatus(newStatus);
|
||||
} catch (e, trace) {
|
||||
TryLimiter.inc(sid);
|
||||
final newStatus = state.status
|
||||
..err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
|
||||
updateStatus(newStatus);
|
||||
updateConnection(ServerConn.failed);
|
||||
Loggers.app.warning('Server status', e, trace);
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set Server.isBusy to false each time this method is called
|
||||
updateConnection(ServerConn.finished);
|
||||
// Reset retry count only after successful preparation
|
||||
TryLimiter.reset(sid);
|
||||
}
|
||||
}
|
||||
|
||||
extension IndividualServerStateExtension on ServerState {
|
||||
bool get needGenClient => conn < ServerConn.connecting;
|
||||
|
||||
bool get canViewDetails => conn == ServerConn.finished;
|
||||
|
||||
String get id => spi.id;
|
||||
}
|
||||
301
lib/data/provider/server/single.freezed.dart
Normal file
301
lib/data/provider/server/single.freezed.dart
Normal file
@@ -0,0 +1,301 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'single.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ServerState {
|
||||
|
||||
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client; Future<void>? get updateFuture;
|
||||
/// Create a copy of ServerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerStateCopyWith<ServerState> get copyWith => _$ServerStateCopyWithImpl<ServerState>(this as ServerState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ServerStateCopyWith<$Res> {
|
||||
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
|
||||
});
|
||||
|
||||
|
||||
$SpiCopyWith<$Res> get spi;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ServerStateCopyWithImpl<$Res>
|
||||
implements $ServerStateCopyWith<$Res> {
|
||||
_$ServerStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ServerState _self;
|
||||
final $Res Function(ServerState) _then;
|
||||
|
||||
/// Create a copy of ServerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
||||
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
||||
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
|
||||
as Future<void>?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of ServerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SpiCopyWith<$Res> get spi {
|
||||
|
||||
return $SpiCopyWith<$Res>(_self.spi, (value) {
|
||||
return _then(_self.copyWith(spi: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ServerState].
|
||||
extension ServerStatePatterns on ServerState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerState() when $default != null:
|
||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerState():
|
||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerState() when $default != null:
|
||||
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ServerState implements ServerState {
|
||||
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client, this.updateFuture});
|
||||
|
||||
|
||||
@override final Spi spi;
|
||||
@override final ServerStatus status;
|
||||
@override@JsonKey() final ServerConn conn;
|
||||
@override final SSHClient? client;
|
||||
@override final Future<void>? updateFuture;
|
||||
|
||||
/// Create a copy of ServerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_ServerState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith<$Res> {
|
||||
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture
|
||||
});
|
||||
|
||||
|
||||
@override $SpiCopyWith<$Res> get spi;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ServerStateCopyWithImpl<$Res>
|
||||
implements _$ServerStateCopyWith<$Res> {
|
||||
__$ServerStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ServerState _self;
|
||||
final $Res Function(_ServerState) _then;
|
||||
|
||||
/// Create a copy of ServerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) {
|
||||
return _then(_ServerState(
|
||||
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
|
||||
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
|
||||
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
|
||||
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable
|
||||
as Future<void>?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ServerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SpiCopyWith<$Res> get spi {
|
||||
|
||||
return $SpiCopyWith<$Res>(_self.spi, (value) {
|
||||
return _then(_self.copyWith(spi: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
108
lib/data/provider/server/single.g.dart
Normal file
108
lib/data/provider/server/single.g.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'single.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ServerNotifier)
|
||||
const serverProvider = ServerNotifierFamily._();
|
||||
|
||||
final class ServerNotifierProvider
|
||||
extends $NotifierProvider<ServerNotifier, ServerState> {
|
||||
const ServerNotifierProvider._({
|
||||
required ServerNotifierFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'serverProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$serverNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'serverProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ServerNotifier create() => ServerNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ServerState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ServerState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ServerNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$serverNotifierHash() => r'185c6b4546c3bc526f5b2ca79d16aed665818863';
|
||||
|
||||
final class ServerNotifierFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
ServerNotifier,
|
||||
ServerState,
|
||||
ServerState,
|
||||
ServerState,
|
||||
String
|
||||
> {
|
||||
const ServerNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'serverProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
ServerNotifierProvider call(String serverId) =>
|
||||
ServerNotifierProvider._(argument: serverId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'serverProvider';
|
||||
}
|
||||
|
||||
abstract class _$ServerNotifier extends $Notifier<ServerState> {
|
||||
late final _$args = ref.$arg as String;
|
||||
String get serverId => _$args;
|
||||
|
||||
ServerState build(String serverId);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<ServerState, ServerState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ServerState, ServerState>,
|
||||
ServerState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,69 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/data/model/sftp/worker.dart';
|
||||
|
||||
class SftpProvider extends Provider {
|
||||
const SftpProvider._();
|
||||
static const instance = SftpProvider._();
|
||||
part 'sftp.freezed.dart';
|
||||
part 'sftp.g.dart';
|
||||
|
||||
static final status = <SftpReqStatus>[].vn;
|
||||
@freezed
|
||||
abstract class SftpState with _$SftpState {
|
||||
const factory SftpState({
|
||||
@Default(<SftpReqStatus>[]) List<SftpReqStatus> requests,
|
||||
}) = _SftpState;
|
||||
}
|
||||
|
||||
static SftpReqStatus? get(int id) {
|
||||
return status.value.singleWhere((element) => element.id == id);
|
||||
@Riverpod(keepAlive: true)
|
||||
class SftpNotifier extends _$SftpNotifier {
|
||||
@override
|
||||
SftpState build() {
|
||||
return const SftpState();
|
||||
}
|
||||
|
||||
static int add(SftpReq req, {Completer? completer}) {
|
||||
final reqStat = SftpReqStatus(notifyListeners: status.notify, completer: completer, req: req);
|
||||
status.value.add(reqStat);
|
||||
status.notify();
|
||||
SftpReqStatus? get(int id) {
|
||||
try {
|
||||
return state.requests.singleWhere((element) => element.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int add(SftpReq req, {Completer? completer}) {
|
||||
final reqStat = SftpReqStatus(
|
||||
notifyListeners: _notifyListeners,
|
||||
completer: completer,
|
||||
req: req,
|
||||
);
|
||||
state = state.copyWith(
|
||||
requests: [...state.requests, reqStat],
|
||||
);
|
||||
return reqStat.id;
|
||||
}
|
||||
|
||||
static void dispose() {
|
||||
for (final item in status.value) {
|
||||
void dispose() {
|
||||
for (final item in state.requests) {
|
||||
item.dispose();
|
||||
}
|
||||
status.value.clear();
|
||||
status.notify();
|
||||
state = state.copyWith(requests: []);
|
||||
}
|
||||
|
||||
static void cancel(int id) {
|
||||
final idx = status.value.indexWhere((e) => e.id == id);
|
||||
if (idx < 0 || idx >= status.value.length) {
|
||||
void cancel(int id) {
|
||||
final idx = state.requests.indexWhere((e) => e.id == id);
|
||||
if (idx < 0 || idx >= state.requests.length) {
|
||||
dprint('SftpProvider.cancel: id $id not found');
|
||||
return;
|
||||
}
|
||||
status.value[idx].dispose();
|
||||
status.value.removeAt(idx);
|
||||
status.notify();
|
||||
final item = state.requests[idx];
|
||||
item.dispose();
|
||||
final newRequests = List<SftpReqStatus>.from(state.requests)
|
||||
..removeAt(idx);
|
||||
state = state.copyWith(requests: newRequests);
|
||||
}
|
||||
|
||||
void _notifyListeners() {
|
||||
// Force state update to notify listeners
|
||||
state = state.copyWith();
|
||||
}
|
||||
}
|
||||
|
||||
277
lib/data/provider/sftp.freezed.dart
Normal file
277
lib/data/provider/sftp.freezed.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'sftp.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SftpState {
|
||||
|
||||
List<SftpReqStatus> get requests;
|
||||
/// Create a copy of SftpState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SftpStateCopyWith<SftpState> get copyWith => _$SftpStateCopyWithImpl<SftpState>(this as SftpState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SftpState&&const DeepCollectionEquality().equals(other.requests, requests));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(requests));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SftpState(requests: $requests)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SftpStateCopyWith<$Res> {
|
||||
factory $SftpStateCopyWith(SftpState value, $Res Function(SftpState) _then) = _$SftpStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<SftpReqStatus> requests
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SftpStateCopyWithImpl<$Res>
|
||||
implements $SftpStateCopyWith<$Res> {
|
||||
_$SftpStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SftpState _self;
|
||||
final $Res Function(SftpState) _then;
|
||||
|
||||
/// Create a copy of SftpState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? requests = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
requests: null == requests ? _self.requests : requests // ignore: cast_nullable_to_non_nullable
|
||||
as List<SftpReqStatus>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SftpState].
|
||||
extension SftpStatePatterns on SftpState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SftpState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SftpState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SftpState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SftpState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SftpState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SftpState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SftpReqStatus> requests)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SftpState() when $default != null:
|
||||
return $default(_that.requests);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SftpReqStatus> requests) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SftpState():
|
||||
return $default(_that.requests);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SftpReqStatus> requests)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SftpState() when $default != null:
|
||||
return $default(_that.requests);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _SftpState implements SftpState {
|
||||
const _SftpState({final List<SftpReqStatus> requests = const <SftpReqStatus>[]}): _requests = requests;
|
||||
|
||||
|
||||
final List<SftpReqStatus> _requests;
|
||||
@override@JsonKey() List<SftpReqStatus> get requests {
|
||||
if (_requests is EqualUnmodifiableListView) return _requests;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_requests);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of SftpState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SftpStateCopyWith<_SftpState> get copyWith => __$SftpStateCopyWithImpl<_SftpState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SftpState&&const DeepCollectionEquality().equals(other._requests, _requests));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_requests));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SftpState(requests: $requests)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SftpStateCopyWith<$Res> implements $SftpStateCopyWith<$Res> {
|
||||
factory _$SftpStateCopyWith(_SftpState value, $Res Function(_SftpState) _then) = __$SftpStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<SftpReqStatus> requests
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SftpStateCopyWithImpl<$Res>
|
||||
implements _$SftpStateCopyWith<$Res> {
|
||||
__$SftpStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SftpState _self;
|
||||
final $Res Function(_SftpState) _then;
|
||||
|
||||
/// Create a copy of SftpState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? requests = null,}) {
|
||||
return _then(_SftpState(
|
||||
requests: null == requests ? _self._requests : requests // ignore: cast_nullable_to_non_nullable
|
||||
as List<SftpReqStatus>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
63
lib/data/provider/sftp.g.dart
Normal file
63
lib/data/provider/sftp.g.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sftp.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(SftpNotifier)
|
||||
const sftpProvider = SftpNotifierProvider._();
|
||||
|
||||
final class SftpNotifierProvider
|
||||
extends $NotifierProvider<SftpNotifier, SftpState> {
|
||||
const SftpNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'sftpProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$sftpNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SftpNotifier create() => SftpNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SftpState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SftpState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$sftpNotifierHash() => r'f8412a4bd1f2bc5919ec31a3eba1c27e9a578f41';
|
||||
|
||||
abstract class _$SftpNotifier extends $Notifier<SftpState> {
|
||||
SftpState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<SftpState, SftpState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<SftpState, SftpState>,
|
||||
SftpState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,105 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/sync.dart';
|
||||
import 'package:server_box/data/model/server/snippet.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class SnippetProvider extends Provider {
|
||||
const SnippetProvider._();
|
||||
static const instance = SnippetProvider._();
|
||||
part 'snippet.freezed.dart';
|
||||
part 'snippet.g.dart';
|
||||
|
||||
static final snippets = <Snippet>[].vn;
|
||||
static final tags = <String>{}.vn;
|
||||
@freezed
|
||||
abstract class SnippetState with _$SnippetState {
|
||||
const factory SnippetState({
|
||||
@Default(<Snippet>[]) List<Snippet> snippets,
|
||||
@Default(<String>{}) Set<String> tags,
|
||||
}) = _SnippetState;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SnippetNotifier extends _$SnippetNotifier {
|
||||
@override
|
||||
void load() {
|
||||
super.load();
|
||||
final snippets_ = Stores.snippet.fetch();
|
||||
SnippetState build() {
|
||||
return _load();
|
||||
}
|
||||
|
||||
void reload() {
|
||||
final newState = _load();
|
||||
if (newState == state) return;
|
||||
state = newState;
|
||||
}
|
||||
|
||||
SnippetState _load() {
|
||||
final snippets = Stores.snippet.fetch();
|
||||
final order = Stores.setting.snippetOrder.fetch();
|
||||
|
||||
List<Snippet> orderedSnippets = snippets;
|
||||
if (order.isNotEmpty) {
|
||||
final surplus = snippets_.reorder(
|
||||
order: order,
|
||||
finder: (n, name) => n.name == name,
|
||||
);
|
||||
final surplus = snippets.reorder(order: order, finder: (n, name) => n.name == name);
|
||||
order.removeWhere((e) => surplus.any((ele) => ele == e));
|
||||
if (order != Stores.setting.snippetOrder.fetch()) {
|
||||
Stores.setting.snippetOrder.put(order);
|
||||
}
|
||||
orderedSnippets = snippets;
|
||||
}
|
||||
snippets.value = snippets_;
|
||||
_updateTags();
|
||||
|
||||
final newTags = _computeTags(orderedSnippets);
|
||||
return stateOrNull?.copyWith(snippets: orderedSnippets, tags: newTags) ??
|
||||
SnippetState(snippets: orderedSnippets, tags: newTags);
|
||||
}
|
||||
|
||||
static void _updateTags() {
|
||||
final tags_ = <String>{};
|
||||
for (final s in snippets.value) {
|
||||
Set<String> _computeTags(List<Snippet> snippets) {
|
||||
final tags = <String>{};
|
||||
for (final s in snippets) {
|
||||
final t = s.tags;
|
||||
if (t != null) {
|
||||
tags_.addAll(t);
|
||||
tags.addAll(t);
|
||||
}
|
||||
}
|
||||
tags.value = tags_;
|
||||
return tags;
|
||||
}
|
||||
|
||||
static void add(Snippet snippet) {
|
||||
snippets.value.add(snippet);
|
||||
snippets.notify();
|
||||
void add(Snippet snippet) {
|
||||
final newSnippets = [...state.snippets, snippet];
|
||||
final newTags = _computeTags(newSnippets);
|
||||
state = state.copyWith(snippets: newSnippets, tags: newTags);
|
||||
Stores.snippet.put(snippet);
|
||||
_updateTags();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void del(Snippet snippet) {
|
||||
snippets.value.remove(snippet);
|
||||
snippets.notify();
|
||||
void del(Snippet snippet) {
|
||||
final newSnippets = state.snippets.where((s) => s != snippet).toList();
|
||||
final newTags = _computeTags(newSnippets);
|
||||
state = state.copyWith(snippets: newSnippets, tags: newTags);
|
||||
Stores.snippet.delete(snippet);
|
||||
_updateTags();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void update(Snippet old, Snippet newOne) {
|
||||
snippets.value.remove(old);
|
||||
snippets.value.add(newOne);
|
||||
snippets.notify();
|
||||
void update(Snippet old, Snippet newOne) {
|
||||
final newSnippets = state.snippets.map((s) => s == old ? newOne : s).toList();
|
||||
final newTags = _computeTags(newSnippets);
|
||||
state = state.copyWith(snippets: newSnippets, tags: newTags);
|
||||
Stores.snippet.delete(old);
|
||||
Stores.snippet.put(newOne);
|
||||
_updateTags();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
static void renameTag(String old, String newOne) {
|
||||
for (final s in snippets.value) {
|
||||
void renameTag(String old, String newOne) {
|
||||
final updatedSnippets = <Snippet>[];
|
||||
for (final s in state.snippets) {
|
||||
if (s.tags?.contains(old) ?? false) {
|
||||
s.tags?.remove(old);
|
||||
s.tags?.add(newOne);
|
||||
Stores.snippet.put(s);
|
||||
final newTags = Set<String>.from(s.tags!);
|
||||
newTags.remove(old);
|
||||
newTags.add(newOne);
|
||||
final updatedSnippet = s.copyWith(tags: newTags.toList());
|
||||
updatedSnippets.add(updatedSnippet);
|
||||
Stores.snippet.put(updatedSnippet);
|
||||
} else {
|
||||
updatedSnippets.add(s);
|
||||
}
|
||||
}
|
||||
_updateTags();
|
||||
final newTags = _computeTags(updatedSnippets);
|
||||
state = state.copyWith(snippets: updatedSnippets, tags: newTags);
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
}
|
||||
|
||||
286
lib/data/provider/snippet.freezed.dart
Normal file
286
lib/data/provider/snippet.freezed.dart
Normal file
@@ -0,0 +1,286 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'snippet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SnippetState {
|
||||
|
||||
List<Snippet> get snippets; Set<String> get tags;
|
||||
/// Create a copy of SnippetState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnippetStateCopyWith<SnippetState> get copyWith => _$SnippetStateCopyWithImpl<SnippetState>(this as SnippetState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnippetState&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.tags, tags));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(tags));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnippetState(snippets: $snippets, tags: $tags)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnippetStateCopyWith<$Res> {
|
||||
factory $SnippetStateCopyWith(SnippetState value, $Res Function(SnippetState) _then) = _$SnippetStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<Snippet> snippets, Set<String> tags
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnippetStateCopyWithImpl<$Res>
|
||||
implements $SnippetStateCopyWith<$Res> {
|
||||
_$SnippetStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnippetState _self;
|
||||
final $Res Function(SnippetState) _then;
|
||||
|
||||
/// Create a copy of SnippetState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? snippets = null,Object? tags = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
snippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable
|
||||
as List<Snippet>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as Set<String>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnippetState].
|
||||
extension SnippetStatePatterns on SnippetState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnippetState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnippetState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnippetState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnippetState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnippetState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnippetState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<Snippet> snippets, Set<String> tags)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnippetState() when $default != null:
|
||||
return $default(_that.snippets,_that.tags);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<Snippet> snippets, Set<String> tags) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnippetState():
|
||||
return $default(_that.snippets,_that.tags);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<Snippet> snippets, Set<String> tags)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnippetState() when $default != null:
|
||||
return $default(_that.snippets,_that.tags);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _SnippetState implements SnippetState {
|
||||
const _SnippetState({final List<Snippet> snippets = const <Snippet>[], final Set<String> tags = const <String>{}}): _snippets = snippets,_tags = tags;
|
||||
|
||||
|
||||
final List<Snippet> _snippets;
|
||||
@override@JsonKey() List<Snippet> get snippets {
|
||||
if (_snippets is EqualUnmodifiableListView) return _snippets;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_snippets);
|
||||
}
|
||||
|
||||
final Set<String> _tags;
|
||||
@override@JsonKey() Set<String> get tags {
|
||||
if (_tags is EqualUnmodifiableSetView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableSetView(_tags);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of SnippetState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnippetStateCopyWith<_SnippetState> get copyWith => __$SnippetStateCopyWithImpl<_SnippetState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnippetState&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._tags, _tags));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_tags));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnippetState(snippets: $snippets, tags: $tags)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnippetStateCopyWith<$Res> implements $SnippetStateCopyWith<$Res> {
|
||||
factory _$SnippetStateCopyWith(_SnippetState value, $Res Function(_SnippetState) _then) = __$SnippetStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<Snippet> snippets, Set<String> tags
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnippetStateCopyWithImpl<$Res>
|
||||
implements _$SnippetStateCopyWith<$Res> {
|
||||
__$SnippetStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnippetState _self;
|
||||
final $Res Function(_SnippetState) _then;
|
||||
|
||||
/// Create a copy of SnippetState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? snippets = null,Object? tags = null,}) {
|
||||
return _then(_SnippetState(
|
||||
snippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable
|
||||
as List<Snippet>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as Set<String>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
63
lib/data/provider/snippet.g.dart
Normal file
63
lib/data/provider/snippet.g.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'snippet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(SnippetNotifier)
|
||||
const snippetProvider = SnippetNotifierProvider._();
|
||||
|
||||
final class SnippetNotifierProvider
|
||||
extends $NotifierProvider<SnippetNotifier, SnippetState> {
|
||||
const SnippetNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'snippetProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$snippetNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SnippetNotifier create() => SnippetNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SnippetState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SnippetState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9';
|
||||
|
||||
abstract class _$SnippetNotifier extends $Notifier<SnippetState> {
|
||||
SnippetState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<SnippetState, SnippetState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<SnippetState, SnippetState>,
|
||||
SnippetState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,57 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/systemd.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
|
||||
final class SystemdProvider {
|
||||
late final VNode<Server> _si;
|
||||
part 'systemd.freezed.dart';
|
||||
part 'systemd.g.dart';
|
||||
|
||||
SystemdProvider.init(Spi spi) {
|
||||
_si = ServerProvider.pick(spi: spi)!;
|
||||
getUnits();
|
||||
}
|
||||
@freezed
|
||||
abstract class SystemdState with _$SystemdState {
|
||||
const factory SystemdState({
|
||||
@Default(false) bool isBusy,
|
||||
@Default(<SystemdUnit>[]) List<SystemdUnit> units,
|
||||
@Default(SystemdScopeFilter.all) SystemdScopeFilter scopeFilter,
|
||||
}) = _SystemdState;
|
||||
}
|
||||
|
||||
final isBusy = false.vn;
|
||||
final units = <SystemdUnit>[].vn;
|
||||
final scopeFilter = SystemdScopeFilter.all.vn;
|
||||
@riverpod
|
||||
class SystemdNotifier extends _$SystemdNotifier {
|
||||
late final ServerState _si;
|
||||
|
||||
void dispose() {
|
||||
isBusy.dispose();
|
||||
units.dispose();
|
||||
scopeFilter.dispose();
|
||||
@override
|
||||
SystemdState build(Spi spi) {
|
||||
final si = ref.read(serverProvider(spi.id));
|
||||
_si = si;
|
||||
// Async initialization
|
||||
Future.microtask(() => getUnits());
|
||||
return const SystemdState();
|
||||
}
|
||||
|
||||
List<SystemdUnit> get filteredUnits {
|
||||
switch (scopeFilter.value) {
|
||||
switch (state.scopeFilter) {
|
||||
case SystemdScopeFilter.all:
|
||||
return units.value;
|
||||
return state.units;
|
||||
case SystemdScopeFilter.system:
|
||||
return units.value.where((unit) => unit.scope == SystemdUnitScope.system).toList();
|
||||
return state.units.where((unit) => unit.scope == SystemdUnitScope.system).toList();
|
||||
case SystemdScopeFilter.user:
|
||||
return units.value.where((unit) => unit.scope == SystemdUnitScope.user).toList();
|
||||
return state.units.where((unit) => unit.scope == SystemdUnitScope.user).toList();
|
||||
}
|
||||
}
|
||||
|
||||
void setScopeFilter(SystemdScopeFilter filter) {
|
||||
state = state.copyWith(scopeFilter: filter);
|
||||
}
|
||||
|
||||
Future<void> getUnits() async {
|
||||
isBusy.value = true;
|
||||
state = state.copyWith(isBusy: true);
|
||||
|
||||
try {
|
||||
final client = _si.value.client;
|
||||
final client = _si.client;
|
||||
final result = await client!.execForOutput(_getUnitsCmd);
|
||||
final units = result.split('\n');
|
||||
|
||||
@@ -57,12 +69,11 @@ final class SystemdProvider {
|
||||
|
||||
final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||
final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
|
||||
state = state.copyWith(units: [...parsedUserUnits, ...parsedSystemUnits], isBusy: false);
|
||||
} catch (e, s) {
|
||||
dprint('Parse systemd', e, s);
|
||||
state = state.copyWith(isBusy: false);
|
||||
}
|
||||
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
|
||||
@@ -75,7 +86,7 @@ for unit in ${unitNames_.join(' ')}; do
|
||||
echo -n "\n${ScriptConstants.separator}\n"
|
||||
done
|
||||
''';
|
||||
final client = _si.value.client!;
|
||||
final client = _si.client!;
|
||||
final result = await client.execForOutput(script);
|
||||
final units = result.split(ScriptConstants.separator);
|
||||
|
||||
|
||||
283
lib/data/provider/systemd.freezed.dart
Normal file
283
lib/data/provider/systemd.freezed.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'systemd.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SystemdState {
|
||||
|
||||
bool get isBusy; List<SystemdUnit> get units; SystemdScopeFilter get scopeFilter;
|
||||
/// Create a copy of SystemdState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SystemdStateCopyWith<SystemdState> get copyWith => _$SystemdStateCopyWithImpl<SystemdState>(this as SystemdState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other.units, units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(units),scopeFilter);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SystemdStateCopyWith<$Res> {
|
||||
factory $SystemdStateCopyWith(SystemdState value, $Res Function(SystemdState) _then) = _$SystemdStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SystemdStateCopyWithImpl<$Res>
|
||||
implements $SystemdStateCopyWith<$Res> {
|
||||
_$SystemdStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SystemdState _self;
|
||||
final $Res Function(SystemdState) _then;
|
||||
|
||||
/// Create a copy of SystemdState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,units: null == units ? _self.units : units // ignore: cast_nullable_to_non_nullable
|
||||
as List<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable
|
||||
as SystemdScopeFilter,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SystemdState].
|
||||
extension SystemdStatePatterns on SystemdState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SystemdState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SystemdState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SystemdState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SystemdState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SystemdState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SystemdState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SystemdState() when $default != null:
|
||||
return $default(_that.isBusy,_that.units,_that.scopeFilter);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SystemdState():
|
||||
return $default(_that.isBusy,_that.units,_that.scopeFilter);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SystemdState() when $default != null:
|
||||
return $default(_that.isBusy,_that.units,_that.scopeFilter);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _SystemdState implements SystemdState {
|
||||
const _SystemdState({this.isBusy = false, final List<SystemdUnit> units = const <SystemdUnit>[], this.scopeFilter = SystemdScopeFilter.all}): _units = units;
|
||||
|
||||
|
||||
@override@JsonKey() final bool isBusy;
|
||||
final List<SystemdUnit> _units;
|
||||
@override@JsonKey() List<SystemdUnit> get units {
|
||||
if (_units is EqualUnmodifiableListView) return _units;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_units);
|
||||
}
|
||||
|
||||
@override@JsonKey() final SystemdScopeFilter scopeFilter;
|
||||
|
||||
/// Create a copy of SystemdState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SystemdStateCopyWith<_SystemdState> get copyWith => __$SystemdStateCopyWithImpl<_SystemdState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other._units, _units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(_units),scopeFilter);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SystemdStateCopyWith<$Res> implements $SystemdStateCopyWith<$Res> {
|
||||
factory _$SystemdStateCopyWith(_SystemdState value, $Res Function(_SystemdState) _then) = __$SystemdStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool isBusy, List<SystemdUnit> units, SystemdScopeFilter scopeFilter
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SystemdStateCopyWithImpl<$Res>
|
||||
implements _$SystemdStateCopyWith<$Res> {
|
||||
__$SystemdStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SystemdState _self;
|
||||
final $Res Function(_SystemdState) _then;
|
||||
|
||||
/// Create a copy of SystemdState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) {
|
||||
return _then(_SystemdState(
|
||||
isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,units: null == units ? _self._units : units // ignore: cast_nullable_to_non_nullable
|
||||
as List<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable
|
||||
as SystemdScopeFilter,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
108
lib/data/provider/systemd.g.dart
Normal file
108
lib/data/provider/systemd.g.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'systemd.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(SystemdNotifier)
|
||||
const systemdProvider = SystemdNotifierFamily._();
|
||||
|
||||
final class SystemdNotifierProvider
|
||||
extends $NotifierProvider<SystemdNotifier, SystemdState> {
|
||||
const SystemdNotifierProvider._({
|
||||
required SystemdNotifierFamily super.from,
|
||||
required Spi super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'systemdProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$systemdNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'systemdProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SystemdNotifier create() => SystemdNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SystemdState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SystemdState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SystemdNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$systemdNotifierHash() => r'030d556efc3d897419cd3462d37cb705813e24c7';
|
||||
|
||||
final class SystemdNotifierFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
SystemdNotifier,
|
||||
SystemdState,
|
||||
SystemdState,
|
||||
SystemdState,
|
||||
Spi
|
||||
> {
|
||||
const SystemdNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'systemdProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
SystemdNotifierProvider call(Spi spi) =>
|
||||
SystemdNotifierProvider._(argument: spi, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'systemdProvider';
|
||||
}
|
||||
|
||||
abstract class _$SystemdNotifier extends $Notifier<SystemdState> {
|
||||
late final _$args = ref.$arg as Spi;
|
||||
Spi get spi => _$args;
|
||||
|
||||
SystemdState build(Spi spi);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<SystemdState, SystemdState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<SystemdState, SystemdState>,
|
||||
SystemdState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,63 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:xterm/core.dart';
|
||||
|
||||
class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier {
|
||||
VirtKeyProvider();
|
||||
part 'virtual_keyboard.g.dart';
|
||||
part 'virtual_keyboard.freezed.dart';
|
||||
|
||||
bool _ctrl = false;
|
||||
bool get ctrl => _ctrl;
|
||||
set ctrl(bool value) {
|
||||
if (value != _ctrl) {
|
||||
_ctrl = value;
|
||||
notifyListeners();
|
||||
@freezed
|
||||
abstract class VirtKeyState with _$VirtKeyState {
|
||||
const factory VirtKeyState({
|
||||
@Default(false) final bool ctrl,
|
||||
@Default(false) final bool alt,
|
||||
@Default(false) final bool shift,
|
||||
}) = _VirtKeyState;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class VirtKeyboard extends _$VirtKeyboard implements TerminalInputHandler {
|
||||
@override
|
||||
VirtKeyState build() {
|
||||
return const VirtKeyState();
|
||||
}
|
||||
|
||||
bool get ctrl => state.ctrl;
|
||||
bool get alt => state.alt;
|
||||
bool get shift => state.shift;
|
||||
|
||||
void setCtrl(bool value) {
|
||||
if (value != state.ctrl) {
|
||||
state = state.copyWith(ctrl: value);
|
||||
}
|
||||
}
|
||||
|
||||
bool _alt = false;
|
||||
bool get alt => _alt;
|
||||
set alt(bool value) {
|
||||
if (value != _alt) {
|
||||
_alt = value;
|
||||
notifyListeners();
|
||||
void setAlt(bool value) {
|
||||
if (value != state.alt) {
|
||||
state = state.copyWith(alt: value);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shift = false;
|
||||
bool get shift => _shift;
|
||||
set shift(bool value) {
|
||||
if (value != _shift) {
|
||||
_shift = value;
|
||||
notifyListeners();
|
||||
void setShift(bool value) {
|
||||
if (value != state.shift) {
|
||||
state = state.copyWith(shift: value);
|
||||
}
|
||||
}
|
||||
|
||||
void reset(TerminalKeyboardEvent e) {
|
||||
if (e.ctrl) {
|
||||
ctrl = false;
|
||||
}
|
||||
if (e.alt) {
|
||||
alt = false;
|
||||
}
|
||||
if (e.shift) {
|
||||
shift = false;
|
||||
}
|
||||
notifyListeners();
|
||||
state = state.copyWith(
|
||||
ctrl: e.ctrl ? false : state.ctrl,
|
||||
alt: e.alt ? false : state.alt,
|
||||
shift: e.shift ? false : state.shift,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String? call(TerminalKeyboardEvent event) {
|
||||
final e = event.copyWith(
|
||||
ctrl: event.ctrl || ctrl,
|
||||
alt: event.alt || alt,
|
||||
shift: event.shift || shift,
|
||||
ctrl: event.ctrl || state.ctrl,
|
||||
alt: event.alt || state.alt,
|
||||
shift: event.shift || state.shift,
|
||||
);
|
||||
if (Stores.setting.sshVirtualKeyAutoOff.fetch()) {
|
||||
reset(e);
|
||||
|
||||
277
lib/data/provider/virtual_keyboard.freezed.dart
Normal file
277
lib/data/provider/virtual_keyboard.freezed.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'virtual_keyboard.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$VirtKeyState {
|
||||
|
||||
bool get ctrl; bool get alt; bool get shift;
|
||||
/// Create a copy of VirtKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$VirtKeyStateCopyWith<VirtKeyState> get copyWith => _$VirtKeyStateCopyWithImpl<VirtKeyState>(this as VirtKeyState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,ctrl,alt,shift);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $VirtKeyStateCopyWith<$Res> {
|
||||
factory $VirtKeyStateCopyWith(VirtKeyState value, $Res Function(VirtKeyState) _then) = _$VirtKeyStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool ctrl, bool alt, bool shift
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$VirtKeyStateCopyWithImpl<$Res>
|
||||
implements $VirtKeyStateCopyWith<$Res> {
|
||||
_$VirtKeyStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final VirtKeyState _self;
|
||||
final $Res Function(VirtKeyState) _then;
|
||||
|
||||
/// Create a copy of VirtKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
ctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable
|
||||
as bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable
|
||||
as bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [VirtKeyState].
|
||||
extension VirtKeyStatePatterns on VirtKeyState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _VirtKeyState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VirtKeyState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _VirtKeyState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VirtKeyState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _VirtKeyState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VirtKeyState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool ctrl, bool alt, bool shift)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VirtKeyState() when $default != null:
|
||||
return $default(_that.ctrl,_that.alt,_that.shift);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool ctrl, bool alt, bool shift) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VirtKeyState():
|
||||
return $default(_that.ctrl,_that.alt,_that.shift);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool ctrl, bool alt, bool shift)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VirtKeyState() when $default != null:
|
||||
return $default(_that.ctrl,_that.alt,_that.shift);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _VirtKeyState implements VirtKeyState {
|
||||
const _VirtKeyState({this.ctrl = false, this.alt = false, this.shift = false});
|
||||
|
||||
|
||||
@override@JsonKey() final bool ctrl;
|
||||
@override@JsonKey() final bool alt;
|
||||
@override@JsonKey() final bool shift;
|
||||
|
||||
/// Create a copy of VirtKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$VirtKeyStateCopyWith<_VirtKeyState> get copyWith => __$VirtKeyStateCopyWithImpl<_VirtKeyState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,ctrl,alt,shift);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$VirtKeyStateCopyWith<$Res> implements $VirtKeyStateCopyWith<$Res> {
|
||||
factory _$VirtKeyStateCopyWith(_VirtKeyState value, $Res Function(_VirtKeyState) _then) = __$VirtKeyStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool ctrl, bool alt, bool shift
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$VirtKeyStateCopyWithImpl<$Res>
|
||||
implements _$VirtKeyStateCopyWith<$Res> {
|
||||
__$VirtKeyStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _VirtKeyState _self;
|
||||
final $Res Function(_VirtKeyState) _then;
|
||||
|
||||
/// Create a copy of VirtKeyState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) {
|
||||
return _then(_VirtKeyState(
|
||||
ctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable
|
||||
as bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable
|
||||
as bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
63
lib/data/provider/virtual_keyboard.g.dart
Normal file
63
lib/data/provider/virtual_keyboard.g.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'virtual_keyboard.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(VirtKeyboard)
|
||||
const virtKeyboardProvider = VirtKeyboardProvider._();
|
||||
|
||||
final class VirtKeyboardProvider
|
||||
extends $NotifierProvider<VirtKeyboard, VirtKeyState> {
|
||||
const VirtKeyboardProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'virtKeyboardProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$virtKeyboardHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
VirtKeyboard create() => VirtKeyboard();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(VirtKeyState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<VirtKeyState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$virtKeyboardHash() => r'1327d412bfb0dd261f3b555f353a8852b4f753e5';
|
||||
|
||||
abstract class _$VirtKeyboard extends $Notifier<VirtKeyState> {
|
||||
VirtKeyState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<VirtKeyState, VirtKeyState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<VirtKeyState, VirtKeyState>,
|
||||
VirtKeyState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
abstract class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 1220;
|
||||
static const int script = 68;
|
||||
static const int build = 1291;
|
||||
static const int script = 70;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ abstract final class GithubIds {
|
||||
'MasedMSD',
|
||||
'GitGitro',
|
||||
'Shin-suechtig',
|
||||
'GT-610'
|
||||
};
|
||||
|
||||
static const participants = <GhId>{
|
||||
@@ -124,6 +125,12 @@ abstract final class GithubIds {
|
||||
'CreeperKong',
|
||||
'zxf945',
|
||||
'cnen2018',
|
||||
'xiaomeng9597',
|
||||
'mingzhao2019',
|
||||
'HHXXYY123',
|
||||
'Lancerys',
|
||||
'yaziku',
|
||||
'yeluosln',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:server_box/data/store/connection_stats.dart';
|
||||
import 'package:server_box/data/store/container.dart';
|
||||
import 'package:server_box/data/store/history.dart';
|
||||
import 'package:server_box/data/store/private_key.dart';
|
||||
@@ -6,25 +8,37 @@ import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/data/store/setting.dart';
|
||||
import 'package:server_box/data/store/snippet.dart';
|
||||
|
||||
final GetIt getIt = GetIt.instance;
|
||||
|
||||
abstract final class Stores {
|
||||
static final setting = SettingStore.instance;
|
||||
static final server = ServerStore.instance;
|
||||
static final container = ContainerStore.instance;
|
||||
static final key = PrivateKeyStore.instance;
|
||||
static final snippet = SnippetStore.instance;
|
||||
static final history = HistoryStore.instance;
|
||||
static SettingStore get setting => getIt<SettingStore>();
|
||||
static ServerStore get server => getIt<ServerStore>();
|
||||
static ContainerStore get container => getIt<ContainerStore>();
|
||||
static PrivateKeyStore get key => getIt<PrivateKeyStore>();
|
||||
static SnippetStore get snippet => getIt<SnippetStore>();
|
||||
static HistoryStore get history => getIt<HistoryStore>();
|
||||
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
|
||||
|
||||
/// All stores that need backup
|
||||
static final List<HiveStore> _allBackup = [
|
||||
SettingStore.instance,
|
||||
ServerStore.instance,
|
||||
ContainerStore.instance,
|
||||
PrivateKeyStore.instance,
|
||||
SnippetStore.instance,
|
||||
HistoryStore.instance,
|
||||
];
|
||||
static List<HiveStore> get _allBackup => [
|
||||
setting,
|
||||
server,
|
||||
container,
|
||||
key,
|
||||
snippet,
|
||||
history,
|
||||
connectionStats,
|
||||
];
|
||||
|
||||
static Future<void> init() async {
|
||||
getIt.registerLazySingleton<SettingStore>(() => SettingStore.instance);
|
||||
getIt.registerLazySingleton<ServerStore>(() => ServerStore.instance);
|
||||
getIt.registerLazySingleton<ContainerStore>(() => ContainerStore.instance);
|
||||
getIt.registerLazySingleton<PrivateKeyStore>(() => PrivateKeyStore.instance);
|
||||
getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);
|
||||
getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);
|
||||
getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);
|
||||
|
||||
await Future.wait(_allBackup.map((store) => store.init()));
|
||||
}
|
||||
|
||||
|
||||
@@ -51,12 +51,31 @@ abstract final class TermSessionManager {
|
||||
|
||||
static void init() {
|
||||
if (isAndroid) {
|
||||
MethodChans.registerHandler((id) async {
|
||||
_entries[id]?.disconnect?.call();
|
||||
});
|
||||
MethodChans.registerHandler(
|
||||
(id) async {
|
||||
_entries[id]?.disconnect?.call();
|
||||
},
|
||||
() {
|
||||
// Stop all connections when notification "Stop All" is pressed
|
||||
stopAllConnections();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when Android notification "Stop All" button is pressed
|
||||
static void stopAllConnections() {
|
||||
// Disconnect all sessions
|
||||
final disconnectCallbacks = _entries.values.map((e) => e.disconnect).where((cb) => cb != null).toList();
|
||||
for (final disconnect in disconnectCallbacks) {
|
||||
disconnect!();
|
||||
}
|
||||
// Clear all entries
|
||||
_entries.clear();
|
||||
_activeId = null;
|
||||
_sync();
|
||||
}
|
||||
|
||||
/// Add a session record and push update to Android.
|
||||
static void add({
|
||||
required String id,
|
||||
|
||||
190
lib/data/store/connection_stats.dart
Normal file
190
lib/data/store/connection_stats.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||
|
||||
class ConnectionStatsStore extends HiveStore {
|
||||
ConnectionStatsStore._() : super('connection_stats');
|
||||
|
||||
static final instance = ConnectionStatsStore._();
|
||||
|
||||
// Record a connection attempt
|
||||
void recordConnection(ConnectionStat stat) {
|
||||
final key = '${stat.serverId}_${ShortId.generate()}';
|
||||
set(key, stat);
|
||||
_cleanOldRecords(stat.serverId);
|
||||
}
|
||||
|
||||
// Clean records older than 30 days for a specific server
|
||||
void _cleanOldRecords(String serverId) {
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(days: 30));
|
||||
final allKeys = keys().toList();
|
||||
final keysToDelete = <String>[];
|
||||
|
||||
for (final key in allKeys) {
|
||||
if (key.startsWith(serverId)) {
|
||||
final parts = key.split('_');
|
||||
if (parts.length >= 2) {
|
||||
final timestamp = int.tryParse(parts.last);
|
||||
if (timestamp != null) {
|
||||
final recordTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
if (recordTime.isBefore(cutoffTime)) {
|
||||
keysToDelete.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToDelete) {
|
||||
remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Get connection stats for a specific server
|
||||
ServerConnectionStats getServerStats(String serverId, String serverName) {
|
||||
final allStats = getConnectionHistory(serverId);
|
||||
|
||||
if (allStats.isEmpty) {
|
||||
return ServerConnectionStats(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
totalAttempts: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
recentConnections: [],
|
||||
successRate: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
final totalAttempts = allStats.length;
|
||||
final successCount = allStats.where((s) => s.result.isSuccess).length;
|
||||
final failureCount = totalAttempts - successCount;
|
||||
final successRate = totalAttempts > 0 ? (successCount / totalAttempts) : 0.0;
|
||||
|
||||
final successTimes = allStats
|
||||
.where((s) => s.result.isSuccess)
|
||||
.map((s) => s.timestamp)
|
||||
.toList();
|
||||
final failureTimes = allStats
|
||||
.where((s) => !s.result.isSuccess)
|
||||
.map((s) => s.timestamp)
|
||||
.toList();
|
||||
|
||||
DateTime? lastSuccessTime;
|
||||
DateTime? lastFailureTime;
|
||||
|
||||
if (successTimes.isNotEmpty) {
|
||||
successTimes.sort((a, b) => b.compareTo(a));
|
||||
lastSuccessTime = successTimes.first;
|
||||
}
|
||||
|
||||
if (failureTimes.isNotEmpty) {
|
||||
failureTimes.sort((a, b) => b.compareTo(a));
|
||||
lastFailureTime = failureTimes.first;
|
||||
}
|
||||
|
||||
// Get recent connections (last 20)
|
||||
final recentConnections = allStats.take(20).toList();
|
||||
|
||||
return ServerConnectionStats(
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
totalAttempts: totalAttempts,
|
||||
successCount: successCount,
|
||||
failureCount: failureCount,
|
||||
lastSuccessTime: lastSuccessTime,
|
||||
lastFailureTime: lastFailureTime,
|
||||
recentConnections: recentConnections,
|
||||
successRate: successRate,
|
||||
);
|
||||
}
|
||||
|
||||
// Get connection history for a specific server
|
||||
List<ConnectionStat> getConnectionHistory(String serverId) {
|
||||
final allKeys = keys().where((key) => key.startsWith(serverId)).toList();
|
||||
final stats = <ConnectionStat>[];
|
||||
|
||||
for (final key in allKeys) {
|
||||
final stat = get<ConnectionStat>(
|
||||
key,
|
||||
fromObj: (val) {
|
||||
if (val is ConnectionStat) return val;
|
||||
if (val is Map<dynamic, dynamic>) {
|
||||
final map = val.toStrDynMap;
|
||||
if (map == null) return null;
|
||||
try {
|
||||
return ConnectionStat.fromJson(map as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
dprint('Parsing ConnectionStat from JSON', e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (stat != null) {
|
||||
stats.add(stat);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp, newest first
|
||||
stats.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Get all servers' stats
|
||||
List<ServerConnectionStats> getAllServerStats() {
|
||||
final serverIds = <String>{};
|
||||
final serverNames = <String, String>{};
|
||||
|
||||
// Get all unique server IDs
|
||||
for (final key in keys()) {
|
||||
final parts = key.split('_');
|
||||
if (parts.length >= 2) {
|
||||
final serverId = parts[0];
|
||||
serverIds.add(serverId);
|
||||
|
||||
// Try to get server name from the stored stat
|
||||
final stat = get<ConnectionStat>(
|
||||
key,
|
||||
fromObj: (val) {
|
||||
if (val is ConnectionStat) return val;
|
||||
if (val is Map<dynamic, dynamic>) {
|
||||
final map = val.toStrDynMap;
|
||||
if (map == null) return null;
|
||||
try {
|
||||
return ConnectionStat.fromJson(map as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
dprint('Parsing ConnectionStat from JSON', e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (stat != null) {
|
||||
serverNames[serverId] = stat.serverName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final allStats = <ServerConnectionStats>[];
|
||||
for (final serverId in serverIds) {
|
||||
final serverName = serverNames[serverId] ?? serverId;
|
||||
final stats = getServerStats(serverId, serverName);
|
||||
allStats.add(stats);
|
||||
}
|
||||
|
||||
return allStats;
|
||||
}
|
||||
|
||||
// Clear all connection stats
|
||||
void clearAll() {
|
||||
box.clear();
|
||||
}
|
||||
|
||||
// Clear stats for a specific server
|
||||
void clearServerStats(String serverId) {
|
||||
final keysToDelete = keys().where((key) => key.startsWith(serverId)).toList();
|
||||
for (final key in keysToDelete) {
|
||||
remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/app/menu/server_func.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||
import 'package:server_box/data/model/app/tab.dart';
|
||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
import 'package:server_box/data/res/default.dart';
|
||||
|
||||
@@ -22,10 +23,7 @@ class SettingStore extends HiveStore {
|
||||
// late final launchPage = property('launchPage', Defaults.launchPageIdx);
|
||||
|
||||
/// Disk view: amount / IO
|
||||
late final serverTabPreferDiskAmount = propertyDefault(
|
||||
'serverTabPreferDiskAmount',
|
||||
false,
|
||||
);
|
||||
late final serverTabPreferDiskAmount = propertyDefault('serverTabPreferDiskAmount', false);
|
||||
|
||||
/// Bigger for bigger font size
|
||||
/// 1.0 means 100%
|
||||
@@ -70,20 +68,26 @@ class SettingStore extends HiveStore {
|
||||
late final locale = propertyDefault('locale', '');
|
||||
|
||||
// SSH virtual key (ctrl | alt) auto turn off
|
||||
late final sshVirtualKeyAutoOff = propertyDefault(
|
||||
'sshVirtualKeyAutoOff',
|
||||
true,
|
||||
);
|
||||
late final sshVirtualKeyAutoOff = propertyDefault('sshVirtualKeyAutoOff', true);
|
||||
|
||||
late final editorFontSize = propertyDefault('editorFontSize', 12.5);
|
||||
|
||||
/// 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
|
||||
late final editorTheme = propertyDefault('editorTheme', Defaults.editorTheme);
|
||||
|
||||
late final editorDarkTheme = propertyDefault(
|
||||
'editorDarkTheme',
|
||||
Defaults.editorDarkTheme,
|
||||
);
|
||||
late final editorDarkTheme = propertyDefault('editorDarkTheme', Defaults.editorDarkTheme);
|
||||
|
||||
late final fullScreen = propertyDefault('fullScreen', false);
|
||||
|
||||
@@ -113,29 +117,20 @@ class SettingStore extends HiveStore {
|
||||
);
|
||||
|
||||
// Only valid on iOS
|
||||
late final autoUpdateHomeWidget = propertyDefault(
|
||||
'autoUpdateHomeWidget',
|
||||
isIOS,
|
||||
);
|
||||
late final autoUpdateHomeWidget = propertyDefault('autoUpdateHomeWidget', isIOS);
|
||||
|
||||
late final autoCheckAppUpdate = propertyDefault('autoCheckAppUpdate', true);
|
||||
|
||||
/// Display server tab function buttons on the bottom of each server card if [true]
|
||||
///
|
||||
/// Otherwise, display them on the top of server detail page
|
||||
late final moveServerFuncs = propertyDefault(
|
||||
'moveOutServerTabFuncBtns',
|
||||
false,
|
||||
);
|
||||
late final moveServerFuncs = propertyDefault('moveOutServerTabFuncBtns', false);
|
||||
|
||||
/// Whether use `rm -r` to delete directory on SFTP
|
||||
late final sftpRmrDir = propertyDefault('sftpRmrDir', false);
|
||||
|
||||
/// Whether use system's primary color as the app's primary color
|
||||
late final useSystemPrimaryColor = propertyDefault(
|
||||
'useSystemPrimaryColor',
|
||||
false,
|
||||
);
|
||||
late final useSystemPrimaryColor = propertyDefault('useSystemPrimaryColor', false);
|
||||
|
||||
/// Only valid on iOS / Android / Windows
|
||||
late final useBioAuth = propertyDefault('useBioAuth', false);
|
||||
@@ -151,10 +146,7 @@ class SettingStore extends HiveStore {
|
||||
late final sftpOpenLastPath = propertyDefault('sftpOpenLastPath', true);
|
||||
|
||||
/// Show folders first in SFTP file browser
|
||||
late final sftpShowFoldersFirst = propertyDefault(
|
||||
'sftpShowFoldersFirst',
|
||||
true,
|
||||
);
|
||||
late final sftpShowFoldersFirst = propertyDefault('sftpShowFoldersFirst', true);
|
||||
|
||||
/// Show tip of suspend
|
||||
late final showSuspendTip = propertyDefault('showSuspendTip', true);
|
||||
@@ -162,10 +154,12 @@ class SettingStore extends HiveStore {
|
||||
/// Whether collapse UI items by default
|
||||
late final collapseUIDefault = propertyDefault('collapseUIDefault', true);
|
||||
|
||||
late final serverFuncBtns = listProperty(
|
||||
'serverBtns',
|
||||
defaultValue: ServerFuncBtn.defaultIdxs,
|
||||
);
|
||||
/// 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);
|
||||
|
||||
/// Docker is more popular than podman, set to `false` to use docker
|
||||
late final usePodman = propertyDefault('usePodman', false);
|
||||
@@ -180,16 +174,10 @@ class SettingStore extends HiveStore {
|
||||
late final containerParseStat = propertyDefault('containerParseStat', true);
|
||||
|
||||
/// Auto refresh container status
|
||||
late final containerAutoRefresh = propertyDefault(
|
||||
'containerAutoRefresh',
|
||||
true,
|
||||
);
|
||||
late final containerAutoRefresh = propertyDefault('containerAutoRefresh', true);
|
||||
|
||||
/// Use double column servers page on Desktop
|
||||
late final doubleColumnServersPage = propertyDefault(
|
||||
'doubleColumnServersPage',
|
||||
true,
|
||||
);
|
||||
late final doubleColumnServersPage = propertyDefault('doubleColumnServersPage', true);
|
||||
|
||||
/// Ignore local network device (eg: br-xxx, ovs-system...)
|
||||
/// when building traffic view on server tab
|
||||
@@ -244,8 +232,7 @@ class SettingStore extends HiveStore {
|
||||
/// Record the position and size of the window.
|
||||
late final windowState = property<WindowState>(
|
||||
'windowState',
|
||||
fromObj: (raw) =>
|
||||
WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
|
||||
fromObj: (raw) => WindowState.fromJson(jsonDecode(raw as String) as Map<String, dynamic>),
|
||||
toObj: (state) => state == null ? null : jsonEncode(state.toJson()),
|
||||
);
|
||||
|
||||
@@ -258,10 +245,7 @@ class SettingStore extends HiveStore {
|
||||
late final sftpEditor = propertyDefault('sftpEditor', '');
|
||||
|
||||
/// Preferred terminal emulator command on desktop
|
||||
late final desktopTerminal = propertyDefault(
|
||||
'desktopTerminal',
|
||||
'x-terminal-emulator',
|
||||
);
|
||||
late final desktopTerminal = propertyDefault('desktopTerminal', 'x-terminal-emulator');
|
||||
|
||||
/// Run foreground service on Android, if the SSH terminal is running
|
||||
late final fgService = propertyDefault('fgService', false);
|
||||
@@ -277,4 +261,17 @@ class SettingStore extends HiveStore {
|
||||
|
||||
/// The backup password
|
||||
late final backupasswd = SecureProp('bakPasswd');
|
||||
|
||||
/// Whether to read SSH config from ~/.ssh/config on first time
|
||||
late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);
|
||||
|
||||
/// Tabs at home page
|
||||
late final homeTabs = listProperty(
|
||||
'homeTabs',
|
||||
defaultValue: AppTab.values,
|
||||
fromObj: AppTab.parseAppTabsFromObj,
|
||||
toObj: (val) {
|
||||
return val?.map((e) => e.name).toList() ?? [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,6 +155,108 @@ abstract class AppLocalizations {
|
||||
/// **'Already in last directory.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'At least one tab must be selected'**
|
||||
String get atLeastOneTab;
|
||||
|
||||
/// No description provided for @authFailTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -185,35 +287,11 @@ abstract class AppLocalizations {
|
||||
/// **'Automatic home widget update'**
|
||||
String get autoUpdateHomeWidget;
|
||||
|
||||
/// No description provided for @backupTip.
|
||||
/// No description provided for @availableTabs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
|
||||
String get backupTip;
|
||||
|
||||
/// No description provided for @backupVersionNotMatch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup version is not match.'**
|
||||
String get backupVersionNotMatch;
|
||||
|
||||
/// No description provided for @backupPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password'**
|
||||
String get backupPassword;
|
||||
|
||||
/// No description provided for @backupPasswordTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**
|
||||
String get backupPasswordTip;
|
||||
|
||||
/// No description provided for @backupPasswordWrong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Incorrect backup password'**
|
||||
String get backupPasswordWrong;
|
||||
/// **'Available Tabs'**
|
||||
String get availableTabs;
|
||||
|
||||
/// No description provided for @backupEncrypted.
|
||||
///
|
||||
@@ -227,11 +305,11 @@ abstract class AppLocalizations {
|
||||
/// **'Backup is not encrypted'**
|
||||
String get backupNotEncrypted;
|
||||
|
||||
/// No description provided for @backupPasswordSet.
|
||||
/// No description provided for @backupPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password set'**
|
||||
String get backupPasswordSet;
|
||||
/// **'Backup password'**
|
||||
String get backupPassword;
|
||||
|
||||
/// No description provided for @backupPasswordRemoved.
|
||||
///
|
||||
@@ -239,6 +317,36 @@ abstract class AppLocalizations {
|
||||
/// **'Backup password removed'**
|
||||
String get backupPasswordRemoved;
|
||||
|
||||
/// No description provided for @backupPasswordSet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup password set'**
|
||||
String get backupPasswordSet;
|
||||
|
||||
/// No description provided for @backupPasswordTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set a password to encrypt backup files. Leave empty to disable encryption.'**
|
||||
String get backupPasswordTip;
|
||||
|
||||
/// No description provided for @backupPasswordWrong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Incorrect backup password'**
|
||||
String get backupPasswordWrong;
|
||||
|
||||
/// No description provided for @backupTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
|
||||
String get backupTip;
|
||||
|
||||
/// No description provided for @backupVersionNotMatch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Backup version is not match.'**
|
||||
String get backupVersionNotMatch;
|
||||
|
||||
/// No description provided for @battery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -257,6 +365,36 @@ abstract class AppLocalizations {
|
||||
/// **'This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \"Battery Optimization\" in this app. For MIUI / HyperOS, please change the power saving policy to \"Unlimited\".'**
|
||||
String get bgRunTip;
|
||||
|
||||
/// No description provided for @clearAllStatsContent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to clear all server connection statistics? This action cannot be undone.'**
|
||||
String get clearAllStatsContent;
|
||||
|
||||
/// No description provided for @clearAllStatsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear All Statistics'**
|
||||
String get clearAllStatsTitle;
|
||||
|
||||
/// No description provided for @clearServerStatsContent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to clear connection statistics for server \"{serverName}\"? This action cannot be undone.'**
|
||||
String clearServerStatsContent(Object serverName);
|
||||
|
||||
/// No description provided for @clearServerStatsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear {serverName} Statistics'**
|
||||
String clearServerStatsTitle(Object serverName);
|
||||
|
||||
/// No description provided for @clearThisServerStats.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear This Server Statistics'**
|
||||
String get clearThisServerStats;
|
||||
|
||||
/// No description provided for @closeAfterSave.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -281,6 +419,24 @@ abstract class AppLocalizations {
|
||||
/// **'Connection'**
|
||||
String get conn;
|
||||
|
||||
/// No description provided for @connectionDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connection Details'**
|
||||
String get connectionDetails;
|
||||
|
||||
/// No description provided for @connectionStats.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connection Statistics'**
|
||||
String get connectionStats;
|
||||
|
||||
/// No description provided for @connectionStatsDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'View server connection success rate and history'**
|
||||
String get connectionStatsDesc;
|
||||
|
||||
/// No description provided for @container.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -371,6 +527,30 @@ abstract class AppLocalizations {
|
||||
/// **'Disconnected'**
|
||||
String get disconnected;
|
||||
|
||||
/// No description provided for @discoverSshServers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discover SSH Servers'**
|
||||
String get discoverSshServers;
|
||||
|
||||
/// No description provided for @discoveryFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discovery failed'**
|
||||
String get discoveryFailed;
|
||||
|
||||
/// No description provided for @discoverySettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discovery Settings'**
|
||||
String get discoverySettings;
|
||||
|
||||
/// No description provided for @discoverySummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discovery Summary'**
|
||||
String get discoverySummary;
|
||||
|
||||
/// No description provided for @disk.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -452,12 +632,6 @@ abstract class AppLocalizations {
|
||||
/// **'Edit virtual keys'**
|
||||
String get editVirtKeys;
|
||||
|
||||
/// No description provided for @editor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Editor'**
|
||||
String get editor;
|
||||
|
||||
/// No description provided for @editorHighlightTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -470,6 +644,18 @@ abstract class AppLocalizations {
|
||||
/// **'Emulator'**
|
||||
String get emulator;
|
||||
|
||||
/// No description provided for @enableMdns.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable mDNS'**
|
||||
String get enableMdns;
|
||||
|
||||
/// No description provided for @enableMdnsDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use mDNS/Bonjour to discover SSH services'**
|
||||
String get enableMdnsDesc;
|
||||
|
||||
/// No description provided for @encode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -524,18 +710,18 @@ abstract class AppLocalizations {
|
||||
/// **'File \'{file}\' too large {size}, max {sizeMax}'**
|
||||
String fileTooLarge(Object file, Object size, Object sizeMax);
|
||||
|
||||
/// No description provided for @finishedAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finished at'**
|
||||
String get finishedAt;
|
||||
|
||||
/// No description provided for @followSystem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Follow system'**
|
||||
String get followSystem;
|
||||
|
||||
/// No description provided for @font.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Font'**
|
||||
String get font;
|
||||
|
||||
/// No description provided for @fontSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -596,6 +782,18 @@ abstract class AppLocalizations {
|
||||
/// **'Code highlighting'**
|
||||
String get highlight;
|
||||
|
||||
/// No description provided for @homeTabs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Home Tabs'**
|
||||
String get homeTabs;
|
||||
|
||||
/// No description provided for @homeTabsCustomizeDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Customize which tabs appear on the home page and their order'**
|
||||
String get homeTabsCustomizeDesc;
|
||||
|
||||
/// No description provided for @homeWidgetUrlConfig.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -632,12 +830,6 @@ abstract class AppLocalizations {
|
||||
/// **'Images list'**
|
||||
String get imagesList;
|
||||
|
||||
/// No description provided for @init.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Initialize'**
|
||||
String get init;
|
||||
|
||||
/// No description provided for @inner.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -692,6 +884,18 @@ abstract class AppLocalizations {
|
||||
/// **'Key Auth'**
|
||||
String get keyAuth;
|
||||
|
||||
/// No description provided for @lastFailure.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last Failure'**
|
||||
String get lastFailure;
|
||||
|
||||
/// No description provided for @lastSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last Success'**
|
||||
String get lastSuccess;
|
||||
|
||||
/// No description provided for @letterCache.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -704,12 +908,6 @@ abstract class AppLocalizations {
|
||||
/// **'Recommended to disable, but after disabling, it will be impossible to input CJK characters.'**
|
||||
String get letterCacheTip;
|
||||
|
||||
/// No description provided for @license.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'License'**
|
||||
String get license;
|
||||
|
||||
/// No description provided for @location.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -728,18 +926,18 @@ abstract class AppLocalizations {
|
||||
/// **'Made with ❤️ by {myGithub}'**
|
||||
String madeWithLove(Object myGithub);
|
||||
|
||||
/// No description provided for @manual.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manual'**
|
||||
String get manual;
|
||||
|
||||
/// No description provided for @max.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'max'**
|
||||
String get max;
|
||||
|
||||
/// No description provided for @maxConcurrency.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Max Concurrency'**
|
||||
String get maxConcurrency;
|
||||
|
||||
/// No description provided for @maxRetryCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -812,6 +1010,12 @@ abstract class AppLocalizations {
|
||||
/// **'New container'**
|
||||
String get newContainer;
|
||||
|
||||
/// No description provided for @noConnectionStatsData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No connection statistics data'**
|
||||
String get noConnectionStatsData;
|
||||
|
||||
/// No description provided for @noLineChart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -938,18 +1142,18 @@ abstract class AppLocalizations {
|
||||
/// **'Prioritize displaying disk capacity'**
|
||||
String get preferDiskAmount;
|
||||
|
||||
/// No description provided for @preview.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Preview'**
|
||||
String get preview;
|
||||
|
||||
/// No description provided for @privateKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Private Key'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -998,6 +1202,12 @@ abstract class AppLocalizations {
|
||||
/// **'Reboot'**
|
||||
String get reboot;
|
||||
|
||||
/// No description provided for @recentConnections.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recent Connections'**
|
||||
String get recentConnections;
|
||||
|
||||
/// No description provided for @rememberPwdInMem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1118,6 +1328,18 @@ abstract class AppLocalizations {
|
||||
/// **'Server order'**
|
||||
String get serverOrder;
|
||||
|
||||
/// No description provided for @serverTabRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Server tab cannot be removed'**
|
||||
String get serverTabRequired;
|
||||
|
||||
/// No description provided for @servers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'servers'**
|
||||
String get servers;
|
||||
|
||||
/// No description provided for @sftpDlPrepare.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1202,6 +1424,120 @@ abstract class AppLocalizations {
|
||||
/// **'Spent time: {time}'**
|
||||
String spentTime(Object time);
|
||||
|
||||
/// No description provided for @sshConfigAllExist.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All servers already exist ({duplicateCount} duplicates found)'**
|
||||
String sshConfigAllExist(Object duplicateCount);
|
||||
|
||||
/// No description provided for @sshConfigDuplicatesSkipped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{duplicateCount} duplicates will be skipped'**
|
||||
String sshConfigDuplicatesSkipped(Object duplicateCount);
|
||||
|
||||
/// No description provided for @sshConfigFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'We found SSH configuration on your system.'**
|
||||
String get sshConfigFound;
|
||||
|
||||
/// No description provided for @sshConfigFoundServers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Found {totalCount} servers'**
|
||||
String sshConfigFoundServers(Object totalCount);
|
||||
|
||||
/// No description provided for @sshConfigImport.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SSH Config Import'**
|
||||
String get sshConfigImport;
|
||||
|
||||
/// No description provided for @sshConfigImportHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Only basic information can be imported, for example: IP/Port.'**
|
||||
String get sshConfigImportHelp;
|
||||
|
||||
/// No description provided for @sshConfigImportPermission.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Would you like to give permission to read ~/.ssh/config and automatically import server settings?'**
|
||||
String get sshConfigImportPermission;
|
||||
|
||||
/// No description provided for @sshConfigImportTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Prompt to read ~/.ssh/config on first server creation'**
|
||||
String get sshConfigImportTip;
|
||||
|
||||
/// No description provided for @sshConfigImported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Imported {count} servers from SSH config'**
|
||||
String sshConfigImported(Object count);
|
||||
|
||||
/// No description provided for @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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Would you like to select the SSH config file manually?'**
|
||||
String get sshConfigManualSelect;
|
||||
|
||||
/// No description provided for @sshConfigNoServers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No servers found in SSH config'**
|
||||
String get sshConfigNoServers;
|
||||
|
||||
/// No description provided for @sshConfigPermissionDenied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cannot access SSH config file due to macOS permissions.'**
|
||||
String get sshConfigPermissionDenied;
|
||||
|
||||
/// No description provided for @sshConfigServersToImport.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{importCount} servers will be imported'**
|
||||
String sshConfigServersToImport(Object importCount);
|
||||
|
||||
/// No description provided for @sshTermHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1280,12 +1616,6 @@ abstract class AppLocalizations {
|
||||
/// **'Switch to {val}'**
|
||||
String switchTo(Object val);
|
||||
|
||||
/// No description provided for @sync.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sync'**
|
||||
String get sync;
|
||||
|
||||
/// No description provided for @syncTip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1304,6 +1634,12 @@ abstract class AppLocalizations {
|
||||
/// **'Tags'**
|
||||
String get tag;
|
||||
|
||||
/// No description provided for @tapToStartDiscovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap the search button to discover SSH servers on your network'**
|
||||
String get tapToStartDiscovery;
|
||||
|
||||
/// No description provided for @temperature.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1364,6 +1700,12 @@ abstract class AppLocalizations {
|
||||
/// **'Total'**
|
||||
String get total;
|
||||
|
||||
/// No description provided for @totalAttempts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total'**
|
||||
String get totalAttempts;
|
||||
|
||||
/// No description provided for @traffic.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1412,12 +1754,6 @@ abstract class AppLocalizations {
|
||||
/// **'Server status update interval'**
|
||||
String get updateServerStatusInterval;
|
||||
|
||||
/// No description provided for @upload.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Upload'**
|
||||
String get upload;
|
||||
|
||||
/// No description provided for @upsideDown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1466,6 +1802,12 @@ abstract class AppLocalizations {
|
||||
/// **'View'**
|
||||
String get view;
|
||||
|
||||
/// No description provided for @viewDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'View Details'**
|
||||
String get viewDetails;
|
||||
|
||||
/// No description provided for @viewErr.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1543,6 +1885,54 @@ abstract class AppLocalizations {
|
||||
/// 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.'**
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user