mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 12:44:59 +01:00
Compare commits
128 Commits
v1.0.1184
...
lollipopki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28ac6145c4 | ||
|
|
6338c6ce6b | ||
|
|
9281a578e7 | ||
|
|
d5e1d89394 | ||
|
|
71e757fe13 | ||
|
|
a0a62acdbc | ||
|
|
9ac866644c | ||
|
|
e226fec03d | ||
|
|
7d47c9d673 | ||
|
|
e49b31ed25 | ||
|
|
87d7feb823 | ||
|
|
84a1bd5519 | ||
|
|
f47d1e7141 | ||
|
|
d14e97485f | ||
|
|
347d294f6e | ||
|
|
d5e22cbc65 | ||
|
|
7926a4d4a6 | ||
|
|
39a3e0800b | ||
|
|
cd3c094af0 | ||
|
|
8589b3b4d7 | ||
|
|
7693e30cbf | ||
|
|
874d28be12 | ||
|
|
06070c29b9 | ||
|
|
bb0ada12e6 | ||
|
|
9ceeaf7cc4 | ||
|
|
29a57ad742 | ||
|
|
2c495a44c3 | ||
|
|
cc300c141a | ||
|
|
26efb8e185 | ||
|
|
06ed38ff45 | ||
|
|
7c35abe30e | ||
|
|
78ef181d4a | ||
|
|
3f15caeaf2 | ||
|
|
6458e736fa | ||
|
|
99fda8b747 | ||
|
|
c5cbb12ac3 | ||
|
|
038f0d4d77 | ||
|
|
141519d952 | ||
|
|
75d1a59e77 | ||
|
|
ca4e65d7a5 | ||
|
|
ffda27d057 | ||
|
|
c548b4ef48 | ||
|
|
70040c5840 | ||
|
|
5272324be6 | ||
|
|
8cbb48ed67 | ||
|
|
03720fa322 | ||
|
|
0b51719070 | ||
|
|
a84231393d | ||
|
|
d6c2cafce7 | ||
|
|
729b76177e | ||
|
|
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 | ||
|
|
fcb3d7e2b3 | ||
|
|
f5634d6e88 | ||
|
|
5497ad83e0 | ||
|
|
4a7827f41a | ||
|
|
60671fe461 | ||
|
|
bc1b6e5a4a | ||
|
|
1d553eccd5 | ||
|
|
68734a9e52 | ||
|
|
ed8a1d18b9 | ||
|
|
e4a9875620 | ||
|
|
6f9aa2ece9 | ||
|
|
13e28675af | ||
|
|
8c0e0f89d5 | ||
|
|
9b01da5a23 | ||
|
|
584af5423a | ||
|
|
95f8e571c1 | ||
|
|
9c9648656d | ||
|
|
6880bcc192 | ||
|
|
3a615449e3 | ||
|
|
46a12bc844 | ||
|
|
8d597294a4 | ||
|
|
682a6e4f2d | ||
|
|
8c3302cf0d | ||
|
|
ec4bf3df24 | ||
|
|
263d4eabb4 | ||
|
|
c6439673b8 | ||
|
|
a35d21981b | ||
|
|
dbc873c0c0 | ||
|
|
e69808a2f6 | ||
|
|
55b3ba63ec | ||
|
|
006e66d825 | ||
|
|
c556c0f1b5 | ||
|
|
c42c701ffc | ||
|
|
e6db2db320 | ||
|
|
66ecb02d9e | ||
|
|
8e7de604ee |
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
|
||||
|
||||
20
.github/workflows/issue-translator.yml
vendored
20
.github/workflows/issue-translator.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 'issue-translator'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
# not require, default false.
|
||||
# Decide whether to modify the issue title.
|
||||
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
|
||||
IS_MODIFY_TITLE: false
|
||||
# not require.
|
||||
# Customize the translation robot prefix message.
|
||||
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
|
||||
172
.github/workflows/release.yml
vendored
172
.github/workflows/release.yml
vendored
@@ -9,18 +9,24 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
APP_NAME: ServerBox
|
||||
RELEASE_TAG: ${{ 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
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.32.1"
|
||||
flutter-version: "3.38.7"
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
@@ -32,51 +38,67 @@ jobs:
|
||||
- name: Build
|
||||
run: dart run fl_build -p android
|
||||
- name: Rename for fdroid
|
||||
shell: bash
|
||||
run: |
|
||||
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
|
||||
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
|
||||
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
|
||||
APK_DIR="build/app/outputs/flutter-apk"
|
||||
shopt -s nullglob
|
||||
|
||||
for arch in arm64 arm amd64; do
|
||||
matches=("$APK_DIR"/"${APP_NAME}"_*_"${arch}".apk)
|
||||
if [ ${#matches[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 APK for ${arch}, found ${#matches[@]}"
|
||||
echo "APK_DIR: $APK_DIR"
|
||||
ls -la "$APK_DIR" || true
|
||||
exit 1
|
||||
fi
|
||||
mv "${matches[0]}" "$APK_DIR/${APP_NAME}_${RELEASE_TAG}_${arch}.apk"
|
||||
done
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm64.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm.apk
|
||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
releaseLinux:
|
||||
name: Release linux
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
# Basic
|
||||
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev libvulkan-dev desktop-file-utils wget
|
||||
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev mesa-utils libvulkan-dev desktop-file-utils wget
|
||||
# App Specific
|
||||
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev
|
||||
# Packaging
|
||||
sudo wget https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage -O /bin/appimagetool
|
||||
sudo chmod +x /bin/appimagetool
|
||||
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev libsecret-1-dev
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build
|
||||
dart run flutter_distributor:main package --platform=linux --target=appimage
|
||||
- name: Rename artifacts
|
||||
dart run fl_build -p linux
|
||||
- name: Rename for release
|
||||
shell: bash
|
||||
run: |
|
||||
appimage_name=$(ls dist/*/*.AppImage)
|
||||
mv $appimage_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
|
||||
shopt -s nullglob
|
||||
matches=("${APP_NAME}"_*_amd64.AppImage)
|
||||
if [ ${#matches[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 AppImage, found ${#matches[@]}"
|
||||
ls -la || true
|
||||
exit 1
|
||||
fi
|
||||
mv "${matches[0]}" "${APP_NAME}_${RELEASE_TAG}_amd64.AppImage"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -85,37 +107,97 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
run: dart run fl_build -p windows
|
||||
- name: Rename for release
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
matches=("${APP_NAME}"_*_windows_amd64.zip)
|
||||
if [ ${#matches[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 zip, found ${#matches[@]}"
|
||||
ls -la || true
|
||||
exit 1
|
||||
fi
|
||||
mv "${matches[0]}" "${APP_NAME}_${RELEASE_TAG}_windows_amd64.zip"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_windows_amd64.zip
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_windows_amd64.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# releaseApple:
|
||||
# name: Release ios and macos
|
||||
# runs-on: macos-latest
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Install Flutter
|
||||
# uses: subosito/flutter-action@v2
|
||||
# with:
|
||||
# channel: 'stable'
|
||||
# flutter-version: '3.32.1'
|
||||
# - name: Build
|
||||
# run: dart run fl_build -p ios,mac
|
||||
# - 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 }}
|
||||
releaseIOS:
|
||||
name: Release iOS
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build -p ios -- --no-codesign
|
||||
shopt -s nullglob
|
||||
IPA_FILES=(build/ios/ipa/*.ipa)
|
||||
if [ ${#IPA_FILES[@]} -ne 1 ]; then
|
||||
echo "Error: expected 1 IPA, found ${#IPA_FILES[@]}"
|
||||
ls -la build/ios/ipa || true
|
||||
exit 1
|
||||
fi
|
||||
IPA_FILE="${IPA_FILES[0]}"
|
||||
echo "Found IPA: $IPA_FILE"
|
||||
cp "$IPA_FILE" "${APP_NAME}_${RELEASE_TAG}_ios.ipa"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_ios.ipa
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
releaseMacOS:
|
||||
name: Release macOS
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
- name: Build
|
||||
run: |
|
||||
dart run fl_build -p macos -- --no-codesign
|
||||
- name: Package
|
||||
run: |
|
||||
RELEASE_DIR="$GITHUB_WORKSPACE/build/macos/Build/Products/Release"
|
||||
APP_DIR="$RELEASE_DIR/$APP_NAME.app"
|
||||
OUT_ZIP="$GITHUB_WORKSPACE/${APP_NAME}_${RELEASE_TAG}_macos.zip"
|
||||
|
||||
if [ ! -d "$RELEASE_DIR" ]; then
|
||||
echo "Error: macOS release directory not found: $RELEASE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "Error: macOS app bundle not found: $APP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$RELEASE_DIR"
|
||||
zip -ry "$OUT_ZIP" "$APP_NAME.app"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_macos.zip
|
||||
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/>.
|
||||
21
README.md
21
README.md
@@ -5,12 +5,12 @@ 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>
|
||||
|
||||
<p align="center">
|
||||
A Flutter project which provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
|
||||
A Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers.
|
||||
<br>
|
||||
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
|
||||
</p>
|
||||
@@ -26,25 +26,26 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 📥 Install
|
||||
## 📥 Installation
|
||||
|
||||
| Platform | From |
|
||||
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|Platform| From|
|
||||
|--|--|
|
||||
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) |
|
||||
| Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |
|
||||
| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) |
|
||||
|
||||
Please only download pkgs from the source that **you trust**!
|
||||
|
||||
## 🔖 Feature
|
||||
## 🔖 Features
|
||||
|
||||
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
|
||||
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`...
|
||||
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
|
||||
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
|
||||
|
||||
## 🆘 Help
|
||||
|
||||
<div align="center">
|
||||
<a href="https://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-Group-pink"></a>
|
||||
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
|
||||
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
|
||||
</div>
|
||||
@@ -60,10 +61,12 @@ Before you open an issue, please read the following:
|
||||
|
||||
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
|
||||
|
||||
## 🧱 Contribution
|
||||
## 🧱 Contributions
|
||||
|
||||
Any positive contribution is welcome.
|
||||
|
||||
If I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.
|
||||
|
||||
### Development
|
||||
|
||||
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
|
||||
@@ -82,4 +85,4 @@ Any positive contribution is welcome.
|
||||
|
||||
## 📝 License
|
||||
|
||||
`GPL v3 lollipopkit`
|
||||
`AGPL v3 lollipopkit & all contributors`
|
||||
|
||||
29
README_zh.md
29
README_zh.md
@@ -5,18 +5,18 @@
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
|
||||
使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
|
||||
<br>
|
||||
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>。
|
||||
</p>
|
||||
|
||||
|
||||
## 🏙️ 截屏
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img width="200px" src="https://cdn.lpkt.cn/serverbox/screenshot/1.jpg"></td>
|
||||
@@ -26,20 +26,19 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## 📥 安装
|
||||
|
||||
平台 | 下载
|
||||
----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
平台|下载
|
||||
--|--
|
||||
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
|
||||
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
|
||||
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)
|
||||
|
||||
请从 **信任** 的来源下载!
|
||||
|
||||
|
||||
## 🔖 特点
|
||||
- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
|
||||
|
||||
- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理,`S.M.A.R.T`...
|
||||
- 特殊支持:`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`...
|
||||
- 本地化
|
||||
- English, 简体中文
|
||||
@@ -47,10 +46,10 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
||||
- Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);
|
||||
- 感谢贡献者们!
|
||||
|
||||
|
||||
## 🆘 帮助
|
||||
|
||||
<div align="center">
|
||||
<a href="https://qm.qq.com/q/daCGa7eShG"><img alt="qq" src="https://img.shields.io/badge/QQ-群-pink"></a>
|
||||
<a href="https://t.me/lpktg"><img alt="donate" src="https://img.shields.io/badge/Telegram-lpktg-green"></a>
|
||||
<a href="https://discord.gg/SsVNbRhK7w"><img alt="discord" src="https://img.shields.io/badge/Discord-lpkt-purple"></a>
|
||||
</div>
|
||||
@@ -59,26 +58,32 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
|
||||
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
|
||||
|
||||
反馈前须知:
|
||||
|
||||
1. 反馈问题请附带 log(点击首页右上角),并以 bug 模版提交。
|
||||
2. 反馈问题前请检查是否是 serverbox 的问题。
|
||||
3. 欢迎所有有效、正面的反馈,主观(比如你觉得其他UI更好看)的反馈不一定会接受
|
||||
|
||||
|
||||
## 🧱 贡献
|
||||
|
||||
任何正面的贡献都欢迎。
|
||||
|
||||
如果我忘记在贡献者列表中添加你的名字,请在你打开的 issue 或 PR 中添加评论让我知道,我会尽快添加。
|
||||
|
||||
### 开发
|
||||
|
||||
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
|
||||
2. 克隆这个仓库, 运行 `flutter run` 启动应用
|
||||
3. 运行 `dart run fl_build -p PLATFORM` 构建应用
|
||||
|
||||
### 翻译
|
||||
|
||||
[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
|
||||
|
||||
## 💡 我的其它 Apps
|
||||
|
||||
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。
|
||||
- [更多](https://github.com/lollipopkit) - 工具 & etc.
|
||||
|
||||
|
||||
## 📝 协议
|
||||
`GPL v3 lollipopkit`
|
||||
|
||||
`AGPL v3 lollipopkit & 所有贡献者`
|
||||
|
||||
@@ -92,6 +92,13 @@ android {
|
||||
// No applicationIdSuffix or resValue here
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
@@ -106,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hasFragileUserData="true"
|
||||
android:restoreAnyVersion="true"
|
||||
tools:targetApi="q">
|
||||
@@ -45,6 +46,15 @@
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<activity
|
||||
android:name=".widget.WidgetConfigureActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Material.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.HomeWidget"
|
||||
android:exported="false"
|
||||
|
||||
@@ -2,14 +2,32 @@ 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
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class ForegroundService : Service() {
|
||||
companion object {
|
||||
@Volatile
|
||||
var isRunning: Boolean = false
|
||||
}
|
||||
private val chanId = "ForegroundServiceChannel"
|
||||
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"
|
||||
|
||||
private var isFgStarted = false
|
||||
private val postedIds = mutableSetOf<Int>()
|
||||
// Stable mapping from session-id -> notification-id to avoid hash collisions
|
||||
private val notificationIdMap = mutableMapOf<String, Int>()
|
||||
private val nextNotificationId = java.util.concurrent.atomic.AtomicInteger(2001)
|
||||
|
||||
private fun logError(message: String, error: Throwable? = null) {
|
||||
Log.e("ForegroundService", message, error)
|
||||
@@ -26,48 +44,51 @@ class ForegroundService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d("ForegroundService", "Service onCreate")
|
||||
isRunning = true
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val action = intent.action
|
||||
Log.d("ForegroundService", "onStartCommand action=$action")
|
||||
|
||||
// Create notification before starting foreground
|
||||
val notification = createNotification()
|
||||
|
||||
// Use try-catch for startForeground
|
||||
try {
|
||||
startForeground(1, notification)
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to start foreground", e)
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
return when (action) {
|
||||
"ACTION_STOP_FOREGROUND" -> {
|
||||
ACTION_STOP_FOREGROUND -> {
|
||||
// Notify Flutter to stop all connections before stopping service
|
||||
val stopAllIntent = Intent("tech.lolli.toolbox.STOP_ALL_CONNECTIONS")
|
||||
sendBroadcast(stopAllIntent)
|
||||
clearAll()
|
||||
stopForegroundService()
|
||||
START_NOT_STICKY
|
||||
}
|
||||
ACTION_UPDATE_SESSIONS -> {
|
||||
val payload = intent.getStringExtra("payload") ?: "{}"
|
||||
handleUpdateSessions(payload)
|
||||
START_STICKY
|
||||
}
|
||||
else -> {
|
||||
// Default bring up foreground with placeholder
|
||||
ensureForeground(createMergedNotification(0, emptyList(), emptyList()))
|
||||
START_STICKY
|
||||
}
|
||||
}
|
||||
@@ -85,68 +106,205 @@ 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")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
val serviceChannel = NotificationChannel(
|
||||
chanId,
|
||||
"ForegroundServiceChannel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "For foreground service"
|
||||
}
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
try {
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val deleteIntent = Intent(this, ForegroundService::class.java).apply {
|
||||
action = "ACTION_STOP_FOREGROUND"
|
||||
}
|
||||
val deletePendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
deleteIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, chanId)
|
||||
if (!isFgStarted) {
|
||||
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 {
|
||||
Notification.Builder(this)
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
if (nm != null) {
|
||||
nm.notify(NOTIFICATION_ID, notification)
|
||||
} else {
|
||||
Log.w("ForegroundService", "NotificationManager is null, cannot update notification")
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
.setContentTitle("Server Box")
|
||||
.setContentText("Running in background")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
|
||||
.build()
|
||||
} catch (e: SecurityException) {
|
||||
logError("Security exception when starting foreground service (likely missing permission)", e)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
logError("Error creating notification", e)
|
||||
// Return a basic notification as fallback
|
||||
return Notification.Builder(this)
|
||||
.setContentTitle("Server Box")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.build()
|
||||
logError("Failed to start/update foreground", e)
|
||||
// Don't stop the service for other exceptions, just log them
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
val stopIntent = Intent(this, ForegroundService::class.java).apply { action = ACTION_STOP_FOREGROUND }
|
||||
val stopPending = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, chanId)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Notification.Builder(this)
|
||||
}
|
||||
|
||||
// Use the earliest session's start time for chronometer
|
||||
val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis()
|
||||
|
||||
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)
|
||||
.setWhen(earliestStartTime)
|
||||
.setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.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) {
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
if (nm == null) {
|
||||
logError("NotificationManager null")
|
||||
return
|
||||
}
|
||||
|
||||
val sessions = mutableListOf<SessionItem>()
|
||||
try {
|
||||
val obj = JSONObject(payload)
|
||||
val arr: JSONArray = obj.optJSONArray("sessions") ?: JSONArray()
|
||||
for (i in 0 until arr.length()) {
|
||||
val s = arr.optJSONObject(i) ?: continue
|
||||
val id = s.optString("id")
|
||||
val title = s.optString("title")
|
||||
val sub = s.optString("subtitle")
|
||||
val whenMs = s.optLong("startTimeMs", System.currentTimeMillis())
|
||||
val status = s.optString("status", "connected")
|
||||
if (id.isNotEmpty()) {
|
||||
sessions.add(SessionItem(id, title, sub, whenMs, status))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to parse payload", e)
|
||||
}
|
||||
|
||||
// Clear if empty
|
||||
if (sessions.isEmpty()) {
|
||||
clearAll()
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel any existing individual notifications (we only show merged notification now)
|
||||
val toCancel = postedIds.toSet()
|
||||
toCancel.forEach { nm.cancel(it) }
|
||||
postedIds.clear()
|
||||
notificationIdMap.clear()
|
||||
|
||||
// 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(NOTIFICATION_ID)
|
||||
postedIds.forEach { id -> nm?.cancel(id) }
|
||||
postedIds.clear()
|
||||
isFgStarted = false
|
||||
}
|
||||
|
||||
data class SessionItem(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val startWhen: Long,
|
||||
val status: String,
|
||||
)
|
||||
|
||||
private fun stopForegroundService() {
|
||||
try {
|
||||
stopForeground(true)
|
||||
if (isFgStarted) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
isFgStarted = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError("Error stopping foreground", e)
|
||||
}
|
||||
@@ -157,5 +315,6 @@ class ForegroundService : Service() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d("ForegroundService", "Service onDestroy")
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -13,20 +16,34 @@ import android.appwidget.AppWidgetManager
|
||||
import tech.lolli.toolbox.widget.HomeWidget
|
||||
|
||||
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)
|
||||
val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
MethodChannel(binaryMessenger, "tech.lolli.toolbox/main_chan").apply {
|
||||
setMethodCallHandler { method, result ->
|
||||
channel = MethodChannel(binaryMessenger, "tech.lolli.toolbox/main_chan")
|
||||
channel.setMethodCallHandler { method, result ->
|
||||
when (method.method) {
|
||||
"sendToBackground" -> {
|
||||
moveTaskToBack(true)
|
||||
result.success(null)
|
||||
}
|
||||
"isServiceRunning" -> {
|
||||
result.success(ForegroundService.isRunning)
|
||||
}
|
||||
"startService" -> {
|
||||
try {
|
||||
reqPerm()
|
||||
if (!notificationsAllowed()) {
|
||||
// Don't start foreground service without notification permission on API 33+
|
||||
result.error("NOTIFICATION_PERMISSION_DENIED", "Notification permission not granted", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
@@ -51,31 +68,138 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
sendBroadcast(intent)
|
||||
result.success(null)
|
||||
}
|
||||
"updateSessions" -> {
|
||||
try {
|
||||
if (!notificationsAllowed()) {
|
||||
// Avoid starting/continuing service updates when notifications are blocked
|
||||
result.error("NOTIFICATION_PERMISSION_DENIED", "Notification permission not granted", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
|
||||
serviceIntent.action = ACTION_UPDATE_SESSIONS
|
||||
serviceIntent.putExtra("payload", method.arguments as String)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to update sessions: ${e.message}")
|
||||
result.error("SERVICE_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun notificationsAllowed(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
true
|
||||
} else {
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleActionIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleActionIntent(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
when (intent.action) {
|
||||
ACTION_DISCONNECT_SESSION -> {
|
||||
val sessionId = intent.getStringExtra("session_id")
|
||||
if (sessionId != null && ::channel.isInitialized) {
|
||||
try {
|
||||
channel.invokeMethod("disconnectSession", mapOf("id" to sessionId))
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to invoke disconnect: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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}")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,24 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.json.JSONObject
|
||||
import org.json.JSONException
|
||||
import tech.lolli.toolbox.R
|
||||
import java.net.URL
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.SocketTimeoutException
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class HomeWidget : AppWidgetProvider() {
|
||||
companion object {
|
||||
private const val TAG = "HomeWidget"
|
||||
private const val NETWORK_TIMEOUT = 10_000L // 10 seconds
|
||||
private const val COROUTINE_TIMEOUT = 15_000L // 15 seconds
|
||||
private val activeUpdates = ConcurrentHashMap<Int, Boolean>()
|
||||
}
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||
@@ -27,105 +38,184 @@ class HomeWidget : AppWidgetProvider() {
|
||||
}
|
||||
|
||||
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
|
||||
val views = RemoteViews(context.packageName, R.layout.home_widget)
|
||||
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
var url = sp.getString("widget_$appWidgetId", null)
|
||||
if (url.isNullOrEmpty()) {
|
||||
url = sp.getString("$appWidgetId", null)
|
||||
}
|
||||
if (url.isNullOrEmpty()) {
|
||||
val gUrl = sp.getString("widget_*", null)
|
||||
url = gUrl
|
||||
}
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
Log.e("HomeWidget", "URL not found")
|
||||
}
|
||||
|
||||
val intentUpdate = Intent(context, HomeWidget::class.java)
|
||||
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
val ids = intArrayOf(appWidgetId)
|
||||
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
|
||||
var flag = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
|
||||
flag = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
val pendingUpdate: PendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
appWidgetId,
|
||||
intentUpdate,
|
||||
flag)
|
||||
views.setOnClickPendingIntent(R.id.widget_container, pendingUpdate)
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
views.setTextViewText(R.id.widget_name, "No URL")
|
||||
// Update the widget to display a message for missing URL
|
||||
views.setViewVisibility(R.id.error_message, View.VISIBLE)
|
||||
views.setTextViewText(R.id.error_message, "Please configure the widget URL.")
|
||||
views.setViewVisibility(R.id.widget_content, View.GONE)
|
||||
views.setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||
views.setFloat(R.id.error_message, "setAlpha", 1f)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
// Prevent concurrent updates for the same widget
|
||||
if (activeUpdates.putIfAbsent(appWidgetId, true) == true) {
|
||||
Log.d(TAG, "Widget $appWidgetId is already updating, skipping")
|
||||
return
|
||||
} else {
|
||||
views.setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
|
||||
views.setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
|
||||
views.setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
|
||||
views.setViewVisibility(R.id.widget_net_label, View.VISIBLE)
|
||||
}
|
||||
|
||||
val views = RemoteViews(context.packageName, R.layout.home_widget)
|
||||
val url = getWidgetUrl(context, appWidgetId)
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
Log.w(TAG, "URL not found for widget $appWidgetId")
|
||||
showErrorState(views, appWidgetManager, appWidgetId, "Please configure the widget URL.")
|
||||
activeUpdates.remove(appWidgetId)
|
||||
return
|
||||
}
|
||||
|
||||
setupClickIntent(context, views, appWidgetId)
|
||||
|
||||
showLoadingState(views, appWidgetManager, appWidgetId)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
|
||||
val jsonObject = JSONObject(jsonStr)
|
||||
val data = jsonObject.getJSONObject("data")
|
||||
val server = data.getString("name")
|
||||
val cpu = data.getString("cpu")
|
||||
val mem = data.getString("mem")
|
||||
val disk = data.getString("disk")
|
||||
val net = data.getString("net")
|
||||
withContext(Dispatchers.Main) {
|
||||
if (mem.isEmpty() || disk.isEmpty()) {
|
||||
Log.e("HomeWidget", "Failed to retrieve status: Memory or disk information is empty")
|
||||
return@withContext
|
||||
withTimeoutOrNull(COROUTINE_TIMEOUT) {
|
||||
try {
|
||||
val serverData = fetchServerData(url)
|
||||
if (serverData != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showSuccessState(views, appWidgetManager, appWidgetId, serverData)
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
showErrorState(views, appWidgetManager, appWidgetId, "Invalid server data received.")
|
||||
}
|
||||
views.setTextViewText(R.id.widget_name, server)
|
||||
views.setTextViewText(R.id.widget_cpu, cpu)
|
||||
views.setTextViewText(R.id.widget_mem, mem)
|
||||
views.setTextViewText(R.id.widget_disk, disk)
|
||||
views.setTextViewText(R.id.widget_net, net)
|
||||
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
|
||||
views.setTextViewText(R.id.widget_time, timeStr)
|
||||
views.setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||
views.setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
|
||||
views.setFloat(R.id.widget_mem_label, "setAlpha", 1f)
|
||||
views.setFloat(R.id.widget_disk_label, "setAlpha", 1f)
|
||||
views.setFloat(R.id.widget_net_label, "setAlpha", 1f)
|
||||
views.setFloat(R.id.widget_time, "setAlpha", 1f)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
} else {
|
||||
throw FileNotFoundException("HTTP response code: $responseCode")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating widget $appWidgetId: ${e.message}", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
val errorMessage = when (e) {
|
||||
is SocketTimeoutException -> "Connection timeout. Please check your network."
|
||||
is IOException -> "Network error. Please check your connection."
|
||||
is JSONException -> "Invalid data format received from server."
|
||||
else -> "Failed to retrieve data: ${e.message}"
|
||||
}
|
||||
showErrorState(views, appWidgetManager, appWidgetId, errorMessage)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("HomeWidget", "Error updating widget: ${e.localizedMessage}", e)
|
||||
} ?: run {
|
||||
Log.w(TAG, "Widget update timed out for widget $appWidgetId")
|
||||
withContext(Dispatchers.Main) {
|
||||
views.setTextViewText(R.id.widget_name, "Error")
|
||||
// Update the widget to display a message for data retrieval failure
|
||||
views.setViewVisibility(R.id.error_message, View.VISIBLE)
|
||||
views.setTextViewText(R.id.error_message, "Failed to retrieve data.")
|
||||
views.setViewVisibility(R.id.widget_content, View.GONE)
|
||||
views.setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||
views.setFloat(R.id.error_message, "setAlpha", 1f)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
showErrorState(views, appWidgetManager, appWidgetId, "Update timed out. Please try again.")
|
||||
}
|
||||
}
|
||||
activeUpdates.remove(appWidgetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWidgetUrl(context: Context, appWidgetId: Int): String? {
|
||||
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
return sp.getString("widget_$appWidgetId", null)
|
||||
?: sp.getString("$appWidgetId", null)
|
||||
?: sp.getString("widget_*", null)
|
||||
}
|
||||
|
||||
private fun setupClickIntent(context: Context, views: RemoteViews, appWidgetId: Int) {
|
||||
val intentConfigure = Intent(context, WidgetConfigureActivity::class.java).apply {
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
}
|
||||
|
||||
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
|
||||
val pendingConfigure = PendingIntent.getActivity(context, appWidgetId, intentConfigure, flag)
|
||||
views.setOnClickPendingIntent(R.id.widget_container, pendingConfigure)
|
||||
}
|
||||
|
||||
private suspend fun fetchServerData(url: String): ServerData? = withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
try {
|
||||
connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = NETWORK_TIMEOUT.toInt()
|
||||
readTimeout = NETWORK_TIMEOUT.toInt()
|
||||
setRequestProperty("User-Agent", "ServerBox-Widget/1.0")
|
||||
setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw IOException("HTTP ${connection.responseCode}: ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
|
||||
parseServerData(jsonStr)
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseServerData(jsonStr: String): ServerData? {
|
||||
return try {
|
||||
val jsonObject = JSONObject(jsonStr)
|
||||
val data = jsonObject.getJSONObject("data")
|
||||
|
||||
val server = data.optString("name", "Unknown Server")
|
||||
val cpu = data.optString("cpu", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||
val mem = data.optString("mem", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||
val disk = data.optString("disk", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||
val net = data.optString("net", "").takeIf { it.isNotBlank() } ?: "N/A"
|
||||
|
||||
// Return data even if some fields are missing, providing defaults
|
||||
// Only reject if we can't parse the JSON structure properly
|
||||
ServerData(server, cpu, mem, disk, net)
|
||||
} catch (e: JSONException) {
|
||||
Log.e(TAG, "JSON parsing error: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoadingState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
|
||||
views.apply {
|
||||
setTextViewText(R.id.widget_name, "Loading...")
|
||||
setViewVisibility(R.id.error_message, View.GONE)
|
||||
setViewVisibility(R.id.widget_content, View.VISIBLE)
|
||||
setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
|
||||
setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
|
||||
setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
|
||||
setViewVisibility(R.id.widget_net_label, View.VISIBLE)
|
||||
setViewVisibility(R.id.widget_progress, View.VISIBLE)
|
||||
setFloat(R.id.widget_name, "setAlpha", 0.7f)
|
||||
}
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
|
||||
private fun showSuccessState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, data: ServerData) {
|
||||
views.apply {
|
||||
setTextViewText(R.id.widget_name, data.name)
|
||||
setTextViewText(R.id.widget_cpu, data.cpu)
|
||||
setTextViewText(R.id.widget_mem, data.mem)
|
||||
setTextViewText(R.id.widget_disk, data.disk)
|
||||
setTextViewText(R.id.widget_net, data.net)
|
||||
|
||||
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
|
||||
setTextViewText(R.id.widget_time, timeStr)
|
||||
|
||||
setViewVisibility(R.id.error_message, View.GONE)
|
||||
setViewVisibility(R.id.widget_content, View.VISIBLE)
|
||||
setViewVisibility(R.id.widget_progress, View.GONE)
|
||||
|
||||
// Smooth fade-in animation
|
||||
setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||
setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
|
||||
setFloat(R.id.widget_mem_label, "setAlpha", 1f)
|
||||
setFloat(R.id.widget_disk_label, "setAlpha", 1f)
|
||||
setFloat(R.id.widget_net_label, "setAlpha", 1f)
|
||||
setFloat(R.id.widget_time, "setAlpha", 1f)
|
||||
}
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
|
||||
private fun showErrorState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMessage: String) {
|
||||
views.apply {
|
||||
setTextViewText(R.id.widget_name, "Error")
|
||||
setViewVisibility(R.id.error_message, View.VISIBLE)
|
||||
setTextViewText(R.id.error_message, errorMessage)
|
||||
setViewVisibility(R.id.widget_content, View.GONE)
|
||||
setViewVisibility(R.id.widget_progress, View.GONE)
|
||||
setFloat(R.id.widget_name, "setAlpha", 1f)
|
||||
setFloat(R.id.error_message, "setAlpha", 1f)
|
||||
}
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
|
||||
data class ServerData(
|
||||
val name: String,
|
||||
val cpu: String,
|
||||
val mem: String,
|
||||
val disk: String,
|
||||
val net: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package tech.lolli.toolbox.widget
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import tech.lolli.toolbox.R
|
||||
|
||||
class WidgetConfigureActivity : Activity() {
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
private lateinit var urlEditText: EditText
|
||||
private lateinit var saveButton: Button
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.widget_configure)
|
||||
|
||||
// 设置结果为取消,以防用户在完成配置前退出
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
// 获取 widget ID
|
||||
val extras = intent.extras
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
)
|
||||
}
|
||||
|
||||
// 如果没有有效的 widget ID,完成 activity
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化 UI 元素
|
||||
urlEditText = findViewById(R.id.url_edit_text)
|
||||
saveButton = findViewById(R.id.save_button)
|
||||
|
||||
// 从 SharedPreferences 加载现有配置
|
||||
val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
|
||||
val existingUrl = sp.getString("widget_$appWidgetId", "")
|
||||
urlEditText.setText(existingUrl)
|
||||
|
||||
// 设置保存按钮点击事件
|
||||
saveButton.setOnClickListener {
|
||||
val url = urlEditText.text.toString().trim()
|
||||
if (url.isEmpty()) {
|
||||
urlEditText.error = "Please enter a URL"
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
// 验证 URL 格式
|
||||
if (!Patterns.WEB_URL.matcher(url).matches()) {
|
||||
urlEditText.error = "Please enter a valid URL"
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
// 保存 URL 到 SharedPreferences
|
||||
val editor = sp.edit()
|
||||
editor.putString("widget_$appWidgetId", url)
|
||||
editor.apply()
|
||||
|
||||
// 更新 widget 使用 AppWidgetManager
|
||||
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||
val updateIntent = Intent(this, HomeWidget::class.java).apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
|
||||
}
|
||||
sendBroadcast(updateIntent)
|
||||
|
||||
// 设置结果并结束 activity
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,17 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/widgetText"
|
||||
android:textSize="23sp"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
tools:text="Server Name" />
|
||||
|
||||
<!-- Wrap the content in a LinearLayout for easy visibility management -->
|
||||
@@ -27,121 +30,138 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_below="@id/widget_name"
|
||||
android:paddingTop="13dp">
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/widget_container_inner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="13dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_cpu_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="2.7dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp"
|
||||
android:src="@drawable/speed_24">
|
||||
</ImageView>
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/speed_24"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="CPU usage" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_cpu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="11dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize = "marquee"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/widgetSummaryText"
|
||||
android:textSize="12.7sp"
|
||||
tools:text="CPU" />
|
||||
android:textSize="12sp"
|
||||
tools:text="CPU: 25.6%" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_mem_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="2.7dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_below="@id/widget_cpu_label"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp"
|
||||
android:src="@drawable/memory_24">
|
||||
</ImageView>
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/memory_24"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="Memory usage" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_mem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="11dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/widgetSummaryText"
|
||||
android:textSize="12.7sp"
|
||||
tools:text="Mem" />
|
||||
android:textSize="12sp"
|
||||
tools:text="Memory: 4.2GB / 8GB" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_disk_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="2.7dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_below="@id/widget_mem_label"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp"
|
||||
android:src="@drawable/storage_24">
|
||||
</ImageView>
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/storage_24"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="Disk usage" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_disk"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="11dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/widgetSummaryText"
|
||||
android:textSize="12.7sp"
|
||||
tools:text="Disk" />
|
||||
android:textSize="12sp"
|
||||
tools:text="Disk: 125GB / 250GB" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_net_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/widget_disk_label"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp"
|
||||
android:src="@drawable/net_24">
|
||||
</ImageView>
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/net_24"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="Network usage" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_net"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="11dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/widgetSummaryText"
|
||||
android:textSize="12.7sp"
|
||||
tools:text="Net" />
|
||||
android:textSize="12sp"
|
||||
tools:text="Network: 15MB/s ↓ 8MB/s ↑" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -149,29 +169,45 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Add a TextView for error messages -->
|
||||
<!-- Error message display -->
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/widget_name"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/widgetSummaryText"
|
||||
android:textSize="12sp"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true"
|
||||
tools:text="Error message" />
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
tools:text="Error message text that might be longer than usual" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:maxLines="2"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/widgetSummaryText"
|
||||
android:textSize="11sp"
|
||||
android:textSize="10sp"
|
||||
android:alpha="0"
|
||||
android:animateLayoutChanges="true"
|
||||
tools:text="UpdateTime" />
|
||||
android:fontFamily="monospace"
|
||||
tools:text="12:34" />
|
||||
|
||||
<!-- Progress indicator for loading state -->
|
||||
<ProgressBar
|
||||
android:id="@+id/widget_progress"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
38
android/app/src/main/res/layout/widget_configure.xml
Normal file
38
android/app/src/main/res/layout/widget_configure.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Widget URL"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@android:color/black" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/url_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="https://server/status"
|
||||
android:inputType="textUri"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@android:drawable/edit_text"
|
||||
android:padding="12dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textColorHint="@android:color/darker_gray" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/save_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Save"
|
||||
android:background="#8b2252"
|
||||
android:textColor="@android:color/white"
|
||||
android:padding="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
|
||||
</full-backup-content>
|
||||
@@ -6,6 +6,7 @@
|
||||
android:minHeight="110dp"
|
||||
android:updatePeriodMillis="1800001"
|
||||
android:initialLayout="@layout/home_widget"
|
||||
android:configure="tech.lolli.toolbox.widget.WidgetConfigureActivity"
|
||||
android:resizeMode="none"
|
||||
android:widgetCategory="home_screen">
|
||||
</appwidget-provider>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
6505
coverage/lcov.info
Normal file
6505
coverage/lcov.info
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
variables:
|
||||
output: dist/
|
||||
releases:
|
||||
- name: linux
|
||||
jobs:
|
||||
- name: release-linux-deb
|
||||
package:
|
||||
platform: linux
|
||||
target: deb
|
||||
- name: release-linux-rpm
|
||||
package:
|
||||
platform: linux
|
||||
target: rpm
|
||||
21
docs/.gitignore
vendored
Normal file
21
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
4
docs/.vscode/extensions.json
vendored
Normal file
4
docs/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
docs/.vscode/launch.json
vendored
Normal file
11
docs/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
49
docs/README.md
Normal file
49
docs/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ └── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
131
docs/astro.config.mjs
Normal file
131
docs/astro.config.mjs
Normal file
@@ -0,0 +1,131 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'Server Box',
|
||||
description: 'A comprehensive cross-platform server management application built with Flutter',
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
zh: {
|
||||
label: '简体中文',
|
||||
lang: 'zh',
|
||||
},
|
||||
de: {
|
||||
label: 'Deutsch',
|
||||
lang: 'de',
|
||||
},
|
||||
fr: {
|
||||
label: 'Français',
|
||||
lang: 'fr',
|
||||
},
|
||||
es: {
|
||||
label: 'Español',
|
||||
lang: 'es',
|
||||
},
|
||||
ja: {
|
||||
label: '日本語',
|
||||
lang: 'ja',
|
||||
},
|
||||
},
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
},
|
||||
social: [
|
||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/lollipopkit/flutter_server_box' },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
translations: {
|
||||
zh: '开始使用',
|
||||
de: 'Erste Schritte',
|
||||
fr: 'Mise en route',
|
||||
es: 'Primeros pasos',
|
||||
ja: 'はじめに',
|
||||
},
|
||||
items: [
|
||||
{ label: 'Introduction', translations: { zh: '介绍', de: 'Einführung', fr: 'Introduction', es: 'Introducción', ja: 'はじめに' }, slug: 'introduction' },
|
||||
{ label: 'Installation', translations: { zh: '安装', de: 'Installation', fr: 'Installation', es: 'Instalación', ja: 'インストール' }, slug: 'installation' },
|
||||
{ label: 'Quick Start', translations: { zh: '快速开始', de: 'Schnellstart', fr: 'Démarrage rapide', es: 'Inicio rápido', ja: 'クイックスタート' }, slug: 'quick-start' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Platform Features',
|
||||
translations: {
|
||||
zh: '平台特性',
|
||||
de: 'Plattformfunktionen',
|
||||
fr: 'Fonctionnalités de la plateforme',
|
||||
es: 'Características de la plataforma',
|
||||
ja: 'プラットフォーム機能',
|
||||
},
|
||||
items: [
|
||||
{ label: 'Mobile', translations: { zh: '移动端', de: 'Mobil', fr: 'Mobile', es: 'Móvil', ja: 'モバイル' }, slug: 'platforms/mobile' },
|
||||
{ label: 'Desktop', translations: { zh: '桌面端', de: 'Desktop', fr: 'Bureau', es: 'Escritorio', ja: 'デスクトップ' }, slug: 'platforms/desktop' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
translations: {
|
||||
zh: '进阶',
|
||||
de: 'Fortgeschritten',
|
||||
fr: 'Avancé',
|
||||
es: 'Avanzado',
|
||||
ja: '高度な設定',
|
||||
},
|
||||
items: [
|
||||
{ label: 'Bulk Import Servers', translations: { zh: '批量导入服务器', de: 'Server-Massenimport', fr: 'Importation massive de serveurs', es: 'Importación masiva de servidores', ja: 'サーバーの一括インポート' }, slug: 'advanced/bulk-import' },
|
||||
{ label: 'Widget Setup', translations: { zh: '小组件设置', de: 'Widget-Einrichtung', fr: 'Configuration du widget', es: 'Configuración de widgets', ja: 'ウィジェット設定' }, slug: 'advanced/widgets' },
|
||||
{ label: 'Custom Commands', translations: { zh: '自定义命令', de: 'Benutzerdefinierte Befehle', fr: 'Commandes personnalisées', es: 'Comandos personalizados', ja: 'カスタムコマンド' }, slug: 'advanced/custom-commands' },
|
||||
{ label: 'Custom Logo', translations: { zh: '自定义 Logo', de: 'Benutzerdefiniertes Logo', fr: 'Logo personnalisé', es: 'Logo personalizado', ja: 'カスタムロゴ' }, slug: 'advanced/custom-logo' },
|
||||
{ label: 'JSON Settings', translations: { zh: 'JSON 设置', de: 'JSON-Einstellungen', fr: 'Paramètres JSON', es: 'Ajustes JSON', ja: 'JSON 設定' }, slug: 'advanced/json-settings' },
|
||||
{ label: 'Common Issues', translations: { zh: '常见问题', de: 'Häufige Probleme', fr: 'Problèmes courants', es: 'Problemas comunes', ja: 'よくある質問' }, slug: 'advanced/troubleshooting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'How It Works',
|
||||
translations: {
|
||||
zh: '工作原理',
|
||||
de: 'Wie es funktioniert',
|
||||
fr: 'Comment ça marche',
|
||||
es: 'Cómo funciona',
|
||||
ja: '仕組み',
|
||||
},
|
||||
items: [
|
||||
{ label: 'Architecture', translations: { zh: '架构', de: 'Architektur', fr: 'Architecture', es: 'Arquitectura', ja: 'アーキテクチャ' }, slug: 'principles/architecture' },
|
||||
{ label: 'SSH Connection', translations: { zh: 'SSH 连接', de: 'SSH-Verbindung', fr: 'Connexion SSH', es: 'Conexión SSH', ja: 'SSH 接続' }, slug: 'principles/ssh' },
|
||||
{ label: 'Terminal', translations: { zh: '终端', de: 'Terminal', fr: 'Terminal', es: 'Terminal', ja: 'ターミナル' }, slug: 'principles/terminal' },
|
||||
{ label: 'SFTP', translations: { zh: 'SFTP', de: 'SFTP', fr: 'SFTP', es: 'SFTP', ja: 'SFTP' }, slug: 'principles/sftp' },
|
||||
{ label: 'State Management', translations: { zh: '状态管理', de: 'Zustandsverwaltung', fr: 'Gestion d\'état', es: 'Gestión de estado', ja: '状態管理' }, slug: 'principles/state' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Development',
|
||||
translations: {
|
||||
zh: '开发',
|
||||
de: 'Entwicklung',
|
||||
fr: 'Développement',
|
||||
es: 'Desarrollo',
|
||||
ja: '開発',
|
||||
},
|
||||
items: [
|
||||
{ label: 'Project Structure', translations: { zh: '项目结构', de: 'Projektstruktur', fr: 'Structure du projet', es: 'Estructura del proyecto', ja: 'プロジェクト構造' }, slug: 'development/structure' },
|
||||
{ label: 'Architecture', translations: { zh: '架构', de: 'Architektur', fr: 'Architecture', es: 'Arquitectura', ja: 'アーキテクチャ' }, slug: 'development/architecture' },
|
||||
{ label: 'State Management', translations: { zh: '状态管理', de: 'Zustandsverwaltung', fr: 'Gestion d\'état', es: 'Gestión de estado', ja: '状態管理' }, slug: 'development/state' },
|
||||
{ label: 'Code Generation', translations: { zh: '代码生成', de: 'Code-Generierung', fr: 'Génération de code', es: 'Generación de código', ja: 'コード生成' }, slug: 'development/codegen' },
|
||||
{ label: 'Building', translations: { zh: '构建', de: 'Bauen', fr: 'Construction', es: 'Construcción', ja: 'ビルド' }, slug: 'development/building' },
|
||||
{ label: 'Testing', translations: { zh: '测试', de: 'Testen', fr: 'Tests', es: 'Pruebas', ja: 'テスト' }, slug: 'development/testing' },
|
||||
],
|
||||
},
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
900
docs/bun.lock
Normal file
900
docs/bun.lock
Normal file
@@ -0,0 +1,900 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "docs",
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.4",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
||||
|
||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="],
|
||||
|
||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="],
|
||||
|
||||
"@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="],
|
||||
|
||||
"@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||
|
||||
"@astrojs/sitemap": ["@astrojs/sitemap@3.7.0", "", { "dependencies": { "sitemap": "^8.0.2", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA=="],
|
||||
|
||||
"@astrojs/starlight": ["@astrojs/starlight@0.37.4", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "magic-string": "^0.30.17", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-ygPGDgRd9nCcNgaYMNN7UeAMAkDOR1ibv3ps3xEz+cuvKG3CRLd19UwdB+Gyz1tbkyfjPWPkFKNhLwNybro8Tw=="],
|
||||
|
||||
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
|
||||
|
||||
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
|
||||
|
||||
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@expressive-code/core": ["@expressive-code/core@0.41.6", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-FvJQP+hG0jWi/FLBSmvHInDqWR7jNANp9PUDjdMqSshHb0y7sxx3vHuoOr6SgXjWw+MGLqorZyPQ0aAlHEok6g=="],
|
||||
|
||||
"@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-d+hkSYXIQot6fmYnOmWAM+7TNWRv/dhfjMsNq+mIZz8Tb4mPHOcgcfZeEM5dV9TDL0ioQNvtcqQNuzA1sRPjxg=="],
|
||||
|
||||
"@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6", "shiki": "^3.2.2" } }, "sha512-Y6zmKBmsIUtWTzdefqlzm/h9Zz0Rc4gNdt2GTIH7fhHH2I9+lDYCa27BDwuBhjqcos6uK81Aca9dLUC4wzN+ng=="],
|
||||
|
||||
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ=="],
|
||||
|
||||
"@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A=="],
|
||||
|
||||
"@pagefind/default-ui": ["@pagefind/default-ui@1.4.0", "", {}, "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ=="],
|
||||
|
||||
"@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.4.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q=="],
|
||||
|
||||
"@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw=="],
|
||||
|
||||
"@pagefind/linux-x64": ["@pagefind/linux-x64@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg=="],
|
||||
|
||||
"@pagefind/windows-x64": ["@pagefind/windows-x64@1.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
|
||||
|
||||
"@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
|
||||
|
||||
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
|
||||
"astro": ["astro@5.16.16", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", "@astrojs/markdown-remark": "6.3.10", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-MFlFvQ84ixaHyqB3uGwMhNHdBLZ3vHawyq3PqzQS2TNWiNfQrxp5ag6S3lX+Cvnh0MUcXX+UnJBPMBHjP1/1ZQ=="],
|
||||
|
||||
"astro-expressive-code": ["astro-expressive-code@0.41.6", "", { "dependencies": { "rehype-expressive-code": "^0.41.6" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-l47tb1uhmVIebHUkw+HEPtU/av0G4O8Q34g2cbkPvC7/e9ZhANcjUUciKt9Hp6gSVDdIuXBBLwJQn2LkeGMOAw=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="],
|
||||
|
||||
"bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="],
|
||||
|
||||
"bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
|
||||
|
||||
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="],
|
||||
|
||||
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
|
||||
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
|
||||
|
||||
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
|
||||
|
||||
"estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
|
||||
|
||||
"estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"expressive-code": ["expressive-code@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6", "@expressive-code/plugin-frames": "^0.41.6", "@expressive-code/plugin-shiki": "^0.41.6", "@expressive-code/plugin-text-markers": "^0.41.6" } }, "sha512-W/5+IQbrpCIM5KGLjO35wlp1NCwDOOVQb+PAvzEoGkW1xjGM807ZGfBKptNWH6UECvt6qgmLyWolCMYKh7eQmA=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||
|
||||
"fontace": ["fontace@0.4.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg=="],
|
||||
|
||||
"fontkitten": ["fontkitten@1.0.2", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
|
||||
|
||||
"hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="],
|
||||
|
||||
"hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="],
|
||||
|
||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="],
|
||||
|
||||
"hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="],
|
||||
|
||||
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
||||
|
||||
"hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="],
|
||||
|
||||
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||
|
||||
"hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="],
|
||||
|
||||
"hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||
|
||||
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
|
||||
|
||||
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="],
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
|
||||
|
||||
"mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
|
||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||
|
||||
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
|
||||
|
||||
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
|
||||
|
||||
"micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
|
||||
|
||||
"micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
|
||||
|
||||
"micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||
|
||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
||||
|
||||
"p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
|
||||
|
||||
"p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
|
||||
|
||||
"p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||
|
||||
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
||||
|
||||
"recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
|
||||
|
||||
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
|
||||
|
||||
"rehype-expressive-code": ["rehype-expressive-code@0.41.6", "", { "dependencies": { "expressive-code": "^0.41.6" } }, "sha512-aBMX8kxPtjmDSFUdZlAWJkMvsQ4ZMASfee90JWIAV8tweltXLzkWC3q++43ToTelI8ac5iC0B3/S/Cl4Ql1y2g=="],
|
||||
|
||||
"rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="],
|
||||
|
||||
"rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
|
||||
|
||||
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||
|
||||
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
|
||||
|
||||
"rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
|
||||
|
||||
"remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
|
||||
|
||||
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
|
||||
|
||||
"retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
|
||||
|
||||
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
|
||||
|
||||
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
|
||||
|
||||
"sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"sitemap": ["sitemap@8.0.2", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ=="],
|
||||
|
||||
"smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unifont": ["unifont@0.7.3", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA=="],
|
||||
|
||||
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
|
||||
|
||||
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
"yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
17
docs/package.json
Normal file
17
docs/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.4",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2"
|
||||
}
|
||||
}
|
||||
1
docs/public/favicon.svg
Normal file
1
docs/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||
|
After Width: | Height: | Size: 696 B |
BIN
docs/src/assets/houston.webp
Normal file
BIN
docs/src/assets/houston.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
30
docs/src/assets/logo.svg
Normal file
30
docs/src/assets/logo.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<!-- Background Circle -->
|
||||
<circle cx="32" cy="32" r="30" fill="#02569B"/>
|
||||
|
||||
<!-- Terminal Window -->
|
||||
<rect x="14" y="16" width="36" height="32" rx="3" fill="#fff" opacity="0.9"/>
|
||||
|
||||
<!-- Terminal Header -->
|
||||
<rect x="14" y="16" width="36" height="8" rx="3" fill="#02569B" opacity="0.3"/>
|
||||
<circle cx="19" cy="20" r="1.5" fill="#ff5f57"/>
|
||||
<circle cx="24" cy="20" r="1.5" fill="#ffbd2e"/>
|
||||
<circle cx="29" cy="20" r="1.5" fill="#28c940"/>
|
||||
|
||||
<!-- Terminal Prompt -->
|
||||
<text x="18" y="34" font-family="monospace" font-size="6" fill="#333">></text>
|
||||
|
||||
<!-- Terminal Cursor -->
|
||||
<rect x="23" y="30" width="6" height="6" fill="#02569B" opacity="0.5">
|
||||
<animate attributeName="opacity" values="0.5;1;0.5" dur="1s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
|
||||
<!-- Server Icon -->
|
||||
<g transform="translate(38, 24)">
|
||||
<rect x="0" y="0" width="8" height="12" rx="1" fill="#02569B"/>
|
||||
<circle cx="2" cy="3" r="0.8" fill="#28c940"/>
|
||||
<circle cx="4" cy="3" r="0.8" fill="#28c940"/>
|
||||
<circle cx="6" cy="3" r="0.8" fill="#28c940"/>
|
||||
<line x1="1" y1="8" x2="7" y2="8" stroke="#fff" stroke-width="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
7
docs/src/content.config.ts
Normal file
7
docs/src/content.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
83
docs/src/content/docs/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/advanced/bulk-import.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Bulk Import Servers
|
||||
description: Import multiple servers from JSON file
|
||||
---
|
||||
|
||||
Import multiple server configurations at once using a JSON file.
|
||||
|
||||
## JSON Format
|
||||
|
||||
:::danger[Security Warning]
|
||||
**Never store plaintext passwords in files!** This JSON example shows a password field for demonstration only, but you should:
|
||||
|
||||
- **Prefer SSH keys** (`keyId`) instead of `pwd` - they're more secure
|
||||
- **Use secret managers** or environment variables if you must use passwords
|
||||
- **Delete the file immediately** after import - don't leave credentials lying around
|
||||
- **Add to .gitignore** - never commit credential files to version control
|
||||
:::
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "My Server",
|
||||
"ip": "example.com",
|
||||
"port": 22,
|
||||
"user": "root",
|
||||
"pwd": "password",
|
||||
"keyId": "",
|
||||
"tags": ["production"],
|
||||
"autoConnect": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | Display name |
|
||||
| `ip` | Yes | Domain or IP address |
|
||||
| `port` | Yes | SSH port (usually 22) |
|
||||
| `user` | Yes | SSH username |
|
||||
| `pwd` | No | Password (avoid - use SSH keys instead) |
|
||||
| `keyId` | No | SSH key name (from Private Keys - recommended) |
|
||||
| `tags` | No | Organization tags |
|
||||
| `autoConnect` | No | Auto-connect on startup |
|
||||
|
||||
## Import Steps
|
||||
|
||||
1. Create JSON file with server configurations
|
||||
2. Settings → Backup → Bulk Import Servers
|
||||
3. Select your JSON file
|
||||
4. Confirm import
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Production",
|
||||
"ip": "prod.example.com",
|
||||
"port": 22,
|
||||
"user": "admin",
|
||||
"keyId": "my-key",
|
||||
"tags": ["production", "web"]
|
||||
},
|
||||
{
|
||||
"name": "Development",
|
||||
"ip": "dev.example.com",
|
||||
"port": 2222,
|
||||
"user": "dev",
|
||||
"keyId": "dev-key",
|
||||
"tags": ["development"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Use SSH keys** instead of passwords when possible
|
||||
- **Test connection** after import
|
||||
- **Organize with tags** for easier management
|
||||
- **Delete JSON file** after import
|
||||
- **Never commit** JSON files with credentials to version control
|
||||
72
docs/src/content/docs/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/advanced/custom-commands.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Custom Commands
|
||||
description: Display custom command output on server page
|
||||
---
|
||||
|
||||
Add custom shell commands to show their output on the server detail page.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Server settings → Custom Commands
|
||||
2. Enter commands in JSON format
|
||||
|
||||
## Basic Format
|
||||
|
||||
```json
|
||||
{
|
||||
"Display Name": "shell command"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"Memory": "free -h",
|
||||
"Disk": "df -h",
|
||||
"Uptime": "uptime"
|
||||
}
|
||||
```
|
||||
|
||||
## Viewing Results
|
||||
|
||||
After setup, custom commands appear on server detail page and refresh automatically.
|
||||
|
||||
## Special Command Names
|
||||
|
||||
### server_card_top_right
|
||||
|
||||
Display on home page server card (top-right corner):
|
||||
|
||||
```json
|
||||
{
|
||||
"server_card_top_right": "your-command-here"
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
**Use absolute paths:**
|
||||
```json
|
||||
{"My Script": "/usr/local/bin/my-script.sh"}
|
||||
```
|
||||
|
||||
**Pipe commands:**
|
||||
```json
|
||||
{"Top Process": "ps aux | sort -rk 3 | head -5"}
|
||||
```
|
||||
|
||||
**Format output:**
|
||||
```json
|
||||
{"CPU Load": "uptime | awk -F'load average:' '{print $2}'"}
|
||||
```
|
||||
|
||||
**Keep commands fast:** Under 5 seconds for best experience
|
||||
|
||||
**Limit output:**
|
||||
```json
|
||||
{"Logs": "tail -20 /var/log/syslog"}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
Commands run with SSH user permissions. Avoid commands that modify system state.
|
||||
54
docs/src/content/docs/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/advanced/custom-logo.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Custom Server Logo
|
||||
description: Use custom images for server cards
|
||||
---
|
||||
|
||||
Display custom logos on server cards using image URLs.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Server settings → Custom Logo
|
||||
2. Enter image URL
|
||||
|
||||
## URL Placeholders
|
||||
|
||||
### {DIST} - Linux Distribution
|
||||
|
||||
Auto-replaced with detected distribution:
|
||||
|
||||
```
|
||||
https://example.com/{DIST}.png
|
||||
```
|
||||
|
||||
Becomes: `debian.png`, `ubuntu.png`, `arch.png`, etc.
|
||||
|
||||
### {BRIGHT} - Theme
|
||||
|
||||
Auto-replaced with current theme:
|
||||
|
||||
```
|
||||
https://example.com/{BRIGHT}.png
|
||||
```
|
||||
|
||||
Becomes: `light.png` or `dark.png`
|
||||
|
||||
### Combine Both
|
||||
|
||||
```
|
||||
https://example.com/{DIST}-{BRIGHT}.png
|
||||
```
|
||||
|
||||
Becomes: `debian-light.png`, `ubuntu-dark.png`, etc.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use PNG or SVG formats
|
||||
- Recommended size: 64x64 to 128x128 pixels
|
||||
- Use HTTPS URLs
|
||||
- Keep file sizes small
|
||||
|
||||
## Supported Distributions
|
||||
|
||||
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
|
||||
|
||||
Full list: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)
|
||||
65
docs/src/content/docs/advanced/json-settings.md
Normal file
65
docs/src/content/docs/advanced/json-settings.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Hidden Settings (JSON)
|
||||
description: Access advanced settings via JSON editor
|
||||
---
|
||||
|
||||
Some settings are hidden from the UI but accessible via JSON editor.
|
||||
|
||||
## Access
|
||||
|
||||
Long-press **Settings** in drawer to open JSON editor.
|
||||
|
||||
## Common Hidden Settings
|
||||
|
||||
### timeOut
|
||||
|
||||
Connection timeout in seconds.
|
||||
|
||||
```json
|
||||
{"timeOut": 10}
|
||||
```
|
||||
|
||||
**Type:** integer | **Default:** 5 | **Range:** 1-60
|
||||
|
||||
### recordHistory
|
||||
|
||||
Save history (SFTP paths, etc.).
|
||||
|
||||
```json
|
||||
{"recordHistory": true}
|
||||
```
|
||||
|
||||
**Type:** boolean | **Default:** true
|
||||
|
||||
### textFactor
|
||||
|
||||
Text scaling factor.
|
||||
|
||||
```json
|
||||
{"textFactor": 1.2}
|
||||
```
|
||||
|
||||
**Type:** double | **Default:** 1.0 | **Range:** 0.8-1.5
|
||||
|
||||
## Finding More Settings
|
||||
|
||||
All settings defined in [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).
|
||||
|
||||
Look for:
|
||||
```dart
|
||||
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
|
||||
```
|
||||
|
||||
## ⚠️ Important
|
||||
|
||||
**Before editing:**
|
||||
- **Create backup** - Wrong settings can cause app to not open
|
||||
- **Edit carefully** - JSON must be valid
|
||||
- **Change one at a time** - Test each setting
|
||||
|
||||
## Recovery
|
||||
|
||||
If app won't open after editing:
|
||||
1. Clear app data (last resort)
|
||||
2. Reinstall app
|
||||
3. Restore from backup
|
||||
118
docs/src/content/docs/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/advanced/troubleshooting.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Common Issues
|
||||
description: Solutions to common problems
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
|
||||
### SSH Won't Connect
|
||||
|
||||
**Symptoms:** Timeout, connection refused, auth failed
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Verify server type:** Only Unix-like systems supported (Linux, macOS, Android/Termux)
|
||||
2. **Test manually:** `ssh user@server -p port`
|
||||
3. **Check firewall:** Port 22 must be open
|
||||
4. **Verify credentials:** Username and password/key correct
|
||||
|
||||
### Frequent Disconnections
|
||||
|
||||
**Symptoms:** Terminal disconnects after inactivity
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Server keep-alive:**
|
||||
```bash
|
||||
# /etc/ssh/sshd_config
|
||||
ClientAliveInterval 60
|
||||
ClientAliveCountMax 3
|
||||
```
|
||||
|
||||
2. **Disable battery optimization:**
|
||||
- MIUI: Battery → "No limits"
|
||||
- Android: Settings → Apps → Disable optimization
|
||||
- iOS: Enable background refresh
|
||||
|
||||
## Input Issues
|
||||
|
||||
### Can't Type Certain Characters
|
||||
|
||||
**Solution:** Settings → Keyboard Type → Switch to `visiblePassword`
|
||||
|
||||
Note: CJK input may not work after this change.
|
||||
|
||||
## App Issues
|
||||
|
||||
### App Crashes on Startup
|
||||
|
||||
**Symptoms:** App won't open, black screen
|
||||
|
||||
**Causes:** Corrupted settings, especially from JSON editor
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Clear app data:**
|
||||
- Android: Settings → Apps → ServerBox → Clear Data
|
||||
- iOS: Delete and reinstall
|
||||
|
||||
2. **Restore backup:** Import backup created before changing settings
|
||||
|
||||
### Backup/Restore Issues
|
||||
|
||||
**Backup not working:**
|
||||
- Check storage space
|
||||
- Verify app has storage permissions
|
||||
- Try different location
|
||||
|
||||
**Restore fails:**
|
||||
- Verify backup file integrity
|
||||
- Check app version compatibility
|
||||
|
||||
## Widget Issues
|
||||
|
||||
### Widget Not Updating
|
||||
|
||||
**iOS:**
|
||||
- Wait up to 30 minutes for automatic refresh
|
||||
- Remove and re-add widget
|
||||
- Check URL ends with `/status`
|
||||
|
||||
**Android:**
|
||||
- Tap widget to force refresh
|
||||
- Verify widget ID matches configuration in app settings
|
||||
|
||||
**watchOS:**
|
||||
- Restart watch app
|
||||
- Wait a few minutes after config change
|
||||
- Verify URL format
|
||||
|
||||
### Widget Shows Error
|
||||
|
||||
- Verify ServerBox Monitor is running on server
|
||||
- Test URL in browser
|
||||
- Check authentication credentials
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### App is Slow
|
||||
|
||||
**Solutions:**
|
||||
- Reduce refresh rate in settings
|
||||
- Check network speed
|
||||
- Disable unused servers
|
||||
|
||||
### High Battery Usage
|
||||
|
||||
**Solutions:**
|
||||
- Increase refresh intervals
|
||||
- Disable background refresh
|
||||
- Close unused SSH sessions
|
||||
|
||||
## Getting Help
|
||||
|
||||
If issues persist:
|
||||
|
||||
1. **Search GitHub Issues:** https://github.com/lollipopkit/flutter_server_box/issues
|
||||
2. **Create New Issue:** Include app version, platform, and steps to reproduce
|
||||
3. **Check Wiki:** This documentation and GitHub Wiki
|
||||
91
docs/src/content/docs/advanced/widgets.md
Normal file
91
docs/src/content/docs/advanced/widgets.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Home Screen Widgets
|
||||
description: Add server status widgets to your home screen
|
||||
---
|
||||
|
||||
Requires [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) installed on your servers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install ServerBox Monitor on your server first. See [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home) for setup instructions.
|
||||
|
||||
After installation, your server should have:
|
||||
- HTTP/HTTPS endpoint
|
||||
- `/status` API endpoint
|
||||
- Optional authentication
|
||||
|
||||
## URL Format
|
||||
|
||||
```
|
||||
https://your-server.com/status
|
||||
```
|
||||
|
||||
Must end with `/status`.
|
||||
|
||||
## iOS Widget
|
||||
|
||||
### Setup
|
||||
|
||||
1. Long press home screen → Tap **+**
|
||||
2. Search "ServerBox"
|
||||
3. Choose widget size
|
||||
4. Long press widget → **Edit Widget**
|
||||
5. Enter URL ending with `/status`
|
||||
|
||||
### Notes
|
||||
|
||||
- Must use HTTPS (except local IPs)
|
||||
- Max refresh rate: 30 minutes (iOS limit)
|
||||
- Add multiple widgets for multiple servers
|
||||
|
||||
## Android Widget
|
||||
|
||||
### Setup
|
||||
|
||||
1. Long press home screen → **Widgets**
|
||||
2. Find "ServerBox" → Add to home screen
|
||||
3. Note the widget ID number displayed
|
||||
4. Open ServerBox app → Settings
|
||||
5. Tap **Config home widget link**
|
||||
6. Add entry: `Widget ID` = `Status URL`
|
||||
|
||||
Example:
|
||||
- Key: `17`
|
||||
- Value: `https://my-server.com/status`
|
||||
|
||||
7. Tap widget on home screen to refresh
|
||||
|
||||
## watchOS Widget
|
||||
|
||||
### Setup
|
||||
|
||||
1. Open iPhone app → Settings
|
||||
2. **iOS Settings** → **Watch app**
|
||||
3. Tap **Add URL**
|
||||
4. Enter URL ending with `/status`
|
||||
5. Wait for watch app to sync
|
||||
|
||||
### Notes
|
||||
|
||||
- Try restarting watch app if not updating
|
||||
- Verify phone and watch are connected
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Widget Not Updating
|
||||
|
||||
**iOS:** Wait up to 30 minutes, then remove and re-add
|
||||
**Android:** Tap widget to force refresh, verify ID in settings
|
||||
**watchOS:** Restart watch app, wait a few minutes
|
||||
|
||||
### Widget Shows Error
|
||||
|
||||
- Verify ServerBox Monitor is running
|
||||
- Test URL in browser
|
||||
- Check URL ends with `/status`
|
||||
|
||||
## Security
|
||||
|
||||
- **Always use HTTPS** when possible
|
||||
- **Local IPs only** on trusted networks
|
||||
83
docs/src/content/docs/de/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/de/advanced/bulk-import.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Massenimport von Servern
|
||||
description: Importieren Sie mehrere Server aus einer JSON-Datei
|
||||
---
|
||||
|
||||
Importieren Sie mehrere Serverkonfigurationen gleichzeitig mithilfe einer JSON-Datei.
|
||||
|
||||
## JSON-Format
|
||||
|
||||
:::danger[Sicherheitswarnung]
|
||||
**Speichern Sie niemals Klartext-Passwörter in Dateien!** Dieses JSON-Beispiel zeigt ein Passwort-Feld nur zur Demonstration, aber Sie sollten:
|
||||
|
||||
- **SSH-Schlüssel bevorzugen** (`keyId`) anstelle von `pwd` - diese sind sicherer
|
||||
- **Passwort-Manager** oder Umgebungsvariablen verwenden, wenn Sie Passwörter verwenden müssen
|
||||
- **Löschen Sie die Datei sofort** nach dem Import - lassen Sie keine Anmeldedaten herumliegen
|
||||
- **Fügen Sie sie zur .gitignore hinzu** - checken Sie niemals Anmeldedatendateien in die Versionsverwaltung ein
|
||||
:::
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Mein Server",
|
||||
"ip": "example.com",
|
||||
"port": 22,
|
||||
"user": "root",
|
||||
"pwd": "password",
|
||||
"keyId": "",
|
||||
"tags": ["production"],
|
||||
"autoConnect": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Felder
|
||||
|
||||
| Feld | Erforderlich | Beschreibung |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Ja | Anzeigename |
|
||||
| `ip` | Ja | Domain oder IP-Adresse |
|
||||
| `port` | Ja | SSH-Port (normalerweise 22) |
|
||||
| `user` | Ja | SSH-Benutzername |
|
||||
| `pwd` | Nein | Passwort (vermeiden - stattdessen SSH-Schlüssel verwenden) |
|
||||
| `keyId` | Nein | SSH-Schlüsselname (aus Private Keys - empfohlen) |
|
||||
| `tags` | Nein | Organisations-Tags |
|
||||
| `autoConnect` | Nein | Automatische Verbindung beim Start |
|
||||
|
||||
## Import-Schritte
|
||||
|
||||
1. Erstellen Sie eine JSON-Datei mit Serverkonfigurationen
|
||||
2. Einstellungen → Backup → Server massenhaft importieren
|
||||
3. Wählen Sie Ihre JSON-Datei aus
|
||||
4. Bestätigen Sie den Import
|
||||
|
||||
## Beispiel
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Produktion",
|
||||
"ip": "prod.example.com",
|
||||
"port": 22,
|
||||
"user": "admin",
|
||||
"keyId": "my-key",
|
||||
"tags": ["production", "web"]
|
||||
},
|
||||
{
|
||||
"name": "Entwicklung",
|
||||
"ip": "dev.example.com",
|
||||
"port": 2222,
|
||||
"user": "dev",
|
||||
"keyId": "dev-key",
|
||||
"tags": ["development"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Tipps
|
||||
|
||||
- **Verwenden Sie SSH-Schlüssel** anstelle von Passwörtern, wann immer möglich
|
||||
- **Testen Sie die Verbindung** nach dem Import
|
||||
- **Organisieren Sie mit Tags** für eine einfachere Verwaltung
|
||||
- **Löschen Sie die JSON-Datei** nach dem Import
|
||||
- **Checken Sie niemals** JSON-Dateien mit Anmeldedaten in die Versionsverwaltung ein
|
||||
72
docs/src/content/docs/de/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/de/advanced/custom-commands.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Benutzerdefinierte Befehle
|
||||
description: Anzeige der Ausgabe benutzerdefinierter Befehle auf der Serverseite
|
||||
---
|
||||
|
||||
Fügen Sie benutzerdefinierte Shell-Befehle hinzu, um deren Ausgabe auf der Server-Detailseite anzuzeigen.
|
||||
|
||||
## Einrichtung
|
||||
|
||||
1. Servereinstellungen → Benutzerdefinierte Befehle
|
||||
2. Befehle im JSON-Format eingeben
|
||||
|
||||
## Basisformat
|
||||
|
||||
```json
|
||||
{
|
||||
"Anzeigename": "Shell-Befehl"
|
||||
}
|
||||
```
|
||||
|
||||
**Beispiel:**
|
||||
```json
|
||||
{
|
||||
"Speicher": "free -h",
|
||||
"Festplatte": "df -h",
|
||||
"Laufzeit": "uptime"
|
||||
}
|
||||
```
|
||||
|
||||
## Ergebnisse anzeigen
|
||||
|
||||
Nach der Einrichtung erscheinen benutzerdefinierte Befehle auf der Server-Detailseite und werden automatisch aktualisiert.
|
||||
|
||||
## Spezielle Befehlsnamen
|
||||
|
||||
### server_card_top_right
|
||||
|
||||
Anzeige auf der Serverkarte der Startseite (oben rechts):
|
||||
|
||||
```json
|
||||
{
|
||||
"server_card_top_right": "Ihr-Befehl-hier"
|
||||
}
|
||||
```
|
||||
|
||||
## Tipps
|
||||
|
||||
**Absolute Pfade verwenden:**
|
||||
```json
|
||||
{"Mein Skript": "/usr/local/bin/mein-skript.sh"}
|
||||
```
|
||||
|
||||
**Pipe-Befehle:**
|
||||
```json
|
||||
{"Top-Prozess": "ps aux | sort -rk 3 | head -5"}
|
||||
```
|
||||
|
||||
**Ausgabe formatieren:**
|
||||
```json
|
||||
{"CPU-Last": "uptime | awk -F'load average:' '{print $2}'"}
|
||||
```
|
||||
|
||||
**Befehle schnell halten:** Unter 5 Sekunden für das beste Erlebnis.
|
||||
|
||||
**Ausgabe begrenzen:**
|
||||
```json
|
||||
{"Logs": "tail -20 /var/log/syslog"}
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
Befehle werden mit den Berechtigungen des SSH-Benutzers ausgeführt. Vermeiden Sie Befehle, die den Systemzustand ändern.
|
||||
54
docs/src/content/docs/de/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/de/advanced/custom-logo.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Benutzerdefiniertes Server-Logo
|
||||
description: Verwenden Sie benutzerdefinierte Bilder für Serverkarten
|
||||
---
|
||||
|
||||
Zeigen Sie benutzerdefinierte Logos auf Serverkarten mithilfe von Bild-URLs an.
|
||||
|
||||
## Einrichtung
|
||||
|
||||
1. Servereinstellungen → Benutzerdefiniertes Logo
|
||||
2. Bild-URL eingeben
|
||||
|
||||
## URL-Platzhalter
|
||||
|
||||
### {DIST} - Linux-Distribution
|
||||
|
||||
Wird automatisch durch die erkannte Distribution ersetzt:
|
||||
|
||||
```
|
||||
https://example.com/{DIST}.png
|
||||
```
|
||||
|
||||
Wird zu: `debian.png`, `ubuntu.png`, `arch.png`, usw.
|
||||
|
||||
### {BRIGHT} - Theme
|
||||
|
||||
Wird automatisch durch das aktuelle Theme ersetzt:
|
||||
|
||||
```
|
||||
https://example.com/{BRIGHT}.png
|
||||
```
|
||||
|
||||
Wird zu: `light.png` oder `dark.png`
|
||||
|
||||
### Beide kombinieren
|
||||
|
||||
```
|
||||
https://example.com/{DIST}-{BRIGHT}.png
|
||||
```
|
||||
|
||||
Wird zu: `debian-light.png`, `ubuntu-dark.png`, usw.
|
||||
|
||||
## Tipps
|
||||
|
||||
- Verwenden Sie PNG- oder SVG-Formate
|
||||
- Empfohlene Größe: 64x64 bis 128x128 Pixel
|
||||
- Verwenden Sie HTTPS-URLs
|
||||
- Halten Sie die Dateigrößen gering
|
||||
|
||||
## Unterstützte Distributionen
|
||||
|
||||
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
|
||||
|
||||
Vollständige Liste: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)
|
||||
64
docs/src/content/docs/de/advanced/json-settings.md
Normal file
64
docs/src/content/docs/de/advanced/json-settings.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Versteckte Einstellungen (JSON)
|
||||
description: Zugriff auf erweiterte Einstellungen über den JSON-Editor
|
||||
---
|
||||
|
||||
Einige Einstellungen sind in der Benutzeroberfläche ausgeblendet, aber über den JSON-Editor zugänglich.
|
||||
|
||||
## Zugriff
|
||||
|
||||
Halten Sie **Einstellungen** in der Seitenleiste lange gedrückt, um den JSON-Editor zu öffnen.
|
||||
|
||||
## Gängige versteckte Einstellungen
|
||||
|
||||
### timeOut
|
||||
|
||||
Verbindungs-Timeout in Sekunden.
|
||||
|
||||
```json
|
||||
{"timeOut": 10}
|
||||
```
|
||||
|
||||
**Typ:** Integer | **Standard:** 5 | **Bereich:** 1-60
|
||||
|
||||
### recordHistory
|
||||
|
||||
Verlauf speichern (SFTP-Pfade, usw.).
|
||||
|
||||
```json
|
||||
{"recordHistory": true}
|
||||
```
|
||||
|
||||
**Typ:** Boolean | **Standard:** true
|
||||
|
||||
### textFactor
|
||||
|
||||
Textskalierungsfaktor.
|
||||
|
||||
```json
|
||||
{"textFactor": 1.2}
|
||||
```
|
||||
|
||||
**Typ:** Double | **Standard:** 1.0 | **Bereich:** 0.8-1.5
|
||||
|
||||
## Weitere Einstellungen finden
|
||||
|
||||
Alle Einstellungen sind in [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart) definiert.
|
||||
|
||||
Suchen Sie nach:
|
||||
```dart
|
||||
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
|
||||
```
|
||||
|
||||
## ⚠️ Wichtig
|
||||
|
||||
**Vor dem Bearbeiten:**
|
||||
- **Backup erstellen** - Falsche Einstellungen können dazu führen, dass die App nicht mehr öffnet
|
||||
- **Sorgfältig bearbeiten** - JSON muss gültig sein
|
||||
|
||||
## Wiederherstellung
|
||||
|
||||
Wenn die App nach dem Bearbeiten nicht mehr öffnet:
|
||||
1. App-Daten löschen (letzter Ausweg)
|
||||
2. App neu installieren
|
||||
3. Aus Backup wiederherstellen
|
||||
118
docs/src/content/docs/de/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/de/advanced/troubleshooting.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Häufige Probleme
|
||||
description: Lösungen für gängige Probleme
|
||||
---
|
||||
|
||||
## Verbindungsprobleme
|
||||
|
||||
### SSH verbindet nicht
|
||||
|
||||
**Symptome:** Timeout, Verbindung abgelehnt, Authentifizierung fehlgeschlagen
|
||||
|
||||
**Lösungen:**
|
||||
|
||||
1. **Servertyp überprüfen:** Nur Unix-ähnliche Systeme werden unterstützt (Linux, macOS, Android/Termux)
|
||||
2. **Manuell testen:** `ssh benutzer@server -p port`
|
||||
3. **Firewall prüfen:** Port 22 muss offen sein
|
||||
4. **Anmeldedaten prüfen:** Benutzername und Passwort/Schlüssel korrekt
|
||||
|
||||
### Häufige Verbindungsabbrüche
|
||||
|
||||
**Symptome:** Das Terminal trennt die Verbindung nach Inaktivität
|
||||
|
||||
**Lösungen:**
|
||||
|
||||
1. **Server Keep-Alive:**
|
||||
```bash
|
||||
# /etc/ssh/sshd_config
|
||||
ClientAliveInterval 60
|
||||
ClientAliveCountMax 3
|
||||
```
|
||||
|
||||
2. **Akku-Optimierung deaktivieren:**
|
||||
- MIUI: Akku → "Keine Beschränkungen"
|
||||
- Android: Einstellungen → Apps → Optimierung deaktivieren
|
||||
- iOS: Hintergrundaktualisierung aktivieren
|
||||
|
||||
## Eingabeprobleme
|
||||
|
||||
### Bestimmte Zeichen können nicht getippt werden
|
||||
|
||||
**Lösung:** Einstellungen → Tastaturtyp → Wechseln zu `visiblePassword`
|
||||
|
||||
Hinweis: CJK-Eingaben funktionieren nach dieser Änderung möglicherweise nicht mehr.
|
||||
|
||||
## App-Probleme
|
||||
|
||||
### App stürzt beim Start ab
|
||||
|
||||
**Symptome:** App öffnet sich nicht, schwarzer Bildschirm
|
||||
|
||||
**Ursachen:** Korrupte Einstellungen, insbesondere durch den JSON-Editor
|
||||
|
||||
**Lösungen:**
|
||||
|
||||
1. **App-Daten löschen:**
|
||||
- Android: Einstellungen → Apps → ServerBox → Daten löschen
|
||||
- iOS: Löschen und neu installieren
|
||||
|
||||
2. **Backup wiederherstellen:** Importieren Sie ein Backup, das vor der Änderung der Einstellungen erstellt wurde
|
||||
|
||||
### Probleme beim Sichern/Wiederherstellen
|
||||
|
||||
**Backup funktioniert nicht:**
|
||||
- Speicherplatz prüfen
|
||||
- Sicherstellen, dass die App Speicherberechtigungen hat
|
||||
- Anderen Speicherort versuchen
|
||||
|
||||
**Wiederherstellung schlägt fehl:**
|
||||
- Integrität der Backup-Datei prüfen
|
||||
- Kompatibilität der App-Version prüfen
|
||||
|
||||
## Widget-Probleme
|
||||
|
||||
### Widget aktualisiert nicht
|
||||
|
||||
**iOS:**
|
||||
- Bis zu 30 Minuten auf automatische Aktualisierung warten
|
||||
- Widget entfernen und neu hinzufügen
|
||||
- Prüfen, ob die URL auf `/status` endet
|
||||
|
||||
**Android:**
|
||||
- Auf das Widget tippen, um die Aktualisierung zu erzwingen
|
||||
- Sicherstellen, dass die Widget-ID mit der Konfiguration in den App-Einstellungen übereinstimmt
|
||||
|
||||
**watchOS:**
|
||||
- Watch-App neu starten
|
||||
- Nach Konfigurationsänderung einige Minuten warten
|
||||
- URL-Format prüfen
|
||||
|
||||
### Widget zeigt Fehler
|
||||
|
||||
- Sicherstellen, dass der ServerBox Monitor auf dem Server läuft
|
||||
- URL im Browser testen
|
||||
- Authentifizierungsdaten prüfen
|
||||
|
||||
## Leistungsprobleme
|
||||
|
||||
### App ist langsam
|
||||
|
||||
**Lösungen:**
|
||||
- Aktualisierungsrate in den Einstellungen reduzieren
|
||||
- Netzwerkgeschwindigkeit prüfen
|
||||
- Nicht verwendete Server deaktivieren
|
||||
|
||||
### Hoher Akkuverbrauch
|
||||
|
||||
**Lösungen:**
|
||||
- Aktualisierungsintervalle vergrößern
|
||||
- Hintergrundaktualisierung deaktivieren
|
||||
- Nicht verwendete SSH-Sitzungen schließen
|
||||
|
||||
## Hilfe erhalten
|
||||
|
||||
Wenn die Probleme weiterhin bestehen:
|
||||
|
||||
1. **GitHub Issues durchsuchen:** https://github.com/lollipopkit/flutter_server_box/issues
|
||||
2. **Neues Issue erstellen:** App-Version, Plattform und Schritte zur Reproduktion angeben
|
||||
3. **Wiki prüfen:** Diese Dokumentation und das GitHub Wiki
|
||||
90
docs/src/content/docs/de/advanced/widgets.md
Normal file
90
docs/src/content/docs/de/advanced/widgets.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Startbildschirm-Widgets
|
||||
description: Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm hinzu
|
||||
---
|
||||
|
||||
Erfordert [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) auf Ihren Servern installiert.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Installieren Sie zuerst ServerBox Monitor auf Ihrem Server. Anweisungen zur Einrichtung finden Sie im [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home).
|
||||
|
||||
Nach der Installation sollte Ihr Server verfügen über:
|
||||
- Einen HTTP/HTTPS-Endpunkt
|
||||
- Einen `/status` API-Endpunkt
|
||||
- Optionale Authentifizierung
|
||||
|
||||
## URL-Format
|
||||
|
||||
```
|
||||
https://ihr-server.com/status
|
||||
```
|
||||
|
||||
Muss auf `/status` enden.
|
||||
|
||||
## iOS-Widget
|
||||
|
||||
### Einrichtung
|
||||
|
||||
1. Startbildschirm lange drücken → Auf **+** tippen
|
||||
2. Nach "ServerBox" suchen
|
||||
3. Widget-Größe wählen
|
||||
4. Widget lange drücken → **Widget bearbeiten**
|
||||
5. URL eingeben, die auf `/status` endet
|
||||
|
||||
### Hinweise
|
||||
|
||||
- Muss HTTPS verwenden (außer bei lokalen IPs)
|
||||
- Maximale Aktualisierungsrate: 30 Minuten (iOS-Limit)
|
||||
- Fügen Sie mehrere Widgets für mehrere Server hinzu
|
||||
|
||||
## Android-Widget
|
||||
|
||||
### Einrichtung
|
||||
|
||||
1. Startbildschirm lange drücken → **Widgets**
|
||||
2. "ServerBox" finden → Zum Startbildschirm hinzufügen
|
||||
3. Notieren Sie sich die angezeigte Widget-ID-Nummer
|
||||
4. ServerBox-App öffnen → Einstellungen
|
||||
5. Tippen Sie auf **Config home widget link**
|
||||
6. Eintrag hinzufügen: `Widget ID` = `Status-URL`
|
||||
|
||||
Beispiel:
|
||||
- Key: `17`
|
||||
- Value: `https://mein-server.com/status`
|
||||
|
||||
7. Tippen Sie auf das Widget auf dem Startbildschirm, um es zu aktualisieren
|
||||
|
||||
## watchOS-Widget
|
||||
|
||||
### Einrichtung
|
||||
|
||||
1. iPhone-App öffnen → Einstellungen
|
||||
2. **iOS-Einstellungen** → **Watch-App**
|
||||
3. Auf **URL hinzufügen** tippen
|
||||
4. URL eingeben, die auf `/status` endet
|
||||
5. Warten, bis die Watch-App synchronisiert ist
|
||||
|
||||
### Hinweise
|
||||
|
||||
- Versuchen Sie, die Watch-App neu zu starten, wenn sie nicht aktualisiert wird
|
||||
- Sicherstellen, dass Telefon und Watch verbunden sind
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Widget aktualisiert nicht
|
||||
|
||||
**iOS:** Warten Sie bis zu 30 Minuten, dann entfernen Sie es und fügen es erneut hinzu.
|
||||
**Android:** Tippen Sie auf das Widget, um die Aktualisierung zu erzwingen, überprüfen Sie die ID in den Einstellungen.
|
||||
**watchOS:** Starten Sie die Watch-App neu, warten Sie einige Minuten.
|
||||
|
||||
### Widget zeigt Fehler an
|
||||
|
||||
- Sicherstellen, dass ServerBox Monitor läuft
|
||||
- URL im Browser testen
|
||||
- Prüfen, ob die URL auf `/status` endet
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- **Verwenden Sie immer HTTPS**, wann immer möglich
|
||||
- **Lokale IPs nur** in vertrauenswürdigen Netzwerken
|
||||
86
docs/src/content/docs/de/development/architecture.md
Normal file
86
docs/src/content/docs/de/development/architecture.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Architektur
|
||||
description: Architekturmuster und Designentscheidungen
|
||||
---
|
||||
|
||||
Server Box folgt den Prinzipien der Clean Architecture mit einer klaren Trennung zwischen Daten-, Domänen- und Präsentationsschicht.
|
||||
|
||||
## Schichtarchitektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Präsentationsschicht │
|
||||
│ (lib/view/page/) │
|
||||
│ - Seiten, Widgets, Controller │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Business-Logik-Schicht │
|
||||
│ (lib/data/provider/) │
|
||||
│ - Riverpod Provider │
|
||||
│ - Zustandsverwaltung │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Datenschicht │
|
||||
│ (lib/data/model/, store/) │
|
||||
│ - Modelle, Speicher, Dienste │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Schlüsselmuster
|
||||
|
||||
### Zustandsverwaltung: Riverpod
|
||||
|
||||
- **Codegenerierung**: Verwendet `riverpod_generator` für typsichere Provider
|
||||
- **State Notifier**: Für veränderlichen Zustand mit Business-Logik
|
||||
- **Async Notifier**: Für Lade- und Fehlerzustände
|
||||
- **Stream Provider**: Für Echtzeitdaten
|
||||
|
||||
### Unveränderliche Modelle: Freezed
|
||||
|
||||
- Alle Datenmodelle verwenden Freezed für Unveränderlichkeit
|
||||
- Union-Typen zur Darstellung von Zuständen
|
||||
- Integrierte JSON-Serialisierung
|
||||
- CopyWith-Erweiterungen für Aktualisierungen
|
||||
|
||||
### Lokale Speicherung: Hive
|
||||
|
||||
- **hive_ce**: Community-Edition von Hive
|
||||
- Keine manuellen `@HiveField` oder `@HiveType` erforderlich
|
||||
- Typ-Adapter werden automatisch generiert
|
||||
- Persistenter Key-Value-Speicher
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Dienste und Stores werden injiziert über:
|
||||
|
||||
1. **Provider**: Stellen Abhängigkeiten der UI zur Verfügung
|
||||
2. **GetIt**: Service-Locator (wo anwendbar)
|
||||
3. **Konstruktor-Injektion**: Explizite Abhängigkeiten
|
||||
|
||||
## Datenfluss
|
||||
|
||||
```
|
||||
Benutzeraktion → Widget → Provider → Dienst/Store → Modell-Update → UI-Neuaufbau
|
||||
```
|
||||
|
||||
1. Benutzer interagiert mit Widget
|
||||
2. Widget ruft Provider-Methode auf
|
||||
3. Provider aktualisiert Zustand über Dienst/Store
|
||||
4. Zustandsänderung löst Neuaufbau der UI aus
|
||||
5. Neuer Zustand spiegelt sich im Widget wider
|
||||
|
||||
## Eigene Abhängigkeiten
|
||||
|
||||
Das Projekt verwendet mehrere eigene Forks zur Funktionserweiterung:
|
||||
|
||||
- **dartssh2**: Erweiterte SSH-Funktionen
|
||||
- **xterm**: Terminal-Emulator mit mobiler Unterstützung
|
||||
- **fl_lib**: Gemeinsame UI-Komponenten und Dienstprogramme
|
||||
|
||||
## Threading
|
||||
|
||||
- **Isolates**: Rechenintensive Aufgaben außerhalb des Main-Threads
|
||||
- **computer-Paket**: Dienstprogramme für Multi-Threading
|
||||
- **Async/Await**: Nicht-blockierende I/O-Operationen
|
||||
116
docs/src/content/docs/de/development/building.md
Normal file
116
docs/src/content/docs/de/development/building.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Bauen
|
||||
description: Bauanleitungen für verschiedene Plattformen
|
||||
---
|
||||
|
||||
Server Box verwendet ein benutzerdefiniertes Build-System (`fl_build`) für plattformübergreifende Builds.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Flutter SDK (stabiler Kanal)
|
||||
- Plattformspezifische Tools (Xcode für iOS, Android Studio für Android)
|
||||
- Rust-Toolchain (für einige native Abhängigkeiten)
|
||||
|
||||
## Entwicklungs-Build
|
||||
|
||||
```bash
|
||||
# Im Entwicklungsmodus ausführen
|
||||
flutter run
|
||||
|
||||
# Auf einem bestimmten Gerät ausführen
|
||||
flutter run -d <device-id>
|
||||
```
|
||||
|
||||
## Produktions-Build
|
||||
|
||||
Das Projekt verwendet `fl_build` zum Bauen:
|
||||
|
||||
```bash
|
||||
# Für eine bestimmte Plattform bauen
|
||||
dart run fl_build -p <platform>
|
||||
|
||||
# Verfügbare Plattformen:
|
||||
# - ios
|
||||
# - android
|
||||
# - macos
|
||||
# - linux
|
||||
# - windows
|
||||
```
|
||||
|
||||
## Plattformspezifische Builds
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p ios
|
||||
```
|
||||
|
||||
Erfordert:
|
||||
- macOS mit Xcode
|
||||
- CocoaPods
|
||||
- Apple Developer Account für die Signierung
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
dart run fl_build -p android
|
||||
```
|
||||
|
||||
Erfordert:
|
||||
- Android SDK
|
||||
- Java Development Kit
|
||||
- Keystore für die Signierung
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p macos
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
dart run fl_build -p linux
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
dart run fl_build -p windows
|
||||
```
|
||||
|
||||
Erfordert Windows mit Visual Studio.
|
||||
|
||||
## Vor/Nach dem Build
|
||||
|
||||
Das Skript `make.dart` übernimmt:
|
||||
|
||||
- Metadaten-Generierung
|
||||
- Aktualisierung der Versions-Strings
|
||||
- Plattformspezifische Konfigurationen
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Clean Build
|
||||
|
||||
```bash
|
||||
flutter clean
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### Versions-Konflikt
|
||||
|
||||
Stellen Sie sicher, dass alle Abhängigkeiten kompatibel sind:
|
||||
```bash
|
||||
flutter pub upgrade
|
||||
```
|
||||
|
||||
## Release-Checkliste
|
||||
|
||||
1. Version in `pubspec.yaml` aktualisieren
|
||||
2. Codegenerierung ausführen
|
||||
3. Tests ausführen
|
||||
4. Für alle Zielplattformen bauen
|
||||
5. Auf physischen Geräten testen
|
||||
6. GitHub-Release erstellen
|
||||
98
docs/src/content/docs/de/development/codegen.md
Normal file
98
docs/src/content/docs/de/development/codegen.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Codegenerierung
|
||||
description: Verwendung von build_runner für die Codegenerierung
|
||||
---
|
||||
|
||||
Server Box verwendet intensiv Codegenerierung für Modelle, Zustandsverwaltung und Serialisierung.
|
||||
|
||||
## Wann sollte die Codegenerierung ausgeführt werden?
|
||||
|
||||
Führen Sie sie aus nach der Änderung von:
|
||||
|
||||
- Modellen mit `@freezed` Annotation
|
||||
- Klassen mit `@JsonSerializable`
|
||||
- Hive-Modellen
|
||||
- Providern mit `@riverpod`
|
||||
- Lokalisierungen (ARB-Dateien)
|
||||
|
||||
## Codegenerierung ausführen
|
||||
|
||||
```bash
|
||||
# Gesamten Code generieren
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Bereinigen und neu generieren
|
||||
dart run build_runner build --delete-conflicting-outputs --clean
|
||||
```
|
||||
|
||||
## Generierte Dateien
|
||||
|
||||
### Freezed (`*.freezed.dart`)
|
||||
|
||||
Unveränderliche Datenmodelle mit Union Types:
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
class ServerState with _$ServerState {
|
||||
const factory ServerState.connected() = Connected;
|
||||
const factory ServerState.disconnected() = Disconnected;
|
||||
const factory ServerState.error(String message) = Error;
|
||||
}
|
||||
```
|
||||
|
||||
### JSON-Serialisierung (`*.g.dart`)
|
||||
|
||||
Generiert durch `json_serializable`:
|
||||
|
||||
```dart
|
||||
@JsonSerializable()
|
||||
class Server {
|
||||
final String id;
|
||||
final String name;
|
||||
final String host;
|
||||
|
||||
Server({required this.id, required this.name, required this.host});
|
||||
|
||||
factory Server.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ServerToJson(this);
|
||||
}
|
||||
```
|
||||
|
||||
### Riverpod Provider (`*.g.dart`)
|
||||
|
||||
Generiert aus der `@riverpod` Annotation:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class MyNotifier extends _$MyNotifier {
|
||||
@override
|
||||
int build() => 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Hive-Adapter (`*.g.dart`)
|
||||
|
||||
Automatisch generiert für Hive-Modelle (hive_ce):
|
||||
|
||||
```dart
|
||||
@HiveType(typeId: 0)
|
||||
class ServerModel {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
}
|
||||
```
|
||||
|
||||
## Generierung der Lokalisierung
|
||||
|
||||
```bash
|
||||
flutter gen-l10n
|
||||
```
|
||||
|
||||
Generiert `lib/generated/l10n/` aus `lib/l10n/*.arb` Dateien.
|
||||
|
||||
## Tipps
|
||||
|
||||
- Verwenden Sie `--delete-conflicting-outputs`, um Konflikte zu vermeiden.
|
||||
- Fügen Sie generierte Dateien zur `.gitignore` hinzu.
|
||||
- Bearbeiten Sie generierte Dateien niemals manuell.
|
||||
115
docs/src/content/docs/de/development/state.md
Normal file
115
docs/src/content/docs/de/development/state.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Zustandsverwaltung
|
||||
description: Riverpod-basierte Zustandsverwaltungsmuster
|
||||
---
|
||||
|
||||
Server Box verwendet Riverpod mit Codegenerierung für die Zustandsverwaltung.
|
||||
|
||||
## Provider-Typen
|
||||
|
||||
### StateProvider
|
||||
|
||||
Einfacher Zustand, der gelesen und geschrieben werden kann:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class Settings extends _$Settings {
|
||||
@override
|
||||
SettingsModel build() {
|
||||
return SettingsModel.defaults();
|
||||
}
|
||||
|
||||
void update(SettingsModel newSettings) {
|
||||
state = newSettings;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AsyncNotifierProvider
|
||||
|
||||
Zustand, der asynchron mit Lade-/Fehlerzuständen geladen wird:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ServerStatus extends _$ServerStatus {
|
||||
@override
|
||||
Future<StatusModel> build(Server server) async {
|
||||
return fetchStatus(server);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => fetchStatus(server));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### StreamProvider
|
||||
|
||||
Echtzeitdaten aus Streams:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
|
||||
return cpuService.monitor(server);
|
||||
}
|
||||
```
|
||||
|
||||
## Zustandsmuster
|
||||
|
||||
### Ladezustände
|
||||
|
||||
```dart
|
||||
state.when(
|
||||
data: (data) => DataWidget(data),
|
||||
loading: () => LoadingWidget(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### Family Provider
|
||||
|
||||
Parametrisierte Provider:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
List<Container> containers(ContainersRef ref, Server server) {
|
||||
return containerService.list(server);
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Dispose
|
||||
|
||||
Provider, die verworfen werden, wenn sie nicht mehr referenziert werden:
|
||||
|
||||
```dart
|
||||
@Riverpod(keepAlive: false)
|
||||
class TempState extends _$TempState {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Codegenerierung nutzen**: Immer die `@riverpod` Annotation verwenden.
|
||||
2. **Provider lokal platzieren**: In der Nähe der Widgets platzieren, die sie nutzen.
|
||||
3. **Singletons vermeiden**: Stattdessen Provider verwenden.
|
||||
4. **Korrekt schichten**: UI-Logik von Business-Logik getrennt halten.
|
||||
|
||||
## Zustand in Widgets lesen
|
||||
|
||||
```dart
|
||||
class ServerWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final status = ref.watch(serverStatusProvider(server));
|
||||
return status.when(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand ändern
|
||||
|
||||
```dart
|
||||
ref.read(settingsProvider.notifier).update(newSettings);
|
||||
```
|
||||
96
docs/src/content/docs/de/development/structure.md
Normal file
96
docs/src/content/docs/de/development/structure.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Projektstruktur
|
||||
description: Verständnis der Server Box Codebasis
|
||||
---
|
||||
|
||||
Das Server Box-Projekt folgt einer modularen Architektur mit einer klaren Trennung der Belange.
|
||||
|
||||
## Verzeichnisstruktur
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # Kern-Dienstprogramme und Erweiterungen
|
||||
├── data/ # Datenschicht
|
||||
│ ├── model/ # Datenmodelle nach Funktionen
|
||||
│ ├── provider/ # Riverpod Provider
|
||||
│ └── store/ # Lokale Speicherung (Hive)
|
||||
├── view/ # UI-Schicht
|
||||
│ ├── page/ # Hauptseiten
|
||||
│ └── widget/ # Wiederverwendbare Widgets
|
||||
├── generated/ # Generierte Lokalisierung
|
||||
├── l10n/ # Lokalisierungs-ARB-Dateien
|
||||
└── hive/ # Hive-Adapter
|
||||
```
|
||||
|
||||
## Kernschicht (`lib/core/`)
|
||||
|
||||
Enthält Dienstprogramme, Erweiterungen und Routing-Konfiguration:
|
||||
|
||||
- **Erweiterungen**: Dart-Erweiterungen für gängige Typen
|
||||
- **Routen**: App-Routing-Konfiguration
|
||||
- **Dienstprogramme**: Gemeinsame Hilfsfunktionen
|
||||
|
||||
## Datenschicht (`lib/data/`)
|
||||
|
||||
### Modelle (`lib/data/model/`)
|
||||
|
||||
Organisiert nach Funktionen:
|
||||
|
||||
- `server/` - Server-Verbindung und Status-Modelle
|
||||
- `container/` - Docker-Container-Modelle
|
||||
- `ssh/` - SSH-Sitzungs-Modelle
|
||||
- `sftp/` - SFTP-Datei-Modelle
|
||||
- `app/` - App-spezifische Modelle
|
||||
|
||||
### Provider (`lib/data/provider/`)
|
||||
|
||||
Riverpod Provider für Dependency Injection und Zustandsverwaltung:
|
||||
|
||||
- Server Provider
|
||||
- UI-Zustands-Provider
|
||||
- Service Provider
|
||||
|
||||
### Stores (`lib/data/store/`)
|
||||
|
||||
Hive-basierte lokale Speicherung:
|
||||
|
||||
- Server-Speicher
|
||||
- Einstellungs-Speicher
|
||||
- Cache-Speicher
|
||||
|
||||
## UI-Schicht (`lib/view/`)
|
||||
|
||||
### Seiten (`lib/view/page/`)
|
||||
|
||||
Hauptbildschirme der Anwendung:
|
||||
|
||||
- `server/` - Server-Verwaltungsseiten
|
||||
- `ssh/` - SSH-Terminal-Seiten
|
||||
- `container/` - Container-Seiten
|
||||
- `setting/` - Einstellungsseiten
|
||||
- `storage/` - SFTP-Seiten
|
||||
- `snippet/` - Snippet-Seiten
|
||||
|
||||
### Widgets (`lib/view/widget/`)
|
||||
|
||||
Wiederverwendbare UI-Komponenten:
|
||||
|
||||
- Server-Karten
|
||||
- Status-Diagramme
|
||||
- Eingabe-Komponenten
|
||||
- Dialoge
|
||||
|
||||
## Generierte Dateien
|
||||
|
||||
- `lib/generated/l10n/` - Automatisch generierte Lokalisierung
|
||||
- `*.g.dart` - Generierter Code (json_serializable, freezed, hive, riverpod)
|
||||
- `*.freezed.dart` - Unveränderliche Freezed-Klassen
|
||||
|
||||
## Verzeichnis "packages" (`/packages/`)
|
||||
|
||||
Enthält eigene Forks von Abhängigkeiten:
|
||||
|
||||
- `dartssh2/` - SSH-Bibliothek
|
||||
- `xterm/` - Terminal-Emulator
|
||||
- `fl_lib/` - Gemeinsame Dienstprogramme
|
||||
- `fl_build/` - Build-System
|
||||
113
docs/src/content/docs/de/development/testing.md
Normal file
113
docs/src/content/docs/de/development/testing.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Testen
|
||||
description: Teststrategien und Ausführung von Tests
|
||||
---
|
||||
|
||||
## Tests ausführen
|
||||
|
||||
```bash
|
||||
# Alle Tests ausführen
|
||||
flutter test
|
||||
|
||||
# Bestimmte Testdatei ausführen
|
||||
flutter test test/battery_test.dart
|
||||
|
||||
# Mit Coverage ausführen
|
||||
flutter test --coverage
|
||||
```
|
||||
|
||||
## Teststruktur
|
||||
|
||||
Tests befinden sich im Verzeichnis `test/` und spiegeln die Struktur von `lib/` wider:
|
||||
|
||||
```
|
||||
test/
|
||||
├── data/
|
||||
│ ├── model/
|
||||
│ └── provider/
|
||||
├── view/
|
||||
│ └── widget/
|
||||
└── test_helpers.dart
|
||||
```
|
||||
|
||||
## Unit-Tests
|
||||
|
||||
Geschäftslogik und Datenmodelle testen:
|
||||
|
||||
```dart
|
||||
test('sollte CPU-Prozentsatz berechnen', () {
|
||||
final cpu = CpuModel(usage: 75.0);
|
||||
expect(cpu.usagePercentage, '75%');
|
||||
});
|
||||
```
|
||||
|
||||
## Widget-Tests
|
||||
|
||||
UI-Komponenten testen:
|
||||
|
||||
```dart
|
||||
testWidgets('ServerCard zeigt Servernamen an', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
home: ServerCard(server: testServer),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Server'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Provider-Tests
|
||||
|
||||
Riverpod Provider testen:
|
||||
|
||||
```dart
|
||||
test('serverStatusProvider gibt Status zurück', () async {
|
||||
final container = ProviderContainer();
|
||||
final status = await container.read(serverStatusProvider(testServer).future);
|
||||
expect(status, isA<StatusModel>());
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
Mocks für externe Abhängigkeiten verwenden:
|
||||
|
||||
```dart
|
||||
class MockSshService extends Mock implements SshService {}
|
||||
|
||||
test('verbindet zum Server', () async {
|
||||
final mockSsh = MockSshService();
|
||||
when(mockSsh.connect(any)).thenAnswer((_) async => true);
|
||||
|
||||
// Test mit Mock
|
||||
});
|
||||
```
|
||||
|
||||
## Integrationstests
|
||||
|
||||
Komplette Benutzerabläufe testen (in `integration_test/`):
|
||||
|
||||
```dart
|
||||
testWidgets('Server hinzufügen Ablauf', (tester) async {
|
||||
await tester.pumpWidget(MyApp());
|
||||
|
||||
// Hinzufügen-Button tippen
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Formular ausfüllen
|
||||
await tester.enterText(find.byKey(Key('name')), 'Test Server');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Arrange-Act-Assert**: Tests klar strukturieren
|
||||
2. **Beschreibende Namen**: Testnamen sollten das Verhalten beschreiben
|
||||
3. **Eine Assertion pro Test**: Tests fokussiert halten
|
||||
4. **Externe Abhängigkeiten mocken**: Nicht von echten Servern abhängig sein
|
||||
5. **Grenzfälle testen**: Leere Listen, Null-Werte, usw.
|
||||
46
docs/src/content/docs/de/index.mdx
Normal file
46
docs/src/content/docs/de/index.mdx
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Server Box
|
||||
description: Eine umfassende plattformübergreifende Server-Management-Anwendung
|
||||
hero:
|
||||
tagline: Verwalten Sie Ihre Linux-Server von überall aus
|
||||
actions:
|
||||
- text: Loslegen
|
||||
link: /de/introduction/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: Auf GitHub ansehen
|
||||
link: https://github.com/lollipopkit/flutter_server_box
|
||||
icon: github
|
||||
variant: minimal
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
## Funktionen
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Echtzeit-Überwachung" icon="chart">
|
||||
Überwachen Sie CPU, Arbeitsspeicher, Festplatte, Netzwerk, GPU und Temperatur mit ansprechenden Echtzeit-Diagrammen.
|
||||
</Card>
|
||||
<Card title="SSH-Terminal" icon="terminal">
|
||||
Voll ausgestattetes SSH-Terminal mit Multi-Tab-Unterstützung und virtueller Tastatur für mobile Geräte.
|
||||
</Card>
|
||||
<Card title="SFTP-Dateibrowser" icon="folder">
|
||||
Verwalten Sie Dateien auf Ihren Servern mit dem integrierten SFTP-Client und dem lokalen Dateibrowser.
|
||||
</Card>
|
||||
<Card title="Docker-Verwaltung" icon="box">
|
||||
Starten, stoppen und überwachen Sie Docker-Container mit einer intuitiven Benutzeroberfläche.
|
||||
</Card>
|
||||
<Card title="Plattformübergreifend" icon="device-mobile">
|
||||
Verfügbar für iOS, Android, macOS, Linux, Windows und watchOS.
|
||||
</Card>
|
||||
<Card title="12+ Sprachen" icon="globe">
|
||||
Vollständige Lokalisierungsunterstützung inklusive Englisch, Chinesisch, Deutsch, Französisch und mehr.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Quick-Links
|
||||
|
||||
- **Download**: Verfügbar im [App Store](https://apps.apple.com/app/id1586449703), auf [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) und bei [F-Droid](https://f-droid.org/)
|
||||
- **Dokumentation**: Entdecken Sie die Anleitungen für den Einstieg in die Server Box
|
||||
- **Support**: Treten Sie unserer Community auf GitHub für Diskussionen und Probleme bei
|
||||
51
docs/src/content/docs/de/installation.mdx
Normal file
51
docs/src/content/docs/de/installation.mdx
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Laden Sie Server Box herunter und installieren Sie es auf Ihrem Gerät
|
||||
---
|
||||
|
||||
Server Box ist für mehrere Plattformen verfügbar. Wählen Sie Ihre bevorzugte Installationsmethode.
|
||||
|
||||
## Mobile Apps
|
||||
|
||||
### iOS
|
||||
|
||||
Laden Sie es aus dem **[App Store](https://apps.apple.com/app/id1586449703)** herunter.
|
||||
|
||||
### Android
|
||||
|
||||
Wählen Sie Ihre bevorzugte Quelle:
|
||||
|
||||
- **[F-Droid](https://f-droid.org/)** – Für Benutzer, die reine FOSS-Quellen bevorzugen
|
||||
- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** – Für die neueste Version direkt von der Quelle
|
||||
|
||||
## Desktop Apps
|
||||
|
||||
### macOS
|
||||
|
||||
Herunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||
|
||||
Funktionen:
|
||||
- Native Menüleisten-Integration
|
||||
- Unterstützung für sowohl Intel als auch Apple Silicon
|
||||
|
||||
### Linux
|
||||
|
||||
Herunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||
|
||||
Verfügbar als AppImage, deb oder tar.gz Pakete.
|
||||
|
||||
### Windows
|
||||
|
||||
Herunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||
|
||||
## watchOS
|
||||
|
||||
Verfügbar im **[App Store](https://apps.apple.com/app/id1586449703)** als Teil der iOS-App.
|
||||
|
||||
## Aus dem Quellcode bauen
|
||||
|
||||
Um Server Box aus dem Quellcode zu bauen, lesen Sie den Abschnitt [Bauen](/de/development/building/) in der Entwicklungsdokumentation.
|
||||
|
||||
## Versionsinformationen
|
||||
|
||||
Besuchen Sie die Seite [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) für die neueste Version und das Änderungsprotokoll.
|
||||
32
docs/src/content/docs/de/introduction.mdx
Normal file
32
docs/src/content/docs/de/introduction.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Einführung
|
||||
description: Erfahren Sie, was Server Box ist und was es kann
|
||||
---
|
||||
|
||||
Server Box ist eine umfassende plattformübergreifende Server-Management-Anwendung, die mit Flutter entwickelt wurde. Sie ermöglicht es Ihnen, Ihre Linux-, Unix- und Windows-Server von überall aus zu überwachen, zu verwalten und zu steuern.
|
||||
|
||||
## Was ist Server Box?
|
||||
|
||||
Server Box bietet eine einheitliche Oberfläche für Server-Administrationsaufgaben über SSH-Verbindungen. Egal, ob Sie Systemadministrator, Entwickler oder Hobbyist mit eigenen Heimservern sind – diese App bietet Ihnen leistungsstarke Server-Management-Tools direkt in Ihrer Tasche.
|
||||
|
||||
## Kernfunktionen
|
||||
|
||||
- **Echtzeit-Überwachung**: Verfolgen Sie CPU, Arbeitsspeicher, Festplattenbelegung, Netzwerkgeschwindigkeit, GPU-Status und Systemtemperaturen.
|
||||
- **SSH-Terminal**: Voller Terminalzugriff mit Multi-Tab-Unterstützung und anpassbarem Erscheinungsbild.
|
||||
- **SFTP-Client**: Durchsuchen und verwalten Sie Dateien auf Ihren Servern.
|
||||
- **Docker-Verwaltung**: Steuern Sie Container mit Leichtigkeit.
|
||||
- **Prozess-Management**: Systemprozesse anzeigen und verwalten.
|
||||
- **Systemd-Dienste**: Systemd-Dienste starten, stoppen und überwachen.
|
||||
- **Netzwerk-Tools**: iPerf-Tests, Ping und Wake-on-LAN.
|
||||
- **Snippets**: Benutzerdefinierte Shell-Befehle speichern und ausführen.
|
||||
|
||||
## Unterstützte Plattformen
|
||||
|
||||
Server Box ist wahrhaft plattformübergreifend:
|
||||
|
||||
- **Mobil**: iOS und Android
|
||||
- **Desktop**: macOS, Linux und Windows
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt ist unter der AGPL v3 lizenziert. Der Quellcode ist auf [GitHub](https://github.com/lollipopkit/flutter_server_box) verfügbar.
|
||||
80
docs/src/content/docs/de/platforms/desktop.md
Normal file
80
docs/src/content/docs/de/platforms/desktop.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Desktop-Funktionen
|
||||
description: Spezifische Funktionen für macOS, Linux und Windows
|
||||
---
|
||||
|
||||
Server Box bietet auf Desktop-Plattformen zusätzliche Produktivitätsfunktionen.
|
||||
|
||||
## macOS
|
||||
|
||||
### Menüleisten-Integration
|
||||
|
||||
- Schneller Serverstatus in der Menüleiste
|
||||
- Serverzugriff mit einem Klick
|
||||
- Kompaktmodus für minimale Ablenkung
|
||||
- Natives macOS-Menüleistenstyling
|
||||
|
||||
### Beständigkeit des Fensterzustands
|
||||
|
||||
- Merkt sich Fensterposition und -größe
|
||||
- Wiederherstellung der vorherigen Sitzung beim Start
|
||||
- Unterstützung für mehrere Monitore
|
||||
|
||||
### Native Funktionen
|
||||
|
||||
- **Titelleiste**: Option für benutzerdefinierte oder System-Titelleiste
|
||||
- **Vollbildmodus**: Dedizierte Serverüberwachung
|
||||
- **Tastenkombinationen**: macOS-native Tastenkürzel
|
||||
- **Touch Bar** (unterstützte Geräte): Schnellaktionen
|
||||
|
||||
## Linux
|
||||
|
||||
### Native Integration
|
||||
|
||||
- Unterstützung für System-Tray
|
||||
- Integration von Desktop-Benachrichtigungen
|
||||
- Integration der Dateiauswahl
|
||||
|
||||
### Fensterverwaltung
|
||||
|
||||
- Unterstützung für X11 und Wayland
|
||||
- Freundlich gegenüber Tiling-Window-Managern
|
||||
- Option für benutzerdefinierte Fensterdekorationen
|
||||
|
||||
## Windows
|
||||
|
||||
### Funktionen
|
||||
|
||||
- System-Tray-Integration
|
||||
- Jump List Schnellaktionen
|
||||
- Native Fenstersteuerung
|
||||
- Option für Autostart beim Booten
|
||||
|
||||
## Plattformübergreifende Desktop-Funktionen
|
||||
|
||||
### Tastenkombinationen
|
||||
|
||||
- **Cmd/Ctrl + N**: Neuer Server
|
||||
- **Cmd/Ctrl + W**: Tab schließen
|
||||
- **Cmd/Ctrl + T**: Neuer Terminal-Tab
|
||||
- **Cmd/Ctrl + ,**: Einstellungen
|
||||
|
||||
### Themes
|
||||
|
||||
- Helles Theme
|
||||
- Dunkles Theme
|
||||
- AMOLED Theme (reines Schwarz)
|
||||
- System-Theme (folgt dem Betriebssystem)
|
||||
|
||||
### Mehrere Fenster
|
||||
|
||||
- Öffnen mehrerer Server in separaten Fenstern
|
||||
- Tabs in ein neues Fenster ziehen
|
||||
- Serverstatistiken Seite an Seite vergleichen
|
||||
|
||||
### Vorteile gegenüber Mobile
|
||||
|
||||
- Größerer Bildschirm für die Überwachung
|
||||
- Volle Tastatur für das Terminal
|
||||
- Schnellere Dateioperationen
|
||||
- Besseres Multitasking
|
||||
77
docs/src/content/docs/de/platforms/mobile.md
Normal file
77
docs/src/content/docs/de/platforms/mobile.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Mobile Funktionen
|
||||
description: Spezifische Funktionen für iOS und Android
|
||||
---
|
||||
|
||||
Server Box bietet mehrere mobile-spezifische Funktionen für iOS- und Android-Geräte.
|
||||
|
||||
## Biometrische Authentifizierung
|
||||
|
||||
Sichern Sie Ihre Server mit biometrischer Authentifizierung:
|
||||
|
||||
- **iOS**: Face ID oder Touch ID
|
||||
- **Android**: Fingerabdruck-Authentifizierung
|
||||
|
||||
Aktivieren Sie dies unter Einstellungen > Sicherheit > Biometrische Authentifizierung.
|
||||
|
||||
## Startbildschirm-Widgets
|
||||
|
||||
Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm für eine schnelle Überwachung hinzu.
|
||||
|
||||
### iOS
|
||||
|
||||
- Auf den Startbildschirm lange drücken
|
||||
- Auf **+** tippen, um ein Widget hinzuzufügen
|
||||
- Nach "Server Box" suchen
|
||||
- Widget-Größe wählen:
|
||||
- Klein: Status eines einzelnen Servers
|
||||
- Mittel: Mehrere Server
|
||||
- Groß: Detaillierte Informationen
|
||||
|
||||
### Android
|
||||
|
||||
- Auf den Startbildschirm lange drücken
|
||||
- Auf **Widgets** tippen
|
||||
- "Server Box" finden
|
||||
- Widget-Typ auswählen
|
||||
|
||||
## Hintergrundbetrieb
|
||||
|
||||
### Android
|
||||
|
||||
Verbindungen im Hintergrund aufrechterhalten:
|
||||
|
||||
- Aktivieren unter Einstellungen > Erweitert > Hintergrundbetrieb
|
||||
- Erfordert Ausschluss von der Akku-Optimierung
|
||||
- Permanente Benachrichtigungen für aktive Verbindungen
|
||||
|
||||
### iOS
|
||||
|
||||
Es gelten Hintergrundbeschränkungen:
|
||||
|
||||
- Verbindungen können im Hintergrund pausieren
|
||||
- Schnelle Wiederverbindung bei Rückkehr zur App
|
||||
- Unterstützung für Hintergrundaktualisierung
|
||||
|
||||
## Push-Benachrichtigungen
|
||||
|
||||
Erhalten Sie Benachrichtigungen für:
|
||||
|
||||
- Server-Offline-Alarme
|
||||
- Warnungen bei hoher Ressourcenauslastung
|
||||
- Alarme bei Abschluss von Aufgaben
|
||||
|
||||
Konfigurieren unter Einstellungen > Benachrichtigungen.
|
||||
|
||||
## Mobile UI-Funktionen
|
||||
|
||||
- **Pull to Refresh**: Serverstatus aktualisieren
|
||||
- **Wischgesten**: Schnelle Serveroperationen
|
||||
- **Querformat**: Besseres Terminal-Erlebnis
|
||||
- **Virtuelle Tastatur**: Terminal-Shortcuts
|
||||
|
||||
## Datei-Integration
|
||||
|
||||
- **Dateien-App (iOS)**: Direkter SFTP-Zugriff aus Dateien
|
||||
- **Storage Access Framework (Android)**: Dateien mit anderen Apps teilen
|
||||
- **Dokumentenauswahl**: Einfache Dateiauswahl
|
||||
214
docs/src/content/docs/de/principles/architecture.md
Normal file
214
docs/src/content/docs/de/principles/architecture.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
title: Architektur-Übersicht
|
||||
description: High-Level-Anwendungsarchitektur
|
||||
---
|
||||
|
||||
Server Box folgt einer Schichtarchitektur mit klarer Trennung der Belange (Separation of Concerns).
|
||||
|
||||
## Architektur-Schichten
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Präsentationsschicht (UI) │
|
||||
│ lib/view/page/, lib/view/widget/ │
|
||||
│ - Seiten, Widgets, Controller │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Business-Logik-Schicht │
|
||||
│ lib/data/provider/ │
|
||||
│ - Riverpod Provider, State Notifier │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Datenzugriffsschicht │
|
||||
│ lib/data/store/, lib/data/model/ │
|
||||
│ - Hive Stores, Datenmodelle │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Externe Integrationsschicht │
|
||||
│ - SSH (dartssh2), Terminal (xterm), SFTP │
|
||||
│ - Plattformspezifischer Code (iOS, Android etc.)│
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Anwendungsgrundlagen
|
||||
|
||||
### Haupteinstiegspunkt
|
||||
|
||||
`lib/main.dart` initialisiert die App:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
runApp(
|
||||
ProviderScope(
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Root-Widget
|
||||
|
||||
`MyApp` bietet:
|
||||
- **Theme-Management**: Umschalten zwischen hellem/dunklem Theme
|
||||
- **Routing-Konfiguration**: Navigationsstruktur
|
||||
- **Provider Scope**: Wurzel für Dependency Injection
|
||||
|
||||
### Startseite
|
||||
|
||||
`HomePage` dient als Navigationszentrum:
|
||||
- **Tab-Interface**: Server, Snippet, Container, SSH
|
||||
- **Zustandsverwaltung**: Zustand pro Tab
|
||||
- **Navigation**: Funktionszugriff
|
||||
|
||||
## Kernsysteme
|
||||
|
||||
### Zustandsverwaltung: Riverpod
|
||||
|
||||
**Warum Riverpod?**
|
||||
- Sicherheit zur Kompilierzeit
|
||||
- Einfache Testbarkeit
|
||||
- Keine Abhängigkeit vom Build-Kontext
|
||||
- Funktioniert plattformübergreifend
|
||||
|
||||
**Verwendete Provider-Typen:**
|
||||
- `StateProvider`: Einfacher veränderlicher Zustand
|
||||
- `AsyncNotifierProvider`: Lade-/Fehler-/Datenzustände
|
||||
- `StreamProvider`: Echtzeit-Datenströme
|
||||
- Future Provider: Einmalige asynchrone Operationen
|
||||
|
||||
### Datenpersistenz: Hive CE
|
||||
|
||||
**Warum Hive CE?**
|
||||
- Keine Abhängigkeiten von nativem Code
|
||||
- Schneller Key-Value-Speicher
|
||||
- Typsicher durch Codegenerierung
|
||||
- Keine manuellen Feld-Annotationen erforderlich
|
||||
|
||||
**Stores:**
|
||||
- `SettingStore`: App-Einstellungen
|
||||
- `ServerStore`: Server-Konfigurationen
|
||||
- `SnippetStore`: Befehls-Snippets
|
||||
- `KeyStore`: SSH-Schlüssel
|
||||
|
||||
### Immutable Modelle: Freezed
|
||||
|
||||
**Vorteile:**
|
||||
- Unveränderlichkeit zur Kompilierzeit
|
||||
- Union Types für Zustände
|
||||
- Integrierte JSON-Serialisierung
|
||||
- CopyWith-Erweiterungen
|
||||
|
||||
## Cross-Plattform-Strategie
|
||||
|
||||
### Plugin-System
|
||||
|
||||
Flutter-Plugins ermöglichen die Plattformintegration:
|
||||
|
||||
| Plattform | Integrationsmethode |
|
||||
|-----------|--------------------|
|
||||
| iOS | CocoaPods, Swift/Obj-C |
|
||||
| Android | Gradle, Kotlin/Java |
|
||||
| macOS | CocoaPods, Swift |
|
||||
| Linux | CMake, C++ |
|
||||
| Windows | CMake, C# |
|
||||
|
||||
### Plattformspezifische Funktionen
|
||||
|
||||
**Nur iOS:**
|
||||
- Startbildschirm-Widgets
|
||||
- Live-Aktivitäten
|
||||
- Apple Watch Begleit-App
|
||||
|
||||
**Nur Android:**
|
||||
- Hintergrunddienst
|
||||
- Push-Benachrichtigungen
|
||||
- Dateisystemzugriff
|
||||
|
||||
**Nur Desktop:**
|
||||
- Menüleisten-Integration
|
||||
- Mehrere Fenster
|
||||
- Benutzerdefinierte Titelleiste
|
||||
|
||||
## Eigene Abhängigkeiten
|
||||
|
||||
### dartssh2 Fork
|
||||
|
||||
Erweiterter SSH-Client mit:
|
||||
- Besserer mobiler Unterstützung
|
||||
- Verbesserter Fehlerbehandlung
|
||||
- Leistungsoptimierungen
|
||||
|
||||
### xterm.dart Fork
|
||||
|
||||
Terminal-Emulator mit:
|
||||
- Für Mobilgeräte optimiertem Rendering
|
||||
- Unterstützung für Touch-Gesten
|
||||
- Integration der virtuellen Tastatur
|
||||
|
||||
### fl_lib
|
||||
|
||||
Paket mit gemeinsamen Dienstprogrammen:
|
||||
- Gemeinsame Widgets
|
||||
- Erweiterungen
|
||||
- Hilfsfunktionen
|
||||
|
||||
## Build-System
|
||||
|
||||
### fl_build Paket
|
||||
|
||||
Eigenes Build-System für:
|
||||
- Multi-Plattform-Builds
|
||||
- Code-Signierung
|
||||
- Asset-Bündelung
|
||||
- Versionsverwaltung
|
||||
|
||||
### Build-Prozess
|
||||
|
||||
```
|
||||
make.dart (Version) → fl_build (Build) → Plattform-Output
|
||||
```
|
||||
|
||||
1. **Pre-build**: Berechnung der Version aus Git
|
||||
2. **Build**: Kompilierung für die Zielplattform
|
||||
3. **Post-build**: Paketierung und Signierung
|
||||
|
||||
## Beispiel für den Datenfluss
|
||||
|
||||
### Aktualisierung des Serverstatus
|
||||
|
||||
```
|
||||
1. Timer löst aus →
|
||||
2. Provider ruft Service auf →
|
||||
3. Service führt SSH-Befehl aus →
|
||||
4. Antwort wird in Modell geparst →
|
||||
5. Zustand wird aktualisiert →
|
||||
6. UI wird mit neuen Daten neu aufgebaut
|
||||
```
|
||||
|
||||
### Ablauf einer Benutzeraktion
|
||||
|
||||
```
|
||||
1. Benutzer tippt auf Schaltfläche →
|
||||
2. Widget ruft Provider-Methode auf →
|
||||
3. Provider aktualisiert Zustand →
|
||||
4. Zustandsänderung löst Neuaufbau aus →
|
||||
5. Neuer Zustand spiegelt sich in der UI wider
|
||||
```
|
||||
|
||||
## Sicherheitsarchitektur
|
||||
|
||||
### Datenschutz
|
||||
|
||||
- **Passwörter**: Verschlüsselt mit flutter_secure_storage
|
||||
- **SSH-Schlüssel**: Verschlüsselt gespeichert
|
||||
- **Host-Fingerabdrücke**: Sicher gespeichert
|
||||
- **Sitzungsdaten**: Werden nicht persistiert
|
||||
|
||||
### Verbindungssicherheit
|
||||
|
||||
- **Host-Key-Verifizierung**: MITM-Erkennung
|
||||
- **Verschlüsselung**: Standard-SSH-Verschlüsselung
|
||||
- **Kein Klartext**: Sensible Daten werden niemals im Klartext gespeichert
|
||||
490
docs/src/content/docs/de/principles/sftp.md
Normal file
490
docs/src/content/docs/de/principles/sftp.md
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
title: SFTP-System
|
||||
description: Wie der SFTP-Dateibrowser funktioniert
|
||||
---
|
||||
|
||||
Das SFTP-System bietet Dateimanagement-Funktionen über SSH.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SFTP UI Schicht │
|
||||
│ - Dateibrowser (remote) │
|
||||
│ - Dateibrowser (lokal) │
|
||||
│ - Transfer-Warteschlange │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SFTP-Zustandsverwaltung │
|
||||
│ - sftpProvider │
|
||||
│ - Pfadverwaltung │
|
||||
│ - Operations-Warteschlange │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SFTP-Protokollschicht │
|
||||
│ - SSH-Subsystem │
|
||||
│ - Dateioperationen │
|
||||
│ - Verzeichnisauflistung │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SSH-Transport │
|
||||
│ - Sicherer Kanal │
|
||||
│ - Daten-Streaming │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Verbindungsaufbau
|
||||
|
||||
### Erstellung des SFTP-Clients
|
||||
|
||||
```dart
|
||||
Future<SftpClient> createSftpClient(Spi spi) async {
|
||||
// 1. SSH-Client abrufen (wiederverwenden, falls verfügbar)
|
||||
final sshClient = await genClient(spi);
|
||||
|
||||
// 2. SFTP-Subsystem öffnen
|
||||
final sftp = await sshClient.openSftp();
|
||||
|
||||
return sftp;
|
||||
}
|
||||
```
|
||||
|
||||
### Wiederverwendung von Verbindungen
|
||||
|
||||
SFTP verwendet bestehende SSH-Verbindungen wieder:
|
||||
|
||||
```dart
|
||||
class ServerProvider {
|
||||
SSHClient? _sshClient;
|
||||
SftpClient? _sftpClient;
|
||||
|
||||
Future<SftpClient> getSftpClient(String spiId) async {
|
||||
_sftpClient ??= await _sshClient!.openSftp();
|
||||
return _sftpClient!;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dateisystem-Operationen
|
||||
|
||||
### Verzeichnisauflistung
|
||||
|
||||
```dart
|
||||
Future<List<SftpFile>> listDirectory(String path) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Verzeichnis auflisten
|
||||
final files = await sftp.listDir(path);
|
||||
|
||||
// Basierend auf Einstellungen sortieren
|
||||
files.sort((a, b) {
|
||||
switch (sortOption) {
|
||||
case SortOption.name:
|
||||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
case SortOption.size:
|
||||
return a.size.compareTo(b.size);
|
||||
case SortOption.time:
|
||||
return a.modified.compareTo(b.modified);
|
||||
}
|
||||
});
|
||||
|
||||
// Ordner zuerst, falls aktiviert
|
||||
if (showFoldersFirst) {
|
||||
final dirs = files.where((f) => f.isDirectory);
|
||||
final regular = files.where((f) => !f.isDirectory);
|
||||
return [...dirs, ...regular];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
```
|
||||
|
||||
### Dateimetadaten
|
||||
|
||||
```dart
|
||||
class SftpFile {
|
||||
final String name;
|
||||
final String path;
|
||||
final int size; // Bytes
|
||||
final int modified; // Unix-Zeitstempel
|
||||
final String permissions; // z.B. "rwxr-xr-x"
|
||||
final String owner;
|
||||
final String group;
|
||||
final bool isDirectory;
|
||||
final bool isSymlink;
|
||||
|
||||
String get sizeFormatted => formatBytes(size);
|
||||
String get modifiedFormatted => formatDate(modified);
|
||||
}
|
||||
```
|
||||
|
||||
## Dateioperationen
|
||||
|
||||
### Hochladen
|
||||
|
||||
```dart
|
||||
Future<void> uploadFile(
|
||||
String localPath,
|
||||
String remotePath,
|
||||
) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Anfrage erstellen
|
||||
final req = SftpReq(
|
||||
spi: spi,
|
||||
remotePath: remotePath,
|
||||
localPath: localPath,
|
||||
type: SftpReqType.upload,
|
||||
);
|
||||
|
||||
// Zur Warteschlange hinzufügen
|
||||
_transferQueue.add(req);
|
||||
|
||||
// Transfer mit Fortschritt ausführen
|
||||
final file = File(localPath);
|
||||
final size = await file.length();
|
||||
final stream = file.openRead();
|
||||
|
||||
await sftp.upload(
|
||||
stream: stream,
|
||||
toPath: remotePath,
|
||||
onProgress: (transferred) {
|
||||
_updateProgress(req, transferred, size);
|
||||
},
|
||||
);
|
||||
|
||||
// Fertigstellen
|
||||
_transferQueue.remove(req);
|
||||
}
|
||||
```
|
||||
|
||||
### Herunterladen
|
||||
|
||||
```dart
|
||||
Future<void> downloadFile(
|
||||
String remotePath,
|
||||
String localPath,
|
||||
) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Lokale Datei erstellen
|
||||
final file = File(localPath);
|
||||
final sink = file.openWrite();
|
||||
|
||||
// Herunterladen mit Fortschritt
|
||||
final stat = await sftp.stat(remotePath);
|
||||
|
||||
await sftp.download(
|
||||
fromPath: remotePath,
|
||||
toSink: sink,
|
||||
onProgress: (transferred) {
|
||||
_updateProgress(
|
||||
SftpReq(...),
|
||||
transferred,
|
||||
stat.size,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await sink.close();
|
||||
}
|
||||
```
|
||||
|
||||
### Berechtigungen bearbeiten
|
||||
|
||||
```dart
|
||||
Future<void> setPermissions(
|
||||
String path,
|
||||
String permissions,
|
||||
) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Berechtigungen parsen (z.B. "rwxr-xr-x" oder "755")
|
||||
final mode = parsePermissions(permissions);
|
||||
|
||||
// Über SSH-Befehl setzen (zuverlässiger als SFTP)
|
||||
final ssh = await getSshClient(spiId);
|
||||
await ssh.exec('chmod $mode "$path"');
|
||||
}
|
||||
```
|
||||
|
||||
## Pfadverwaltung
|
||||
|
||||
### Pfadstruktur
|
||||
|
||||
```dart
|
||||
class PathWithPrefix {
|
||||
final String prefix; // z.B. "/home/user"
|
||||
final String path; // Relativ oder absolut
|
||||
|
||||
String get fullPath {
|
||||
if (path.startsWith('/')) {
|
||||
return path; // Absoluter Pfad
|
||||
}
|
||||
return '$prefix/$path'; // Relativer Pfad
|
||||
}
|
||||
|
||||
PathWithPrefix cd(String subPath) {
|
||||
return PathWithPrefix(
|
||||
prefix: fullPath,
|
||||
path: subPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Navigationsverlauf
|
||||
|
||||
```dart
|
||||
class PathHistory {
|
||||
final List<String> _history = [];
|
||||
int _index = -1;
|
||||
|
||||
void push(String path) {
|
||||
// Vorwärtsverlauf entfernen
|
||||
_history.removeRange(_index + 1, _history.length);
|
||||
_history.add(path);
|
||||
_index = _history.length - 1;
|
||||
}
|
||||
|
||||
String? back() {
|
||||
if (_index > 0) {
|
||||
_index--;
|
||||
return _history[_index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? forward() {
|
||||
if (_index < _history.length - 1) {
|
||||
_index++;
|
||||
return _history[_index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Transfersystem
|
||||
|
||||
### Transfer-Anfrage
|
||||
|
||||
```dart
|
||||
class SftpReq {
|
||||
final Spi spi;
|
||||
final String remotePath;
|
||||
final String localPath;
|
||||
final SftpReqType type;
|
||||
final DateTime createdAt;
|
||||
|
||||
int? totalBytes;
|
||||
int? transferredBytes;
|
||||
String? error;
|
||||
}
|
||||
```
|
||||
|
||||
### Fortschrittsverfolgung
|
||||
|
||||
```dart
|
||||
class TransferProgress {
|
||||
final SftpReq request;
|
||||
final int total;
|
||||
final int transferred;
|
||||
final DateTime startTime;
|
||||
|
||||
double get percentage => (transferred / total) * 100;
|
||||
Duration get elapsed => DateTime.now().difference(startTime);
|
||||
|
||||
String get speedFormatted {
|
||||
final bytesPerSecond = transferred / elapsed.inSeconds;
|
||||
return formatSpeed(bytesPerSecond);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Warteschlangen-Management
|
||||
|
||||
```dart
|
||||
class TransferQueue {
|
||||
final List<SftpReq> _queue = [];
|
||||
final Map<String, TransferProgress> _progress = {};
|
||||
int _concurrent = 3; // Max. gleichzeitige Transfers
|
||||
|
||||
Future<void> process() async {
|
||||
final active = _progress.values.where((p) => p.isInProgress);
|
||||
if (active.length >= _concurrent) return;
|
||||
|
||||
final pending = _queue.where((r) => !_progress.containsKey(r.id));
|
||||
for (final req in pending.take(_concurrent - active.length)) {
|
||||
_executeTransfer(req);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _executeTransfer(SftpReq req) async {
|
||||
try {
|
||||
_progress[req.id] = TransferProgress.inProgress(req);
|
||||
|
||||
if (req.type == SftpReqType.upload) {
|
||||
await uploadFile(req.localPath, req.remotePath);
|
||||
} else {
|
||||
await downloadFile(req.remotePath, req.localPath);
|
||||
}
|
||||
|
||||
_progress[req.id] = TransferProgress.completed(req);
|
||||
} catch (e) {
|
||||
_progress[req.id] = TransferProgress.failed(req, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lokales Speichermuster
|
||||
|
||||
### Download-Cache
|
||||
|
||||
Heruntergeladene Dateien werden gespeichert unter:
|
||||
|
||||
```dart
|
||||
String getLocalDownloadPath(String spiId, String remotePath) {
|
||||
final normalized = remotePath.replaceAll('/', '_');
|
||||
return 'Paths.file/$spiId/$normalized';
|
||||
}
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
- Remote: `/var/log/nginx/access.log`
|
||||
- spiId: `server-123`
|
||||
- Lokal: `Paths.file/server-123/_var_log_nginx_access.log`
|
||||
|
||||
## Dateibearbeitung
|
||||
|
||||
### Bearbeitungs-Workflow
|
||||
|
||||
```dart
|
||||
Future<void> editFile(String path) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// 1. Größe prüfen
|
||||
final stat = await sftp.stat(path);
|
||||
if (stat.size > editorMaxSize) {
|
||||
showWarning('Datei zu groß für den integrierten Editor');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Temporär herunterladen
|
||||
final temp = await downloadToTemp(path);
|
||||
|
||||
// 3. Im Editor öffnen
|
||||
final content = await openEditor(temp.path);
|
||||
|
||||
// 4. Zurück hochladen
|
||||
await uploadFile(temp.path, path);
|
||||
|
||||
// 5. Aufräumen
|
||||
await temp.delete();
|
||||
}
|
||||
```
|
||||
|
||||
### Integration externer Editoren
|
||||
|
||||
```dart
|
||||
Future<void> editInExternalEditor(String path) async {
|
||||
final ssh = await getSshClient(spiId);
|
||||
|
||||
// Terminal mit Editor öffnen
|
||||
final editor = getSetting('sftpEditor', 'vim');
|
||||
await ssh.exec('$editor "$path"');
|
||||
|
||||
// Benutzer bearbeitet im Terminal
|
||||
// Nach dem Speichern die SFTP-Ansicht aktualisieren
|
||||
}
|
||||
```
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### Berechtigungsfehler
|
||||
|
||||
```dart
|
||||
try {
|
||||
await sftp.upload(...);
|
||||
} on SftpPermissionException {
|
||||
showError('Berechtigung verweigert: ${stat.path}');
|
||||
showHint('Prüfen Sie Dateiberechtigungen und Eigentümerschaft');
|
||||
}
|
||||
```
|
||||
|
||||
### Verbindungsfehler
|
||||
|
||||
```dart
|
||||
try {
|
||||
await sftp.listDir(path);
|
||||
} on SftpConnectionException {
|
||||
showError('Verbindung verloren');
|
||||
await reconnect();
|
||||
}
|
||||
```
|
||||
|
||||
### Speicherplatzfehler
|
||||
|
||||
```dart
|
||||
try {
|
||||
await sftp.upload(...);
|
||||
} on SftpNoSpaceException {
|
||||
showError('Festplatte auf dem Remote-Server voll');
|
||||
}
|
||||
```
|
||||
|
||||
## Leistungsoptimierungen
|
||||
|
||||
### Verzeichnis-Caching
|
||||
|
||||
```dart
|
||||
class DirectoryCache {
|
||||
final Map<String, CachedDirectory> _cache = {};
|
||||
final Duration ttl = Duration(minutes: 5);
|
||||
|
||||
Future<List<SftpFile>> list(String path) async {
|
||||
final cached = _cache[path];
|
||||
if (cached != null && !cached.isExpired) {
|
||||
return cached.files;
|
||||
}
|
||||
|
||||
final files = await sftp.listDir(path);
|
||||
_cache[path] = CachedDirectory(files);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Für große Verzeichnisse (>1000 Einträge):
|
||||
|
||||
```dart
|
||||
List<SftpFile> loadPage(String path, int page, int pageSize) {
|
||||
final all = cache[path] ?? [];
|
||||
final start = page * pageSize;
|
||||
final end = start + pageSize;
|
||||
return all.sublist(start, end.clamp(0, all.length));
|
||||
}
|
||||
```
|
||||
|
||||
### Paginierung
|
||||
|
||||
```dart
|
||||
class PaginatedDirectory {
|
||||
static const pageSize = 100;
|
||||
|
||||
Future<List<SftpFile>> getPage(int page) async {
|
||||
final offset = page * pageSize;
|
||||
return await sftp.listDir(
|
||||
path,
|
||||
offset: offset,
|
||||
limit: pageSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
305
docs/src/content/docs/de/principles/ssh.md
Normal file
305
docs/src/content/docs/de/principles/ssh.md
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: SSH-Verbindung
|
||||
description: Wie SSH-Verbindungen aufgebaut und verwaltet werden
|
||||
---
|
||||
|
||||
Verständnis der SSH-Verbindungen in Server Box.
|
||||
|
||||
## Verbindungsablauf
|
||||
|
||||
```text
|
||||
Benutzereingabe → Spi-Konfiguration → genClient() → SSH-Client → Sitzung
|
||||
```
|
||||
|
||||
### Schritt 1: Konfiguration
|
||||
|
||||
Das `Spi` (Server Parameter Info) Modell enthält:
|
||||
|
||||
```dart
|
||||
class Spi {
|
||||
String id; // eindeutige ID / unique identifier
|
||||
String name; // Servername
|
||||
String ip; // IP-Adresse
|
||||
int port; // SSH-Port (Standard 22)
|
||||
String user; // Benutzername
|
||||
String? pwd; // Passwort (verschlüsselt)
|
||||
String? keyId; // SSH-Schlüssel-ID
|
||||
String? jumpId; // Jump-Server-ID
|
||||
String? alterUrl; // Alternative URL
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 2: Client-Generierung
|
||||
|
||||
`genClient(spi)` erstellt den SSH-Client:
|
||||
|
||||
```dart
|
||||
Future<SSHClient> genClient(Spi spi) async {
|
||||
// 1. Socket aufbauen
|
||||
var socket = await connect(spi.ip, spi.port);
|
||||
|
||||
// 2. Alternative URL versuchen, falls fehlgeschlagen
|
||||
if (socket == null && spi.alterUrl != null) {
|
||||
socket = await connect(spi.alterUrl, spi.port);
|
||||
}
|
||||
|
||||
if (socket == null) {
|
||||
throw ConnectionException('Unable to connect');
|
||||
}
|
||||
|
||||
// 3. Authentifizieren
|
||||
final client = SSHClient(
|
||||
socket: socket,
|
||||
username: spi.user,
|
||||
onPasswordRequest: () => spi.pwd,
|
||||
onIdentityRequest: () => loadKey(spi.keyId),
|
||||
);
|
||||
|
||||
// 4. Host-Key verifizieren
|
||||
await verifyHostKey(client, spi);
|
||||
|
||||
return client;
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 3: Jump-Server (falls konfiguriert)
|
||||
|
||||
Für Jump-Server, rekursive Verbindung:
|
||||
|
||||
```dart
|
||||
if (spi.jumpId != null) {
|
||||
final jumpClient = await genClient(getJumpSpi(spi.jumpId));
|
||||
final forwarded = await jumpClient.forwardLocal(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
);
|
||||
// Über weitergeleiteten Socket verbinden
|
||||
}
|
||||
```
|
||||
|
||||
## Authentifizierungsmethoden
|
||||
|
||||
### Passwort-Authentifizierung
|
||||
|
||||
```dart
|
||||
onPasswordRequest: () => spi.pwd
|
||||
```
|
||||
|
||||
- Passwort verschlüsselt in Hive gespeichert
|
||||
- Bei Verbindung entschlüsselt
|
||||
- Zur Verifizierung an den Server gesendet
|
||||
|
||||
### Private-Key-Authentifizierung
|
||||
|
||||
```dart
|
||||
onIdentityRequest: () async {
|
||||
final key = await KeyStore.get(spi.keyId);
|
||||
return decyptPem(key.pem, key.password);
|
||||
}
|
||||
```
|
||||
|
||||
**Schlüssel-Ladeprozess:**
|
||||
1. Verschlüsselten Schlüssel aus `KeyStore` abrufen
|
||||
2. Passwort entschlüsseln (Biometrie/Eingabeaufforderung)
|
||||
3. PEM-Format parsen
|
||||
4. Zeilenenden standardisieren (LF)
|
||||
5. Zur Authentifizierung zurückgeben
|
||||
|
||||
### Tastatur-Interaktiv (Keyboard-Interactive)
|
||||
|
||||
```dart
|
||||
onUserInfoRequest: (instructions) async {
|
||||
// Challenge-Response handhaben
|
||||
return responses;
|
||||
}
|
||||
```
|
||||
|
||||
Unterstützt:
|
||||
- Passwort-Authentifizierung
|
||||
- OTP-Token
|
||||
- Zwei-Faktor-Authentifizierung (2FA)
|
||||
|
||||
## Host-Key-Verifizierung
|
||||
|
||||
### Warum Host-Keys verifizieren?
|
||||
|
||||
Verhindert **Man-in-the-Middle (MITM)** Angriffe, indem sichergestellt wird, dass Sie sich mit demselben Server verbinden.
|
||||
|
||||
### Speicherformat
|
||||
|
||||
```text
|
||||
{spi.id}::{keyType}
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
```text
|
||||
mein-server::ssh-ed25519
|
||||
mein-server::ecdsa-sha2-nistp256
|
||||
```
|
||||
|
||||
### Fingerabdruck-Formate
|
||||
|
||||
**MD5 Hex:**
|
||||
```text
|
||||
aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
|
||||
```
|
||||
|
||||
**Base64:**
|
||||
```text
|
||||
SHA256:AbCdEf1234567890...=
|
||||
```
|
||||
|
||||
### Verifizierungsablauf
|
||||
|
||||
```dart
|
||||
Future<void> verifyHostKey(SSHClient client, Spi spi) async {
|
||||
final key = await client.hostKey;
|
||||
final keyType = key.type;
|
||||
final fingerprint = md5Hex(key); // oder base64
|
||||
|
||||
final stored = SettingStore.sshKnownHostsFingerprints
|
||||
['${spi.id}::$keyType'];
|
||||
|
||||
if (stored == null) {
|
||||
// Neuer Host - Benutzer fragen
|
||||
final trust = await promptUser(
|
||||
'Unbekannter Host',
|
||||
'Fingerabdruck: $fingerprint',
|
||||
);
|
||||
if (trust) {
|
||||
SettingStore.sshKnownHostsFingerprints
|
||||
['${spi.id}::$keyType'] = fingerprint;
|
||||
}
|
||||
} else if (stored != fingerprint) {
|
||||
// Geändert - Benutzer warnen
|
||||
await warnUser(
|
||||
'Host-Key geändert!',
|
||||
'Möglicher MITM-Angriff',
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sitzungsverwaltung
|
||||
|
||||
### Verbindungs-Pooling
|
||||
|
||||
Aktive Clients werden im `ServerProvider` verwaltet:
|
||||
|
||||
```dart
|
||||
class ServerProvider {
|
||||
final Map<String, SSHClient> _clients = {};
|
||||
|
||||
SSHClient getClient(String spiId) {
|
||||
return _clients[spiId] ??= connect(spiId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Keep-Alive
|
||||
|
||||
Verbindung bei Inaktivität aufrechterhalten:
|
||||
|
||||
```dart
|
||||
Timer.periodic(
|
||||
Duration(seconds: 30),
|
||||
(_) => client.sendKeepAlive(),
|
||||
);
|
||||
```
|
||||
|
||||
### Automatische Wiederverbindung
|
||||
|
||||
Bei Verbindungsverlust:
|
||||
|
||||
```dart
|
||||
client.onError.listen((error) async {
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
reconnect();
|
||||
});
|
||||
```
|
||||
|
||||
## Lebenszyklus einer Verbindung
|
||||
|
||||
```text
|
||||
┌─────────────┐
|
||||
│ Initial │
|
||||
└──────┬──────┘
|
||||
│ connect()
|
||||
↓
|
||||
┌─────────────┐
|
||||
│ Verbinden │ ←──┐
|
||||
└──────┬──────┘ │
|
||||
│ Erfolg │
|
||||
↓ │ Fehler (Retry)
|
||||
┌─────────────┐ │
|
||||
│ Verbunden │───┘
|
||||
└──────┬──────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────┐
|
||||
│ Aktiv │ ──→ Befehle senden
|
||||
└──────┬──────┘
|
||||
│
|
||||
↓ (Fehler/Trennung)
|
||||
┌─────────────┐
|
||||
│ Getrennt │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### Verbindungs-Timeout
|
||||
|
||||
```dart
|
||||
try {
|
||||
await client.connect().timeout(
|
||||
Duration(seconds: 30),
|
||||
);
|
||||
} on TimeoutException {
|
||||
throw ConnectionException('Verbindungs-Timeout');
|
||||
}
|
||||
```
|
||||
|
||||
### Authentifizierungsfehler
|
||||
|
||||
```dart
|
||||
onAuthFail: (error) {
|
||||
if (error.contains('password')) {
|
||||
return 'Ungültiges Passwort';
|
||||
} else if (error.contains('key')) {
|
||||
return 'Ungültiger SSH-Schlüssel';
|
||||
}
|
||||
return 'Authentifizierung fehlgeschlagen';
|
||||
}
|
||||
```
|
||||
|
||||
### Host-Key-Abweichung
|
||||
|
||||
```dart
|
||||
onHostKeyMismatch: (stored, current) {
|
||||
showSecurityWarning(
|
||||
'Host-Key hat sich geändert!',
|
||||
'Möglicher MITM-Angriff',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Leistungsaspekte
|
||||
|
||||
### Wiederverwendung von Verbindungen
|
||||
|
||||
- Wiederverwendung von Clients über Funktionen hinweg
|
||||
- Nicht unnötig trennen/wiederverbinden
|
||||
- Verbindungs-Pooling für gleichzeitige Operationen
|
||||
|
||||
### Optimale Einstellungen
|
||||
|
||||
- **Timeout**: 30 Sekunden (anpassbar)
|
||||
- **Keep-Alive**: Alle 30 Sekunden
|
||||
- **Wiederholungsverzögerung**: 5 Sekunden
|
||||
|
||||
### Netzwerkeffizienz
|
||||
|
||||
- Einzelne Verbindung für mehrere Operationen
|
||||
- Befehle pipelinen, wenn möglich
|
||||
- Das Öffnen mehrerer Verbindungen vermeiden
|
||||
221
docs/src/content/docs/de/principles/state.md
Normal file
221
docs/src/content/docs/de/principles/state.md
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
title: Zustandsverwaltung
|
||||
description: Wie der Zustand mit Riverpod verwaltet wird
|
||||
---
|
||||
|
||||
Verständnis der Architektur zur Zustandsverwaltung in Server Box.
|
||||
|
||||
## Warum Riverpod?
|
||||
|
||||
**Hauptvorteile:**
|
||||
- **Sicherheit zur Kompilierzeit**: Fehler werden bereits beim Kompilieren abgefangen
|
||||
- **Kein BuildContext erforderlich**: Zugriff auf den Zustand von überall aus
|
||||
- **Einfache Testbarkeit**: Provider können leicht isoliert getestet werden
|
||||
- **Codegenerierung**: Weniger Boilerplate, typsicher
|
||||
|
||||
## Provider-Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ UI-Schicht (Widgets) │
|
||||
│ - ConsumerWidget / ConsumerStatefulWidget │
|
||||
│ - ref.watch() / ref.read() │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓ beobachtet (watches)
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Provider-Schicht │
|
||||
│ - @riverpod Annotationen │
|
||||
│ - Generierte *.g.dart Dateien │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓ nutzt (uses)
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Service- / Store-Schicht │
|
||||
│ - Business-Logik │
|
||||
│ - Datenzugriff │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Verwendete Provider-Typen
|
||||
|
||||
### 1. StateProvider (Einfacher Zustand)
|
||||
|
||||
Für einfachen, beobachtbaren Zustand:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ThemeNotifier extends _$ThemeNotifier {
|
||||
@override
|
||||
ThemeMode build() {
|
||||
// Aus Einstellungen laden
|
||||
return SettingStore.themeMode;
|
||||
}
|
||||
|
||||
void setTheme(ThemeMode mode) {
|
||||
state = mode;
|
||||
SettingStore.themeMode = mode; // Persistieren
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verwendung:**
|
||||
```dart
|
||||
class MyWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themeNotifierProvider);
|
||||
return Text('Theme: $theme');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AsyncNotifierProvider (Asynchroner Zustand)
|
||||
|
||||
Für Daten, die asynchron geladen werden:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ServerStatus extends _$ServerStatus {
|
||||
@override
|
||||
Future<StatusModel> build(Server server) async {
|
||||
// Initiales Laden
|
||||
return await fetchStatus(server);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return await fetchStatus(server);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verwendung:**
|
||||
```dart
|
||||
final status = ref.watch(serverStatusProvider(server));
|
||||
|
||||
status.when(
|
||||
data: (data) => StatusWidget(data),
|
||||
loading: () => LoadingWidget(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### 3. StreamProvider (Echtzeit-Daten)
|
||||
|
||||
Für kontinuierliche Datenströme:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
|
||||
final client = ref.watch(sshClientProvider(server));
|
||||
final stream = client.monitorCpu();
|
||||
|
||||
// Ressourcen freigeben, wenn nicht mehr beobachtet
|
||||
ref.onDispose(() {
|
||||
client.stopMonitoring();
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
```
|
||||
|
||||
**Verwendung:**
|
||||
```dart
|
||||
final cpu = ref.watch(cpuUsageProvider(server));
|
||||
|
||||
cpu.when(
|
||||
data: (usage) => CpuChart(usage),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Family Provider (Parametrisiert)
|
||||
|
||||
Provider, die Parameter akzeptieren:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Future<List<Container>> containers(ContainersRef ref, Server server) async {
|
||||
final client = await ref.watch(sshClientProvider(server).future);
|
||||
return await client.listContainers();
|
||||
}
|
||||
```
|
||||
|
||||
**Verwendung:**
|
||||
```dart
|
||||
final containers = ref.watch(containersProvider(server));
|
||||
|
||||
// Verschiedene Server = verschiedene gecachte Zustände
|
||||
final containers2 = ref.watch(containersProvider(server2));
|
||||
```
|
||||
|
||||
## Muster für Zustandsaktualisierungen
|
||||
|
||||
### Direkte Zustandsaktualisierung
|
||||
|
||||
```dart
|
||||
ref.read(settingsProvider.notifier).updateTheme(darkMode);
|
||||
```
|
||||
|
||||
### Berechneter Zustand (Computed State)
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
int totalServers(TotalServersRef ref) {
|
||||
final servers = ref.watch(serversProvider);
|
||||
return servers.length;
|
||||
}
|
||||
```
|
||||
|
||||
### Abgeleiteter Zustand (Derived State)
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
List<Server> onlineServers(OnlineServersRef ref) {
|
||||
final all = ref.watch(serversProvider);
|
||||
return all.where((s) => s.isOnline).toList();
|
||||
}
|
||||
```
|
||||
|
||||
## Server-spezifischer Zustand
|
||||
|
||||
### Pro-Server Provider
|
||||
|
||||
Jeder Server hat einen isolierten Zustand:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ServerProvider extends _$ServerProvider {
|
||||
@override
|
||||
ServerState build(Server server) {
|
||||
return ServerState.disconnected();
|
||||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
state = ServerState.connecting();
|
||||
try {
|
||||
final client = await genClient(server.spi);
|
||||
state = ServerState.connected(client);
|
||||
} catch (e) {
|
||||
state = ServerState.error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Leistungsoptimierungen
|
||||
|
||||
- **Provider Keep-Alive**: `@Riverpod(keepAlive: true)` verwenden, um automatische Entsorgung ohne Listener zu verhindern.
|
||||
- **Selektives Beobachten**: `select` verwenden, um nur bestimmte Teile des Zustands zu beobachten.
|
||||
- **Provider Caching**: Family Provider cachen Ergebnisse pro Parameter.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Provider lokal platzieren**: In der Nähe der Widgets, die sie nutzen.
|
||||
2. **Codegenerierung nutzen**: Immer `@riverpod` verwenden.
|
||||
3. **Provider fokussiert halten**: Jedes Provider sollte nur eine Aufgabe haben.
|
||||
4. **Ladezustände behandeln**: AsyncValue-Zustände immer vollständig behandeln.
|
||||
5. **Ressourcen entsorgen**: `ref.onDispose()` für Aufräumarbeiten nutzen.
|
||||
6. **Tiefe Provider-Bäume vermeiden**: Den Provider-Graphen flach halten.
|
||||
198
docs/src/content/docs/de/principles/terminal.md
Normal file
198
docs/src/content/docs/de/principles/terminal.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: Terminal-Implementierung
|
||||
description: Wie das SSH-Terminal intern funktioniert
|
||||
---
|
||||
|
||||
Das SSH-Terminal ist eine der komplexesten Funktionen, aufgebaut auf einem benutzerdefinierten xterm.dart-Fork.
|
||||
|
||||
## Architektur-Übersicht
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Terminal UI Schicht │
|
||||
│ - Tab-Management │
|
||||
│ - Virtuelle Tastatur │
|
||||
│ - Textauswahl │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ xterm.dart Emulator │
|
||||
│ - PTY (Pseudo Terminal) │
|
||||
│ - VT100/ANSI Emulation │
|
||||
│ - Rendering-Engine │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SSH-Client-Schicht │
|
||||
│ - SSH-Sitzung │
|
||||
│ - Kanalverwaltung │
|
||||
│ - Daten-Streaming │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Remote-Server │
|
||||
│ - Shell-Prozess │
|
||||
│ - Befehlsausführung │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Lebenszyklus einer Terminal-Sitzung
|
||||
|
||||
### 1. Sitzungserstellung
|
||||
|
||||
```dart
|
||||
Future<TerminalSession> createSession(Spi spi) async {
|
||||
// 1. SSH-Client abrufen
|
||||
final client = await genClient(spi);
|
||||
|
||||
// 2. PTY erstellen
|
||||
final pty = await client.openPty(
|
||||
term: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
);
|
||||
|
||||
// 3. Terminal-Emulator initialisieren
|
||||
final terminal = Terminal(
|
||||
backend: PtyBackend(pty),
|
||||
);
|
||||
|
||||
// 4. Resize-Handler einrichten
|
||||
terminal.onResize.listen((size) {
|
||||
pty.resize(size.cols, size.rows);
|
||||
});
|
||||
|
||||
return TerminalSession(
|
||||
terminal: terminal,
|
||||
pty: pty,
|
||||
client: client,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Terminal-Emulation
|
||||
|
||||
Der xterm.dart-Fork bietet:
|
||||
|
||||
**VT100/ANSI Emulation:**
|
||||
- Cursor-Bewegung
|
||||
- Farben (256-Farben-Unterstützung)
|
||||
- Textattribute (fett, unterstrichen, usw.)
|
||||
- Scroll-Bereiche
|
||||
- Alternativer Bildschirmpuffer
|
||||
|
||||
**Rendering:**
|
||||
- Zeilenbasiertes Rendering
|
||||
- Unterstützung für bidirektionalen Text
|
||||
- Unicode/Emoji Unterstützung
|
||||
- Optimierte Redraws
|
||||
|
||||
### 3. Datenfluss
|
||||
|
||||
```
|
||||
Benutzereingabe
|
||||
↓
|
||||
Virtuelle Tastatur / Physische Tastatur
|
||||
↓
|
||||
Terminal-Emulator (Taste → Escape-Sequenz)
|
||||
↓
|
||||
SSH-Kanal (senden)
|
||||
↓
|
||||
Remote PTY
|
||||
↓
|
||||
Remote Shell
|
||||
↓
|
||||
Befehlsausgabe
|
||||
↓
|
||||
SSH-Kanal (empfangen)
|
||||
↓
|
||||
Terminal-Emulator (Analyse von ANSI-Codes)
|
||||
↓
|
||||
Rendering auf dem Bildschirm
|
||||
```
|
||||
|
||||
## Multi-Tab System
|
||||
|
||||
### Tab-Management
|
||||
|
||||
Tabs behalten ihren Zustand bei Navigationswechseln bei:
|
||||
- SSH-Verbindung bleibt aktiv
|
||||
- Terminalzustand bleibt erhalten
|
||||
- Scroll-Puffer bleibt bestehen
|
||||
- Eingabeverlauf bleibt erhalten
|
||||
|
||||
## Virtuelle Tastatur
|
||||
|
||||
### Plattformspezifische Implementierung
|
||||
|
||||
**iOS:**
|
||||
- UIView-basierte benutzerdefinierte Tastatur
|
||||
- Umschaltbar mit Tastatur-Button
|
||||
- Automatisches Ein-/Ausblenden basierend auf dem Fokus
|
||||
|
||||
**Android:**
|
||||
- Benutzerdefinierte Eingabemethode
|
||||
- Integriert in die Systemtastatur
|
||||
- Schnellaktionstasten
|
||||
|
||||
### Tastatur-Buttons
|
||||
|
||||
| Button | Aktion |
|
||||
|--------|--------|
|
||||
| **Umschalten** | Systemtastatur ein-/ausblenden |
|
||||
| **Ctrl** | Ctrl-Modifikator senden |
|
||||
| **Alt** | Alt-Modifikator senden |
|
||||
| **SFTP** | Aktuelles Verzeichnis öffnen |
|
||||
| **Zwischenablage** | Kontextsensitive Kopieren/Einfügen |
|
||||
| **Snippets** | Snippet ausführen |
|
||||
|
||||
## Textauswahl
|
||||
|
||||
1. **Langes Drücken**: Auswahlmodus aktivieren
|
||||
2. **Ziehen**: Auswahl erweitern
|
||||
3. **Loslassen**: In die Zwischenablage kopieren
|
||||
|
||||
## Schriftart und Dimensionen
|
||||
|
||||
### Größenberechnung
|
||||
|
||||
```dart
|
||||
class TerminalDimensions {
|
||||
static Size calculate(double fontSize, Size screenSize) {
|
||||
final charWidth = fontSize * 0.6; // Monospace-Seitenverhältnis
|
||||
final charHeight = fontSize * 1.2;
|
||||
|
||||
final cols = (screenSize.width / charWidth).floor();
|
||||
final rows = (screenSize.height / charHeight).floor();
|
||||
|
||||
return Size(cols.toDouble(), rows.toDouble());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pinch-to-Zoom
|
||||
|
||||
```dart
|
||||
GestureDetector(
|
||||
onScaleStart: () => _baseFontSize = currentFontSize,
|
||||
onScaleUpdate: (details) {
|
||||
final newFontSize = _baseFontSize * details.scale;
|
||||
resize(newFontSize);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Farbschema
|
||||
|
||||
- **Hell (Light)**: Heller Hintergrund, dunkler Text
|
||||
- **Dunkel (Dark)**: Dunkler Hintergrund, heller Text
|
||||
- **AMOLED**: Rein schwarzer Hintergrund
|
||||
|
||||
## Leistungsoptimierungen
|
||||
|
||||
- **Dirty Rectangle**: Nur geänderte Regionen neu zeichnen
|
||||
- **Zeilen-Caching**: Gerenderte Zeilen cachen
|
||||
- **Lazy Scrolling**: Virtuelles Scrollen für lange Puffer
|
||||
- **Batch-Updates**: Mehrere Schreibvorgänge zusammenfassen
|
||||
- **Kompression**: Kompression des Scroll-Puffers
|
||||
- **Debouncing**: Debouncing für schnelle Eingaben
|
||||
45
docs/src/content/docs/de/quick-start.mdx
Normal file
45
docs/src/content/docs/de/quick-start.mdx
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Schnellstart
|
||||
description: In wenigen Minuten mit Server Box loslegen
|
||||
---
|
||||
|
||||
Folgen Sie dieser Schnellstartanleitung, um sich mit Ihrem ersten Server zu verbinden und mit der Überwachung zu beginnen.
|
||||
|
||||
## Schritt 1: Einen Server hinzufügen
|
||||
|
||||
1. Öffnen Sie Server Box
|
||||
2. Tippen Sie auf die Schaltfläche **+**, um einen neuen Server hinzuzufügen
|
||||
3. Geben Sie die Serverinformationen ein:
|
||||
- **Name**: Ein Anzeigename für Ihren Server
|
||||
- **Host**: IP-Adresse oder Domainname
|
||||
- **Port**: SSH-Port (Standard: 22)
|
||||
- **Benutzer**: SSH-Benutzername
|
||||
- **Passwort oder Schlüssel**: Authentifizierungsmethode
|
||||
|
||||
4. Tippen Sie auf **Speichern**, um den Server hinzuzufügen
|
||||
|
||||
## Schritt 2: Verbinden und Überwachen
|
||||
|
||||
1. Tippen Sie auf Ihre Serverkarte, um die Verbindung herzustellen
|
||||
2. Die App baut eine SSH-Verbindung auf
|
||||
3. Sie sehen den Echtzeit-Status für:
|
||||
- CPU-Auslastung
|
||||
- Arbeitsspeicher (RAM) und Swap
|
||||
- Festplattenbelegung
|
||||
- Netzwerkgeschwindigkeit
|
||||
|
||||
## Schritt 3: Funktionen erkunden
|
||||
|
||||
Sobald die Verbindung hergestellt ist, können Sie:
|
||||
|
||||
- **Terminal öffnen**: Tippen Sie auf die Terminal-Schaltfläche für vollen SSH-Zugriff
|
||||
- **Dateien durchsuchen**: Verwenden Sie SFTP, um Dateien zu verwalten
|
||||
- **Container verwalten**: Docker-Container anzeigen und steuern
|
||||
- **Prozesse anzeigen**: Laufende Prozesse überprüfen
|
||||
- **Snippets ausführen**: Gespeicherte Befehle ausführen
|
||||
|
||||
## Tipps
|
||||
|
||||
- **Biometrische Authentifizierung**: Aktivieren Sie Face ID / Touch ID / Fingerabdruck für schnellen Zugriff (Mobilgerät)
|
||||
- **Startbildschirm-Widgets**: Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm hinzu (iOS/Android)
|
||||
- **Hintergrundbetrieb**: Halten Sie Verbindungen im Hintergrund aktiv (Android)
|
||||
86
docs/src/content/docs/development/architecture.md
Normal file
86
docs/src/content/docs/development/architecture.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Architecture
|
||||
description: Architecture patterns and design decisions
|
||||
---
|
||||
|
||||
Server Box follows clean architecture principles with clear separation between data, domain, and presentation layers.
|
||||
|
||||
## Layered Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
│ (lib/view/page/) │
|
||||
│ - Pages, Widgets, Controllers │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Business Logic Layer │
|
||||
│ (lib/data/provider/) │
|
||||
│ - Riverpod Providers │
|
||||
│ - State Management │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Data Layer │
|
||||
│ (lib/data/model/, store/) │
|
||||
│ - Models, Storage, Services │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### State Management: Riverpod
|
||||
|
||||
- **Code Generation**: Uses `riverpod_generator` for type-safe providers
|
||||
- **State Notifiers**: For mutable state with business logic
|
||||
- **Async Notifiers**: For loading and error states
|
||||
- **Stream Providers**: For real-time data
|
||||
|
||||
### Immutable Models: Freezed
|
||||
|
||||
- All data models use Freezed for immutability
|
||||
- Union types for state representation
|
||||
- Built-in JSON serialization
|
||||
- CopyWith extensions for updates
|
||||
|
||||
### Local Storage: Hive
|
||||
|
||||
- **hive_ce**: Community edition of Hive
|
||||
- No manual `@HiveField` or `@HiveType` needed
|
||||
- Type adapters auto-generated
|
||||
- Persistent key-value storage
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Services and stores are injected via:
|
||||
|
||||
1. **Providers**: Expose dependencies to UI
|
||||
2. **GetIt**: Service location (where applicable)
|
||||
3. **Constructor Injection**: Explicit dependencies
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User Action → Widget → Provider → Service/Store → Model Update → UI Rebuild
|
||||
```
|
||||
|
||||
1. User interacts with widget
|
||||
2. Widget calls provider method
|
||||
3. Provider updates state via service/store
|
||||
3. State change triggers UI rebuild
|
||||
4. New state reflected in widget
|
||||
|
||||
## Custom Dependencies
|
||||
|
||||
The project uses several custom forks to extend functionality:
|
||||
|
||||
- **dartssh2**: Enhanced SSH features
|
||||
- **xterm**: Terminal emulator with mobile support
|
||||
- **fl_lib**: Shared UI components and utilities
|
||||
|
||||
## Threading
|
||||
|
||||
- **Isolates**: Heavy computation off main thread
|
||||
- **computer package**: Multi-threading utilities
|
||||
- **Async/Await**: Non-blocking I/O operations
|
||||
116
docs/src/content/docs/development/building.md
Normal file
116
docs/src/content/docs/development/building.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Building
|
||||
description: Build instructions for different platforms
|
||||
---
|
||||
|
||||
Server Box uses a custom build system (`fl_build`) for cross-platform builds.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Flutter SDK (stable channel)
|
||||
- Platform-specific tools (Xcode for iOS, Android Studio for Android)
|
||||
- Rust toolchain (for some native dependencies)
|
||||
|
||||
## Development Build
|
||||
|
||||
```bash
|
||||
# Run in development mode
|
||||
flutter run
|
||||
|
||||
# Run on specific device
|
||||
flutter run -d <device-id>
|
||||
```
|
||||
|
||||
## Production Build
|
||||
|
||||
The project uses `fl_build` for building:
|
||||
|
||||
```bash
|
||||
# Build for specific platform
|
||||
dart run fl_build -p <platform>
|
||||
|
||||
# Available platforms:
|
||||
# - ios
|
||||
# - android
|
||||
# - macos
|
||||
# - linux
|
||||
# - windows
|
||||
```
|
||||
|
||||
## Platform-Specific Builds
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p ios
|
||||
```
|
||||
|
||||
Requires:
|
||||
- macOS with Xcode
|
||||
- CocoaPods
|
||||
- Apple Developer account for signing
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
dart run fl_build -p android
|
||||
```
|
||||
|
||||
Requires:
|
||||
- Android SDK
|
||||
- Java Development Kit
|
||||
- Keystore for signing
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p macos
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
dart run fl_build -p linux
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
dart run fl_build -p windows
|
||||
```
|
||||
|
||||
Requires Windows with Visual Studio.
|
||||
|
||||
## Pre/Post Build
|
||||
|
||||
The `make.dart` script handles:
|
||||
|
||||
- Metadata generation
|
||||
- Version string updates
|
||||
- Platform-specific configurations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Clean Build
|
||||
|
||||
```bash
|
||||
flutter clean
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### Version Mismatch
|
||||
|
||||
Ensure all dependencies are compatible:
|
||||
```bash
|
||||
flutter pub upgrade
|
||||
```
|
||||
|
||||
## Release Checklist
|
||||
|
||||
1. Update version in `pubspec.yaml`
|
||||
2. Run code generation
|
||||
3. Run tests
|
||||
4. Build for all target platforms
|
||||
5. Test on physical devices
|
||||
6. Create GitHub release
|
||||
98
docs/src/content/docs/development/codegen.md
Normal file
98
docs/src/content/docs/development/codegen.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Code Generation
|
||||
description: Using build_runner for code generation
|
||||
---
|
||||
|
||||
Server Box heavily uses code generation for models, state management, and serialization.
|
||||
|
||||
## When to Run Code Generation
|
||||
|
||||
Run after modifying:
|
||||
|
||||
- Models with `@freezed` annotation
|
||||
- Classes with `@JsonSerializable`
|
||||
- Hive models
|
||||
- Providers with `@riverpod`
|
||||
- Localizations (ARB files)
|
||||
|
||||
## Running Code Generation
|
||||
|
||||
```bash
|
||||
# Generate all code
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Clean and regenerate
|
||||
dart run build_runner build --delete-conflicting-outputs --clean
|
||||
```
|
||||
|
||||
## Generated Files
|
||||
|
||||
### Freezed (`*.freezed.dart`)
|
||||
|
||||
Immutable data models with union types:
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
class ServerState with _$ServerState {
|
||||
const factory ServerState.connected() = Connected;
|
||||
const factory ServerState.disconnected() = Disconnected;
|
||||
const factory ServerState.error(String message) = Error;
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Serialization (`*.g.dart`)
|
||||
|
||||
Generated from `json_serializable`:
|
||||
|
||||
```dart
|
||||
@JsonSerializable()
|
||||
class Server {
|
||||
final String id;
|
||||
final String name;
|
||||
final String host;
|
||||
|
||||
Server({required this.id, required this.name, required this.host});
|
||||
|
||||
factory Server.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ServerToJson(this);
|
||||
}
|
||||
```
|
||||
|
||||
### Riverpod Providers (`*.g.dart`)
|
||||
|
||||
Generated from `@riverpod` annotation:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class MyNotifier extends _$MyNotifier {
|
||||
@override
|
||||
int build() => 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Hive Adapters (`*.g.dart`)
|
||||
|
||||
Auto-generated for Hive models (hive_ce):
|
||||
|
||||
```dart
|
||||
@HiveType(typeId: 0)
|
||||
class ServerModel {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
}
|
||||
```
|
||||
|
||||
## Localization Generation
|
||||
|
||||
```bash
|
||||
flutter gen-l10n
|
||||
```
|
||||
|
||||
Generates `lib/generated/l10n/` from `lib/l10n/*.arb` files.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `--delete-conflicting-outputs` to avoid conflicts
|
||||
- Add generated files to `.gitignore`
|
||||
- Never manually edit generated files
|
||||
115
docs/src/content/docs/development/state.md
Normal file
115
docs/src/content/docs/development/state.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: State Management
|
||||
description: Riverpod-based state management patterns
|
||||
---
|
||||
|
||||
Server Box uses Riverpod with code generation for state management.
|
||||
|
||||
## Provider Types
|
||||
|
||||
### StateProvider
|
||||
|
||||
Simple state that can be read and written:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class Settings extends _$Settings {
|
||||
@override
|
||||
SettingsModel build() {
|
||||
return SettingsModel.defaults();
|
||||
}
|
||||
|
||||
void update(SettingsModel newSettings) {
|
||||
state = newSettings;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AsyncNotifierProvider
|
||||
|
||||
State that loads asynchronously with loading/error states:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ServerStatus extends _$ServerStatus {
|
||||
@override
|
||||
Future<StatusModel> build(Server server) async {
|
||||
return fetchStatus(server);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => fetchStatus(server));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### StreamProvider
|
||||
|
||||
Real-time data from streams:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
|
||||
return cpuService.monitor(server);
|
||||
}
|
||||
```
|
||||
|
||||
## State Patterns
|
||||
|
||||
### Loading States
|
||||
|
||||
```dart
|
||||
state.when(
|
||||
data: (data) => DataWidget(data),
|
||||
loading: () => LoadingWidget(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### Family Providers
|
||||
|
||||
Parameterized providers:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
List<Container> containers(ContainersRef ref, Server server) {
|
||||
return containerService.list(server);
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Dispose
|
||||
|
||||
Providers that dispose when no longer referenced:
|
||||
|
||||
```dart
|
||||
@Riverpod(keepAlive: false)
|
||||
class TempState extends _$TempState {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use code generation**: Always use `@riverpod` annotation
|
||||
2. **Co-locate providers**: Place near consuming widgets
|
||||
3. **Avoid singletons**: Use providers instead
|
||||
4. **Layer correctly**: Keep UI logic separate from business logic
|
||||
|
||||
## Reading State in Widgets
|
||||
|
||||
```dart
|
||||
class ServerWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final status = ref.watch(serverStatusProvider(server));
|
||||
return status.when(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Modifying State
|
||||
|
||||
```dart
|
||||
ref.read(settingsProvider.notifier).update(newSettings);
|
||||
```
|
||||
96
docs/src/content/docs/development/structure.md
Normal file
96
docs/src/content/docs/development/structure.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Project Structure
|
||||
description: Understanding the Server Box codebase
|
||||
---
|
||||
|
||||
The Server Box project follows a modular architecture with clear separation of concerns.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # Core utilities and extensions
|
||||
├── data/ # Data layer
|
||||
│ ├── model/ # Data models by feature
|
||||
│ ├── provider/ # Riverpod providers
|
||||
│ └── store/ # Local storage (Hive)
|
||||
├── view/ # UI layer
|
||||
│ ├── page/ # Main pages
|
||||
│ └── widget/ # Reusable widgets
|
||||
├── generated/ # Generated localization
|
||||
├── l10n/ # Localization ARB files
|
||||
└── hive/ # Hive adapters
|
||||
```
|
||||
|
||||
## Core Layer (`lib/core/`)
|
||||
|
||||
Contains utilities, extensions, and routing configuration:
|
||||
|
||||
- **Extensions**: Dart extensions for common types
|
||||
- **Routes**: App routing configuration
|
||||
- **Utils**: Shared utility functions
|
||||
|
||||
## Data Layer (`lib/data/`)
|
||||
|
||||
### Models (`lib/data/model/`)
|
||||
|
||||
Organized by feature:
|
||||
|
||||
- `server/` - Server connection and status models
|
||||
- `container/` - Docker container models
|
||||
- `ssh/` - SSH session models
|
||||
- `sftp/` - SFTP file models
|
||||
- `app/` - App-specific models
|
||||
|
||||
### Providers (`lib/data/provider/`)
|
||||
|
||||
Riverpod providers for dependency injection and state management:
|
||||
|
||||
- Server providers
|
||||
- UI state providers
|
||||
- Service providers
|
||||
|
||||
### Stores (`lib/data/store/`)
|
||||
|
||||
Hive-based local storage:
|
||||
|
||||
- Server storage
|
||||
- Settings storage
|
||||
- Cache storage
|
||||
|
||||
## View Layer (`lib/view/`)
|
||||
|
||||
### Pages (`lib/view/page/`)
|
||||
|
||||
Main application screens:
|
||||
|
||||
- `server/` - Server management pages
|
||||
- `ssh/` - SSH terminal pages
|
||||
- `container/` - Container pages
|
||||
- `setting/` - Settings pages
|
||||
- `storage/` - SFTP pages
|
||||
- `snippet/` - Snippet pages
|
||||
|
||||
### Widgets (`lib/view/widget/`)
|
||||
|
||||
Reusable UI components:
|
||||
|
||||
- Server cards
|
||||
- Status charts
|
||||
- Input components
|
||||
- Dialogs
|
||||
|
||||
## Generated Files
|
||||
|
||||
- `lib/generated/l10n/` - Auto-generated localization
|
||||
- `*.g.dart` - Generated code (json_serializable, freezed, hive, riverpod)
|
||||
- `*.freezed.dart` - Freezed immutable classes
|
||||
|
||||
## Packages Directory (`/packages/`)
|
||||
|
||||
Contains custom forks of dependencies:
|
||||
|
||||
- `dartssh2/` - SSH library
|
||||
- `xterm/` - Terminal emulator
|
||||
- `fl_lib/` - Shared utilities
|
||||
- `fl_build/` - Build system
|
||||
113
docs/src/content/docs/development/testing.md
Normal file
113
docs/src/content/docs/development/testing.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Testing
|
||||
description: Testing strategies and running tests
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
flutter test
|
||||
|
||||
# Run specific test file
|
||||
flutter test test/battery_test.dart
|
||||
|
||||
# Run with coverage
|
||||
flutter test --coverage
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests are located in the `test/` directory mirroring the lib structure:
|
||||
|
||||
```
|
||||
test/
|
||||
├── data/
|
||||
│ ├── model/
|
||||
│ └── provider/
|
||||
├── view/
|
||||
│ └── widget/
|
||||
└── test_helpers.dart
|
||||
```
|
||||
|
||||
## Unit Tests
|
||||
|
||||
Test business logic and data models:
|
||||
|
||||
```dart
|
||||
test('should calculate CPU percentage', () {
|
||||
final cpu = CpuModel(usage: 75.0);
|
||||
expect(cpu.usagePercentage, '75%');
|
||||
});
|
||||
```
|
||||
|
||||
## Widget Tests
|
||||
|
||||
Test UI components:
|
||||
|
||||
```dart
|
||||
testWidgets('ServerCard displays server name', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
home: ServerCard(server: testServer),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Server'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Provider Tests
|
||||
|
||||
Test Riverpod providers:
|
||||
|
||||
```dart
|
||||
test('serverStatusProvider returns status', () async {
|
||||
final container = ProviderContainer();
|
||||
final status = await container.read(serverStatusProvider(testServer).future);
|
||||
expect(status, isA<StatusModel>());
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
Use mocks for external dependencies:
|
||||
|
||||
```dart
|
||||
class MockSshService extends Mock implements SshService {}
|
||||
|
||||
test('connects to server', () async {
|
||||
final mockSsh = MockSshService();
|
||||
when(mockSsh.connect(any)).thenAnswer((_) async => true);
|
||||
|
||||
// Test with mock
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Test complete user flows (in `integration_test/`):
|
||||
|
||||
```dart
|
||||
testWidgets('add server flow', (tester) async {
|
||||
await tester.pumpWidget(MyApp());
|
||||
|
||||
// Tap add button
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Fill form
|
||||
await tester.enterText(find.byKey(Key('name')), 'Test Server');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Arrange-Act-Assert**: Structure tests clearly
|
||||
2. **Descriptive names**: Test names should describe behavior
|
||||
3. **One assertion per test**: Keep tests focused
|
||||
4. **Mock external deps**: Don't depend on real servers
|
||||
5. **Test edge cases**: Empty lists, null values, etc.
|
||||
83
docs/src/content/docs/es/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/es/advanced/bulk-import.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Importación Masiva de Servidores
|
||||
description: Importar múltiples servidores desde un archivo JSON
|
||||
---
|
||||
|
||||
Importa múltiples configuraciones de servidor a la vez utilizando un archivo JSON.
|
||||
|
||||
## Formato JSON
|
||||
|
||||
:::danger[Advertencia de Seguridad]
|
||||
**¡Nunca guardes contraseñas en texto plano en archivos!** Este ejemplo JSON muestra un campo de contraseña solo con fines demostrativos, pero deberías:
|
||||
|
||||
- **Preferir claves SSH** (`keyId`) en lugar de `pwd`; son más seguras
|
||||
- **Usar gestores de secretos** o variables de entorno si debes usar contraseñas
|
||||
- **Eliminar el archivo inmediatamente** después de la importación; no dejes credenciales tiradas
|
||||
- **Añadir a .gitignore**: nunca subas archivos de credenciales al control de versiones
|
||||
:::
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Mi Servidor",
|
||||
"ip": "example.com",
|
||||
"port": 22,
|
||||
"user": "root",
|
||||
"pwd": "password",
|
||||
"keyId": "",
|
||||
"tags": ["production"],
|
||||
"autoConnect": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Campos
|
||||
|
||||
| Campo | Requerido | Descripción |
|
||||
|-------|-----------|-------------|
|
||||
| `name` | Sí | Nombre para mostrar |
|
||||
| `ip` | Sí | Dominio o dirección IP |
|
||||
| `port` | Sí | Puerto SSH (usualmente 22) |
|
||||
| `user` | Sí | Usuario SSH |
|
||||
| `pwd` | No | Contraseña (evitar - usar claves SSH en su lugar) |
|
||||
| `keyId` | No | Nombre de la clave SSH (de Claves Privadas - recomendado) |
|
||||
| `tags` | No | Etiquetas de organización |
|
||||
| `autoConnect` | No | Autoconexión al iniciar |
|
||||
|
||||
## Pasos para Importar
|
||||
|
||||
1. Crea un archivo JSON con las configuraciones del servidor
|
||||
2. Ajustes → Copia de seguridad → Importación masiva de servidores
|
||||
3. Selecciona tu archivo JSON
|
||||
4. Confirma la importación
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Producción",
|
||||
"ip": "prod.example.com",
|
||||
"port": 22,
|
||||
"user": "admin",
|
||||
"keyId": "mi-clave",
|
||||
"tags": ["production", "web"]
|
||||
},
|
||||
{
|
||||
"name": "Desarrollo",
|
||||
"ip": "dev.example.com",
|
||||
"port": 2222,
|
||||
"user": "dev",
|
||||
"keyId": "dev-clave",
|
||||
"tags": ["development"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Consejos
|
||||
|
||||
- **Usa claves SSH** en lugar de contraseñas cuando sea posible
|
||||
- **Prueba la conexión** después de la importación
|
||||
- **Organiza con etiquetas** para una gestión más sencilla
|
||||
- **Elimina el archivo JSON** después de la importación
|
||||
- **Nunca subas** archivos JSON con credenciales al control de versiones
|
||||
72
docs/src/content/docs/es/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/es/advanced/custom-commands.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Comandos Personalizados
|
||||
description: Mostrar la salida de comandos personalizados en la página del servidor
|
||||
---
|
||||
|
||||
Añade comandos shell personalizados para mostrar su salida en la página de detalles del servidor.
|
||||
|
||||
## Configuración
|
||||
|
||||
1. Ajustes del servidor → Comandos personalizados
|
||||
2. Introduce los comandos en formato JSON
|
||||
|
||||
## Formato Básico
|
||||
|
||||
```json
|
||||
{
|
||||
"Nombre a mostrar": "comando shell"
|
||||
}
|
||||
```
|
||||
|
||||
**Ejemplo:**
|
||||
```json
|
||||
{
|
||||
"Memoria": "free -h",
|
||||
"Disco": "df -h",
|
||||
"Tiempo de actividad": "uptime"
|
||||
}
|
||||
```
|
||||
|
||||
## Ver Resultados
|
||||
|
||||
Tras la configuración, los comandos personalizados aparecerán en la página de detalles del servidor y se actualizarán automáticamente.
|
||||
|
||||
## Nombres de Comando Especiales
|
||||
|
||||
### server_card_top_right
|
||||
|
||||
Se muestra en la tarjeta del servidor de la página de inicio (esquina superior derecha):
|
||||
|
||||
```json
|
||||
{
|
||||
"server_card_top_right": "tu-comando-aquí"
|
||||
}
|
||||
```
|
||||
|
||||
## Consejos
|
||||
|
||||
**Usa rutas absolutas:**
|
||||
```json
|
||||
{"Mi Script": "/usr/local/bin/mi-script.sh"}
|
||||
```
|
||||
|
||||
**Comandos con tuberías (pipes):**
|
||||
```json
|
||||
{"Proceso principal": "ps aux | sort -rk 3 | head -5"}
|
||||
```
|
||||
|
||||
**Formatear salida:**
|
||||
```json
|
||||
{"Carga de CPU": "uptime | awk -F'load average:' '{print $2}'"}
|
||||
```
|
||||
|
||||
**Mantén los comandos rápidos:** Menos de 5 segundos para una mejor experiencia.
|
||||
|
||||
**Limitar salida:**
|
||||
```json
|
||||
{"Logs": "tail -20 /var/log/syslog"}
|
||||
```
|
||||
|
||||
## Seguridad
|
||||
|
||||
Los comandos se ejecutan con los permisos del usuario SSH. Evita comandos que modifiquen el estado del sistema.
|
||||
54
docs/src/content/docs/es/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/es/advanced/custom-logo.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Logo de Servidor Personalizado
|
||||
description: Usa imágenes personalizadas para las tarjetas de servidor
|
||||
---
|
||||
|
||||
Muestra logos personalizados en las tarjetas de servidor mediante URLs de imagen.
|
||||
|
||||
## Configuración
|
||||
|
||||
1. Ajustes del servidor → Logo personalizado
|
||||
2. Introduce la URL de la imagen
|
||||
|
||||
## Marcadores de posición de URL
|
||||
|
||||
### {DIST} - Distribución Linux
|
||||
|
||||
Se reemplaza automáticamente por la distribución detectada:
|
||||
|
||||
```
|
||||
https://ejemplo.com/{DIST}.png
|
||||
```
|
||||
|
||||
Se convierte en: `debian.png`, `ubuntu.png`, `arch.png`, etc.
|
||||
|
||||
### {BRIGHT} - Tema
|
||||
|
||||
Se reemplaza automáticamente por el tema actual:
|
||||
|
||||
```
|
||||
https://ejemplo.com/{BRIGHT}.png
|
||||
```
|
||||
|
||||
Se convierte en: `light.png` o `dark.png`
|
||||
|
||||
### Combinar ambos
|
||||
|
||||
```
|
||||
https://ejemplo.com/{DIST}-{BRIGHT}.png
|
||||
```
|
||||
|
||||
Se convierte en: `debian-light.png`, `ubuntu-dark.png`, etc.
|
||||
|
||||
## Consejos
|
||||
|
||||
- Usa formatos PNG o SVG
|
||||
- Tamaño recomendado: de 64x64 a 128x128 píxeles
|
||||
- Usa URLs HTTPS
|
||||
- Mantén tamaños de archivo pequeños
|
||||
|
||||
## Distribuciones Soportadas
|
||||
|
||||
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
|
||||
|
||||
Lista completa: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)
|
||||
64
docs/src/content/docs/es/advanced/json-settings.md
Normal file
64
docs/src/content/docs/es/advanced/json-settings.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Ajustes Ocultos (JSON)
|
||||
description: Accede a ajustes avanzados mediante el editor JSON
|
||||
---
|
||||
|
||||
Algunos ajustes están ocultos en la interfaz de usuario pero son accesibles a través del editor JSON.
|
||||
|
||||
## Acceso
|
||||
|
||||
Mantén pulsado **Ajustes** en el menú lateral para abrir el editor JSON.
|
||||
|
||||
## Ajustes Ocultos Comunes
|
||||
|
||||
### timeOut
|
||||
|
||||
Tiempo de espera de conexión en segundos.
|
||||
|
||||
```json
|
||||
{"timeOut": 10}
|
||||
```
|
||||
|
||||
**Tipo:** entero | **Predeterminado:** 5 | **Rango:** 1-60
|
||||
|
||||
### recordHistory
|
||||
|
||||
Guardar historial (rutas SFTP, etc.).
|
||||
|
||||
```json
|
||||
{"recordHistory": true}
|
||||
```
|
||||
|
||||
**Tipo:** booleano | **Predeterminado:** true
|
||||
|
||||
### textFactor
|
||||
|
||||
Factor de escala de texto.
|
||||
|
||||
```json
|
||||
{"textFactor": 1.2}
|
||||
```
|
||||
|
||||
**Tipo:** doble | **Predeterminado:** 1.0 | **Rango:** 0.8-1.5
|
||||
|
||||
## Encontrar Más Ajustes
|
||||
|
||||
Todos los ajustes están definidos en [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).
|
||||
|
||||
Busca:
|
||||
```dart
|
||||
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
|
||||
```
|
||||
|
||||
## ⚠️ Importante
|
||||
|
||||
**Antes de editar:**
|
||||
- **Crea una copia de seguridad**: unos ajustes incorrectos pueden hacer que la app no se abra
|
||||
- **Edita con cuidado**: el JSON debe ser válido
|
||||
|
||||
## Recuperación
|
||||
|
||||
Si la aplicación no se abre tras editar:
|
||||
1. Borra los datos de la aplicación (último recurso)
|
||||
2. Reinstala la aplicación
|
||||
3. Restaura desde una copia de seguridad
|
||||
118
docs/src/content/docs/es/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/es/advanced/troubleshooting.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Problemas Comunes
|
||||
description: Soluciones a problemas frecuentes
|
||||
---
|
||||
|
||||
## Problemas de Conexión
|
||||
|
||||
### SSH no conecta
|
||||
|
||||
**Síntomas:** Tiempo de espera agotado (timeout), conexión rechazada, fallo de autenticación
|
||||
|
||||
**Soluciones:**
|
||||
|
||||
1. **Verificar el tipo de servidor:** Solo se admiten sistemas tipo Unix (Linux, macOS, Android/Termux)
|
||||
2. **Probar manualmente:** `ssh usuario@servidor -p puerto`
|
||||
3. **Comprobar el cortafuegos:** El puerto 22 debe estar abierto
|
||||
4. **Verificar credenciales:** Usuario y contraseña/clave correctos
|
||||
|
||||
### Desconexiones frecuentes
|
||||
|
||||
**Síntomas:** El terminal se desconecta tras un periodo de inactividad
|
||||
|
||||
**Soluciones:**
|
||||
|
||||
1. **Keep-alive del servidor:**
|
||||
```bash
|
||||
# /etc/ssh/sshd_config
|
||||
ClientAliveInterval 60
|
||||
ClientAliveCountMax 3
|
||||
```
|
||||
|
||||
2. **Desactivar optimización de batería:**
|
||||
- MIUI: Batería → "Sin restricciones"
|
||||
- Android: Ajustes → Aplicaciones → Desactivar optimización
|
||||
- iOS: Activar actualización en segundo plano
|
||||
|
||||
## Problemas de Entrada
|
||||
|
||||
### No se pueden escribir ciertos caracteres
|
||||
|
||||
**Solución:** Ajustes → Tipo de teclado → Cambiar a `visiblePassword`
|
||||
|
||||
Nota: Es posible que la entrada CJK (chino, japonés, coreano) no funcione tras este cambio.
|
||||
|
||||
## Problemas de la Aplicación
|
||||
|
||||
### La aplicación se cierra al iniciar
|
||||
|
||||
**Síntomas:** La aplicación no se abre, pantalla en negro
|
||||
|
||||
**Causas:** Ajustes corruptos, especialmente tras usar el editor JSON
|
||||
|
||||
**Soluciones:**
|
||||
|
||||
1. **Borrar datos de la aplicación:**
|
||||
- Android: Ajustes → Aplicaciones → ServerBox → Borrar datos
|
||||
- iOS: Eliminar y reinstalar
|
||||
|
||||
2. **Restaurar copia de seguridad:** Importar una copia de seguridad creada antes de cambiar los ajustes
|
||||
|
||||
### Problemas con Copia de Seguridad/Restauración
|
||||
|
||||
**La copia de seguridad no funciona:**
|
||||
- Comprobar espacio de almacenamiento
|
||||
- Verificar que la aplicación tiene permisos de almacenamiento
|
||||
- Probar una ubicación diferente
|
||||
|
||||
**La restauración falla:**
|
||||
- Verificar la integridad del archivo de copia de seguridad
|
||||
- Comprobar la compatibilidad de la versión de la aplicación
|
||||
|
||||
## Problemas con Widgets
|
||||
|
||||
### El Widget no se actualiza
|
||||
|
||||
**iOS:**
|
||||
- Esperar hasta 30 minutos para la actualización automática
|
||||
- Eliminar y volver a añadir el widget
|
||||
- Comprobar que la URL termina en `/status`
|
||||
|
||||
**Android:**
|
||||
- Pulsar el widget para forzar la actualización
|
||||
- Verificar que el ID del widget coincide con la configuración en los ajustes de la aplicación
|
||||
|
||||
**watchOS:**
|
||||
- Reiniciar la aplicación del reloj
|
||||
- Esperar unos minutos tras cambiar la configuración
|
||||
- Verificar el formato de la URL
|
||||
|
||||
### El Widget muestra un error
|
||||
|
||||
- Verificar que ServerBox Monitor se está ejecutando en el servidor
|
||||
- Probar la URL en un navegador
|
||||
- Comprobar las credenciales de autenticación
|
||||
|
||||
## Problemas de Rendimiento
|
||||
|
||||
### La aplicación va lenta
|
||||
|
||||
**Soluciones:**
|
||||
- Reducir la tasa de refresco en los ajustes
|
||||
- Comprobar la velocidad de la red
|
||||
- Desactivar servidores no utilizados
|
||||
|
||||
### Alto consumo de batería
|
||||
|
||||
**Soluciones:**
|
||||
- Aumentar los intervalos de refresco
|
||||
- Desactivar la actualización en segundo plano
|
||||
- Cerrar sesiones SSH no utilizadas
|
||||
|
||||
## Obtener Ayuda
|
||||
|
||||
Si los problemas persisten:
|
||||
|
||||
1. **Buscar en GitHub Issues:** https://github.com/lollipopkit/flutter_server_box/issues
|
||||
2. **Crear nueva Issue:** Incluir versión de la aplicación, plataforma y pasos para reproducir
|
||||
3. **Consultar la Wiki:** Esta documentación y la Wiki de GitHub
|
||||
90
docs/src/content/docs/es/advanced/widgets.md
Normal file
90
docs/src/content/docs/es/advanced/widgets.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Widgets de Pantalla de Inicio
|
||||
description: Añade widgets de estado del servidor a tu pantalla de inicio
|
||||
---
|
||||
|
||||
Requiere tener instalado [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) en tus servidores.
|
||||
|
||||
## Requisitos Previos
|
||||
|
||||
Instala primero ServerBox Monitor en tu servidor. Consulta la [Wiki de ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor/wiki/Home) para ver las instrucciones de configuración.
|
||||
|
||||
Tras la instalación, tu servidor debería tener:
|
||||
- Un punto de acceso (endpoint) HTTP/HTTPS
|
||||
- El punto de acceso API `/status`
|
||||
- Autenticación opcional
|
||||
|
||||
## Formato de URL
|
||||
|
||||
```
|
||||
https://tu-servidor.com/status
|
||||
```
|
||||
|
||||
Debe terminar en `/status`.
|
||||
|
||||
## Widget de iOS
|
||||
|
||||
### Configuración
|
||||
|
||||
1. Mantén pulsada la pantalla de inicio → Toca el símbolo **+**
|
||||
2. Busca "ServerBox"
|
||||
3. Elige el tamaño del widget
|
||||
4. Mantén pulsado el widget → **Editar widget**
|
||||
5. Introduce la URL terminada en `/status`
|
||||
|
||||
### Notas
|
||||
|
||||
- Debe usar HTTPS (excepto IPs locales)
|
||||
- Tasa máxima de refresco: 30 minutos (límite de iOS)
|
||||
- Añade varios widgets para varios servidores
|
||||
|
||||
## Widget de Android
|
||||
|
||||
### Configuración
|
||||
|
||||
1. Mantén pulsada la pantalla de inicio → **Widgets**
|
||||
2. Busca "ServerBox" → Añadir a la pantalla de inicio
|
||||
3. Anota el número de ID del widget que aparece
|
||||
4. Abre la app ServerBox → Ajustes
|
||||
5. Toca en **Configurar enlace de widget de inicio**
|
||||
6. Añade la entrada: `Widget ID` = `URL de estado`
|
||||
|
||||
Ejemplo:
|
||||
- Clave (Key): `17`
|
||||
- Valor (Value): `https://mi-servidor.com/status`
|
||||
|
||||
7. Toca el widget en la pantalla de inicio para refrescarlo
|
||||
|
||||
## Widget de watchOS
|
||||
|
||||
### Configuración
|
||||
|
||||
1. Abre la app en el iPhone → Ajustes
|
||||
2. **Ajustes de iOS** → **App del Watch**
|
||||
3. Toca en **Añadir URL**
|
||||
4. Introduce la URL terminada en `/status`
|
||||
5. Espera a que la app del reloj se sincronice
|
||||
|
||||
### Notas
|
||||
|
||||
- Prueba a reiniciar la app del reloj si no se actualiza
|
||||
- Verifica que el teléfono y el reloj están conectados
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### El Widget no se actualiza
|
||||
|
||||
**iOS:** Espera hasta 30 minutos, luego elimínalo y vuelve a añadirlo.
|
||||
**Android:** Toca el widget para forzar el refresco, verifica el ID en los ajustes.
|
||||
**watchOS:** Reinicia la app del reloj, espera unos minutos.
|
||||
|
||||
### El Widget muestra un error
|
||||
|
||||
- Verifica que ServerBox Monitor se está ejecutando
|
||||
- Prueba la URL en un navegador
|
||||
- Comprueba que la URL termina en `/status`
|
||||
|
||||
## Seguridad
|
||||
|
||||
- **Usa siempre HTTPS** cuando sea posible
|
||||
- **IPs locales solo** en redes de confianza
|
||||
86
docs/src/content/docs/es/development/architecture.md
Normal file
86
docs/src/content/docs/es/development/architecture.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Arquitectura
|
||||
description: Patrones de arquitectura y decisiones de diseño
|
||||
---
|
||||
|
||||
Server Box sigue los principios de Clean Architecture con una clara separación entre las capas de datos, dominio y presentación.
|
||||
|
||||
## Arquitectura por Capas
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Capa de Presentación │
|
||||
│ (lib/view/page/) │
|
||||
│ - Páginas, Widgets, Controladores │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Capa de Lógica de Negocio │
|
||||
│ (lib/data/provider/) │
|
||||
│ - Riverpod Providers │
|
||||
│ - Gestión de Estado │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Capa de Datos │
|
||||
│ (lib/data/model/, store/) │
|
||||
│ - Modelos, Almacén, Servicios │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Patrones Clave
|
||||
|
||||
### Gestión de Estado: Riverpod
|
||||
|
||||
- **Generación de Código**: Usa `riverpod_generator` para providers con tipado seguro
|
||||
- **State Notifiers**: Para estados mutables con lógica de negocio
|
||||
- **Async Notifiers**: Para estados de carga y error
|
||||
- **Stream Providers**: Para datos en tiempo real
|
||||
|
||||
### Modelos Inmutables: Freezed
|
||||
|
||||
- Todos los modelos de datos usan Freezed para inmutabilidad
|
||||
- Tipos Union para representación de estados
|
||||
- Serialización JSON integrada
|
||||
- Extensiones CopyWith para actualizaciones
|
||||
|
||||
### Almacenamiento Local: Hive
|
||||
|
||||
- **hive_ce**: Edición comunitaria de Hive
|
||||
- No se requiere `@HiveField` o `@HiveType` manual
|
||||
- Adaptadores de tipo generados automáticamente
|
||||
- Almacenamiento persistente clave-valor
|
||||
|
||||
## Inyección de Dependencias
|
||||
|
||||
Los servicios y almacenes se inyectan a través de:
|
||||
|
||||
1. **Providers**: Exponen dependencias a la UI
|
||||
2. **GetIt**: Localizador de servicios (donde sea aplicable)
|
||||
3. **Inyección en Constructor**: Dependencias explícitas
|
||||
|
||||
## Flujo de Datos
|
||||
|
||||
```
|
||||
Acción de Usuario → Widget → Provider → Servicio/Almacén → Actualización de Modelo → Reconstrucción de UI
|
||||
```
|
||||
|
||||
1. El usuario interactúa con el widget
|
||||
2. El widget llama al método del provider
|
||||
3. El provider actualiza el estado a través del servicio/almacén
|
||||
4. El cambio de estado activa la reconstrucción de la UI
|
||||
5. El nuevo estado se refleja en el widget
|
||||
|
||||
## Dependencias Personalizadas
|
||||
|
||||
El proyecto utiliza varias ramas (forks) personalizadas para extender la funcionalidad:
|
||||
|
||||
- **dartssh2**: Funciones SSH mejoradas
|
||||
- **xterm**: Emulador de terminal con soporte móvil
|
||||
- **fl_lib**: Componentes de UI y utilidades compartidas
|
||||
|
||||
## Multihilo
|
||||
|
||||
- **Isolates**: Computación pesada fuera del hilo principal
|
||||
- **paquete computer**: Utilidades para multihilo
|
||||
- **Async/Await**: Operaciones de E/S no bloqueantes
|
||||
116
docs/src/content/docs/es/development/building.md
Normal file
116
docs/src/content/docs/es/development/building.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Compilación
|
||||
description: Instrucciones de compilación para diferentes plataformas
|
||||
---
|
||||
|
||||
Server Box utiliza un sistema de compilación personalizado (`fl_build`) para compilaciones multiplataforma.
|
||||
|
||||
## Requisitos Previos
|
||||
|
||||
- Flutter SDK (canal stable)
|
||||
- Herramientas específicas de cada plataforma (Xcode para iOS, Android Studio para Android)
|
||||
- Cadena de herramientas de Rust (para algunas dependencias nativas)
|
||||
|
||||
## Compilación de Desarrollo
|
||||
|
||||
```bash
|
||||
# Ejecutar en modo desarrollo
|
||||
flutter run
|
||||
|
||||
# Ejecutar en un dispositivo específico
|
||||
flutter run -d <id-del-dispositivo>
|
||||
```
|
||||
|
||||
## Compilación de Producción
|
||||
|
||||
El proyecto utiliza `fl_build` para compilar:
|
||||
|
||||
```bash
|
||||
# Compilar para una plataforma específica
|
||||
dart run fl_build -p <plataforma>
|
||||
|
||||
# Plataformas disponibles:
|
||||
# - ios
|
||||
# - android
|
||||
# - macos
|
||||
# - linux
|
||||
# - windows
|
||||
```
|
||||
|
||||
## Compilaciones Específicas por Plataforma
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p ios
|
||||
```
|
||||
|
||||
Requiere:
|
||||
- macOS con Xcode
|
||||
- CocoaPods
|
||||
- Cuenta de Apple Developer para la firma
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
dart run fl_build -p android
|
||||
```
|
||||
|
||||
Requiere:
|
||||
- Android SDK
|
||||
- Java Development Kit
|
||||
- Keystore para la firma
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p macos
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
dart run fl_build -p linux
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
dart run fl_build -p windows
|
||||
```
|
||||
|
||||
Requiere Windows con Visual Studio.
|
||||
|
||||
## Pre/Post Compilación
|
||||
|
||||
El script `make.dart` se encarga de:
|
||||
|
||||
- Generación de metadatos
|
||||
- Actualización de cadenas de versión
|
||||
- Configuraciones específicas de plataforma
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### Compilación Limpia
|
||||
|
||||
```bash
|
||||
flutter clean
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### Discrepancia de Versión
|
||||
|
||||
Asegúrate de que todas las dependencias son compatibles:
|
||||
```bash
|
||||
flutter pub upgrade
|
||||
```
|
||||
|
||||
## Lista de Verificación de Lanzamiento
|
||||
|
||||
1. Actualizar la versión en `pubspec.yaml`
|
||||
2. Ejecutar la generación de código
|
||||
3. Ejecutar las pruebas
|
||||
4. Compilar para todas las plataformas de destino
|
||||
5. Probar en dispositivos físicos
|
||||
6. Crear lanzamiento (release) en GitHub
|
||||
98
docs/src/content/docs/es/development/codegen.md
Normal file
98
docs/src/content/docs/es/development/codegen.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Generación de Código
|
||||
description: Uso de build_runner para la generación de código
|
||||
---
|
||||
|
||||
Server Box utiliza intensivamente la generación de código para modelos, gestión de estado y serialización.
|
||||
|
||||
## Cuándo Ejecutar la Generación de Código
|
||||
|
||||
Ejecutar tras modificar:
|
||||
|
||||
- Modelos con la anotación `@freezed`
|
||||
- Clases con `@JsonSerializable`
|
||||
- Modelos de Hive
|
||||
- Providers con `@riverpod`
|
||||
- Localizaciones (archivos ARB)
|
||||
|
||||
## Ejecutar la Generación de Código
|
||||
|
||||
```bash
|
||||
# Generar todo el código
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Limpiar y regenerar
|
||||
dart run build_runner build --delete-conflicting-outputs --clean
|
||||
```
|
||||
|
||||
## Archivos Generados
|
||||
|
||||
### Freezed (`*.freezed.dart`)
|
||||
|
||||
Modelos de datos inmutables con tipos Union:
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
class ServerState with _$ServerState {
|
||||
const factory ServerState.connected() = Connected;
|
||||
const factory ServerState.disconnected() = Disconnected;
|
||||
const factory ServerState.error(String message) = Error;
|
||||
}
|
||||
```
|
||||
|
||||
### Serialización JSON (`*.g.dart`)
|
||||
|
||||
Generado por `json_serializable`:
|
||||
|
||||
```dart
|
||||
@JsonSerializable()
|
||||
class Server {
|
||||
final String id;
|
||||
final String name;
|
||||
final String host;
|
||||
|
||||
Server({required this.id, required this.name, required this.host});
|
||||
|
||||
factory Server.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ServerToJson(this);
|
||||
}
|
||||
```
|
||||
|
||||
### Providers de Riverpod (`*.g.dart`)
|
||||
|
||||
Generados a partir de la anotación `@riverpod`:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class MyNotifier extends _$MyNotifier {
|
||||
@override
|
||||
int build() => 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Adaptadores de Hive (`*.g.dart`)
|
||||
|
||||
Auto-generados para modelos de Hive (hive_ce):
|
||||
|
||||
```dart
|
||||
@HiveType(typeId: 0)
|
||||
class ServerModel {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
}
|
||||
```
|
||||
|
||||
## Generación de Localización
|
||||
|
||||
```bash
|
||||
flutter gen-l10n
|
||||
```
|
||||
|
||||
Genera `lib/generated/l10n/` a partir de los archivos `lib/l10n/*.arb`.
|
||||
|
||||
## Consejos
|
||||
|
||||
- Usa `--delete-conflicting-outputs` para evitar conflictos
|
||||
- Añade los archivos generados al `.gitignore`
|
||||
- Nunca edites manualmente los archivos generados
|
||||
115
docs/src/content/docs/es/development/state.md
Normal file
115
docs/src/content/docs/es/development/state.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Gestión de Estado
|
||||
description: Patrones de gestión de estado basados en Riverpod
|
||||
---
|
||||
|
||||
Server Box utiliza Riverpod con generación de código para la gestión de estado.
|
||||
|
||||
## Tipos de Provider
|
||||
|
||||
### StateProvider
|
||||
|
||||
Estado simple que se puede leer y escribir:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class Settings extends _$Settings {
|
||||
@override
|
||||
SettingsModel build() {
|
||||
return SettingsModel.defaults();
|
||||
}
|
||||
|
||||
void update(SettingsModel newSettings) {
|
||||
state = newSettings;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AsyncNotifierProvider
|
||||
|
||||
Estado que se carga de forma asíncrona con estados de carga/error:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ServerStatus extends _$ServerStatus {
|
||||
@override
|
||||
Future<StatusModel> build(Server server) async {
|
||||
return fetchStatus(server);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => fetchStatus(server));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### StreamProvider
|
||||
|
||||
Datos en tiempo real desde flujos (streams):
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
|
||||
return cpuService.monitor(server);
|
||||
}
|
||||
```
|
||||
|
||||
## Patrones de Estado
|
||||
|
||||
### Estados de Carga
|
||||
|
||||
```dart
|
||||
state.when(
|
||||
data: (data) => DataWidget(data),
|
||||
loading: () => LoadingWidget(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### Family Providers
|
||||
|
||||
Providers parametrizados:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
List<Container> containers(ContainersRef ref, Server server) {
|
||||
return containerService.list(server);
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Dispose
|
||||
|
||||
Providers que se eliminan cuando ya no están referenciados:
|
||||
|
||||
```dart
|
||||
@Riverpod(keepAlive: false)
|
||||
class TempState extends _$TempState {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Mejores Prácticas
|
||||
|
||||
1. **Usar generación de código**: Usa siempre la anotación `@riverpod`
|
||||
2. **Co-localizar providers**: Ponlos cerca de los widgets que los consumen
|
||||
3. **Evitar singletons**: Usa providers en su lugar
|
||||
4. **Capas correctas**: Mantén la lógica de UI separada de la lógica de negocio
|
||||
|
||||
## Leer el Estado en Widgets
|
||||
|
||||
```dart
|
||||
class ServerWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final status = ref.watch(serverStatusProvider(server));
|
||||
return status.when(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Modificar el Estado
|
||||
|
||||
```dart
|
||||
ref.read(settingsProvider.notifier).update(newSettings);
|
||||
```
|
||||
96
docs/src/content/docs/es/development/structure.md
Normal file
96
docs/src/content/docs/es/development/structure.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Estructura del Proyecto
|
||||
description: Comprendiendo la base de código de Server Box
|
||||
---
|
||||
|
||||
El proyecto Server Box sigue una arquitectura modular con una clara separación de responsabilidades.
|
||||
|
||||
## Estructura de Directorios
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # Utilidades centrales y extensiones
|
||||
├── data/ # Capa de datos
|
||||
│ ├── model/ # Modelos de datos por función
|
||||
│ ├── provider/ # Riverpod providers
|
||||
│ └── store/ # Almacenamiento local (Hive)
|
||||
├── view/ # Capa de UI
|
||||
│ ├── page/ # Páginas principales
|
||||
│ └── widget/ # Widgets reutilizables
|
||||
├── generated/ # Localización generada
|
||||
├── l10n/ # Archivos ARB de localización
|
||||
└── hive/ # Adaptadores de Hive
|
||||
```
|
||||
|
||||
## Capa Central (`lib/core/`)
|
||||
|
||||
Contiene utilidades, extensiones y configuración de rutas:
|
||||
|
||||
- **Extensions**: Extensiones de Dart para tipos comunes
|
||||
- **Routes**: Configuración de rutas de la app
|
||||
- **Utils**: Funciones de utilidad compartidas
|
||||
|
||||
## Capa de Datos (`lib/data/`)
|
||||
|
||||
### Modelos (`lib/data/model/`)
|
||||
|
||||
Organizados por función:
|
||||
|
||||
- `server/` - Modelos de conexión y estado del servidor
|
||||
- `container/` - Modelos de contenedores Docker
|
||||
- `ssh/` - Modelos de sesión SSH
|
||||
- `sftp/` - Modelos de archivos SFTP
|
||||
- `app/` - Modelos específicos de la app
|
||||
|
||||
### Providers (`lib/data/provider/`)
|
||||
|
||||
Providers de Riverpod para inyección de dependencias y gestión de estado:
|
||||
|
||||
- Providers de servidor
|
||||
- Providers de estado de UI
|
||||
- Providers de servicios
|
||||
|
||||
### Almacenes (`lib/data/store/`)
|
||||
|
||||
Almacenamiento local basado en Hive:
|
||||
|
||||
- Almacén de servidores
|
||||
- Almacén de ajustes
|
||||
- Almacén de caché
|
||||
|
||||
## Capa de Vista (`lib/view/`)
|
||||
|
||||
### Páginas (`lib/view/page/`)
|
||||
|
||||
Pantallas principales de la aplicación:
|
||||
|
||||
- `server/` - Páginas de gestión de servidores
|
||||
- `ssh/` - Páginas de terminal SSH
|
||||
- `container/` - Páginas de contenedores
|
||||
- `setting/` - Páginas de ajustes
|
||||
- `storage/` - Páginas de SFTP
|
||||
- `snippet/` - Páginas de fragmentos (snippets)
|
||||
|
||||
### Widgets (`lib/view/widget/`)
|
||||
|
||||
Componentes de UI reutilizables:
|
||||
|
||||
- Tarjetas de servidor
|
||||
- Gráficos de estado
|
||||
- Componentes de entrada
|
||||
- Diálogos
|
||||
|
||||
## Archivos Generados
|
||||
|
||||
- `lib/generated/l10n/` - Localización auto-generada
|
||||
- `*.g.dart` - Código generado (json_serializable, freezed, hive, riverpod)
|
||||
- `*.freezed.dart` - Clases inmutables de Freezed
|
||||
|
||||
## Directorio de Paquetes (`/packages/`)
|
||||
|
||||
Contiene ramas (forks) personalizadas de las dependencias:
|
||||
|
||||
- `dartssh2/` - Librería SSH
|
||||
- `xterm/` - Emulador de terminal
|
||||
- `fl_lib/` - Utilidades compartidas
|
||||
- `fl_build/` - Sistema de compilación
|
||||
113
docs/src/content/docs/es/development/testing.md
Normal file
113
docs/src/content/docs/es/development/testing.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Pruebas
|
||||
description: Estrategias de prueba y ejecución de pruebas
|
||||
---
|
||||
|
||||
## Ejecución de Pruebas
|
||||
|
||||
```bash
|
||||
# Ejecutar todas las pruebas
|
||||
flutter test
|
||||
|
||||
# Ejecutar un archivo de prueba específico
|
||||
flutter test test/battery_test.dart
|
||||
|
||||
# Ejecutar con cobertura
|
||||
flutter test --coverage
|
||||
```
|
||||
|
||||
## Estructura de las Pruebas
|
||||
|
||||
Las pruebas se encuentran en el directorio `test/` reflejando la estructura de lib:
|
||||
|
||||
```
|
||||
test/
|
||||
├── data/
|
||||
│ ├── model/
|
||||
│ └── provider/
|
||||
├── view/
|
||||
│ └── widget/
|
||||
└── test_helpers.dart
|
||||
```
|
||||
|
||||
## Pruebas Unitarias
|
||||
|
||||
Probar la lógica de negocio y los modelos de datos:
|
||||
|
||||
```dart
|
||||
test('debería calcular el porcentaje de CPU', () {
|
||||
final cpu = CpuModel(usage: 75.0);
|
||||
expect(cpu.usagePercentage, '75%');
|
||||
});
|
||||
```
|
||||
|
||||
## Pruebas de Widgets
|
||||
|
||||
Probar componentes de la interfaz de usuario (UI):
|
||||
|
||||
```dart
|
||||
testWidgets('ServerCard muestra el nombre del servidor', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
home: ServerCard(server: testServer),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Server'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Pruebas de Providers
|
||||
|
||||
Probar providers de Riverpod:
|
||||
|
||||
```dart
|
||||
test('serverStatusProvider devuelve el estado', () async {
|
||||
final container = ProviderContainer();
|
||||
final status = await container.read(serverStatusProvider(testServer).future);
|
||||
expect(status, isA<StatusModel>());
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking (Simulaciones)
|
||||
|
||||
Utilizar mocks para dependencias externas:
|
||||
|
||||
```dart
|
||||
class MockSshService extends Mock implements SshService {}
|
||||
|
||||
test('se conecta al servidor', () async {
|
||||
final mockSsh = MockSshService();
|
||||
when(mockSsh.connect(any)).thenAnswer((_) async => true);
|
||||
|
||||
// Probar con el mock
|
||||
});
|
||||
```
|
||||
|
||||
## Pruebas de Integración
|
||||
|
||||
Probar flujos de usuario completos (en `integration_test/`):
|
||||
|
||||
```dart
|
||||
testWidgets('flujo de agregar servidor', (tester) async {
|
||||
await tester.pumpWidget(MyApp());
|
||||
|
||||
// Tocar el botón de agregar
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Completar el formulario
|
||||
await tester.enterText(find.byKey(Key('name')), 'Test Server');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Buenas Prácticas
|
||||
|
||||
1. **Arrange-Act-Assert**: Estructurar las pruebas claramente.
|
||||
2. **Nombres descriptivos**: Los nombres de las pruebas deben describir el comportamiento.
|
||||
3. **Una aserción por prueba**: Mantener las pruebas enfocadas.
|
||||
4. **Simular dependencias externas**: No depender de servidores reales.
|
||||
5. **Probar casos límite**: Listas vacías, valores nulos, etc.
|
||||
46
docs/src/content/docs/es/index.mdx
Normal file
46
docs/src/content/docs/es/index.mdx
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Server Box
|
||||
description: Una aplicación integral de gestión de servidores multiplataforma
|
||||
hero:
|
||||
tagline: Administra tus servidores Linux desde cualquier lugar
|
||||
actions:
|
||||
- text: Empezar
|
||||
link: /es/introduction/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: Ver en GitHub
|
||||
link: https://github.com/lollipopkit/flutter_server_box
|
||||
icon: github
|
||||
variant: minimal
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
## Características
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Monitoreo en Tiempo Real" icon="chart">
|
||||
Monitorea CPU, memoria, disco, red, GPU y temperatura con hermosos gráficos en tiempo real.
|
||||
</Card>
|
||||
<Card title="Terminal SSH" icon="terminal">
|
||||
Terminal SSH con todas las funciones, soporte para múltiples pestañas y teclado virtual para dispositivos móviles.
|
||||
</Card>
|
||||
<Card title="Navegador de Archivos SFTP" icon="folder">
|
||||
Administra archivos en tus servidores con el cliente SFTP integrado y el navegador de archivos local.
|
||||
</Card>
|
||||
<Card title="Gestión de Docker" icon="box">
|
||||
Inicia, detén y monitorea contenedores Docker con una interfaz intuitiva.
|
||||
</Card>
|
||||
<Card title="Multiplataforma" icon="device-mobile">
|
||||
Disponible en iOS, Android, macOS, Linux, Windows y watchOS.
|
||||
</Card>
|
||||
<Card title="Más de 12 Idiomas" icon="globe">
|
||||
Soporte completo de localización que incluye inglés, chino, alemán, francés y más.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Enlaces Rápidos
|
||||
|
||||
- **Descarga**: Disponible en [App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) y [F-Droid](https://f-droid.org/)
|
||||
- **Documentación**: Explora las guías para comenzar con Server Box
|
||||
- **Soporte**: Únete a nuestra comunidad en GitHub para discusiones y problemas
|
||||
51
docs/src/content/docs/es/installation.mdx
Normal file
51
docs/src/content/docs/es/installation.mdx
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Instalación
|
||||
description: Descarga e instala Server Box en tu dispositivo
|
||||
---
|
||||
|
||||
Server Box está disponible en múltiples plataformas. Elige tu método de instalación preferido.
|
||||
|
||||
## Aplicaciones Móviles
|
||||
|
||||
### iOS
|
||||
|
||||
Descárgalo desde la **[App Store](https://apps.apple.com/app/id1586449703)**.
|
||||
|
||||
### Android
|
||||
|
||||
Elige tu fuente preferida:
|
||||
|
||||
- **[F-Droid](https://f-droid.org/)** - Para usuarios que prefieren fuentes exclusivamente FOSS (Software Libre y de Código Abierto)
|
||||
- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - Para la última versión directamente desde la fuente
|
||||
|
||||
## Aplicaciones de Escritorio
|
||||
|
||||
### macOS
|
||||
|
||||
Descárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||
|
||||
Características:
|
||||
- Integración nativa con la barra de menú
|
||||
- Soporte para Intel y Apple Silicon
|
||||
|
||||
### Linux
|
||||
|
||||
Descárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||
|
||||
Disponible en paquetes AppImage, deb o tar.gz.
|
||||
|
||||
### Windows
|
||||
|
||||
Descárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||
|
||||
## watchOS
|
||||
|
||||
Disponible en la **[App Store](https://apps.apple.com/app/id1586449703)** como parte de la aplicación para iOS.
|
||||
|
||||
## Compilación desde el Código Fuente
|
||||
|
||||
Para compilar Server Box desde el código fuente, consulta la sección de [Compilación](/es/development/building/) en la documentación de desarrollo.
|
||||
|
||||
## Información de Versión
|
||||
|
||||
Consulta la página de [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) para ver la última versión y el registro de cambios.
|
||||
32
docs/src/content/docs/es/introduction.mdx
Normal file
32
docs/src/content/docs/es/introduction.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Introducción
|
||||
description: Aprende qué es Server Box y qué puede hacer
|
||||
---
|
||||
|
||||
Server Box es una aplicación integral de gestión de servidores multiplataforma creada con Flutter. Te permite monitorear, gestionar y controlar tus servidores Linux, Unix y Windows desde cualquier lugar.
|
||||
|
||||
## ¿Qué es Server Box?
|
||||
|
||||
Server Box proporciona una interfaz unificada para tareas de administración de servidores a través de conexiones SSH. Ya seas un administrador de sistemas, desarrollador o entusiasta con servidores domésticos, esta aplicación pone potentes herramientas de gestión de servidores en tu bolsillo.
|
||||
|
||||
## Capacidades Clave
|
||||
|
||||
- **Monitoreo en Tiempo Real**: Sigue el uso de CPU, memoria, disco, velocidad de red, estado de GPU y temperaturas del sistema.
|
||||
- **Terminal SSH**: Acceso total a la terminal con soporte multi-pestaña y apariencia personalizable.
|
||||
- **Cliente SFTP**: Explora y gestiona archivos en tus servidores.
|
||||
- **Gestión de Docker**: Controla contenedores con facilidad.
|
||||
- **Gestión de Procesos**: Visualiza y gestiona procesos del sistema.
|
||||
- **Servicios Systemd**: Inicia, detén y monitorea servicios systemd.
|
||||
- **Herramientas de Red**: Pruebas iPerf, ping y Wake-on-LAN.
|
||||
- **Snippets**: Guarda y ejecuta comandos de shell personalizados.
|
||||
|
||||
## Plataformas Soportadas
|
||||
|
||||
Server Box es verdaderamente multiplataforma:
|
||||
|
||||
- **Móvil**: iOS y Android
|
||||
- **Escritorio**: macOS, Linux y Windows
|
||||
|
||||
## Licencia
|
||||
|
||||
Este proyecto está bajo la licencia AGPL v3. El código fuente está disponible en [GitHub](https://github.com/lollipopkit/flutter_server_box).
|
||||
80
docs/src/content/docs/es/platforms/desktop.md
Normal file
80
docs/src/content/docs/es/platforms/desktop.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Funciones de Escritorio
|
||||
description: Funciones específicas para macOS, Linux y Windows
|
||||
---
|
||||
|
||||
Server Box en plataformas de escritorio ofrece funciones de productividad adicionales.
|
||||
|
||||
## macOS
|
||||
|
||||
### Integración en la Barra de Menús
|
||||
|
||||
- Estado rápido del servidor en la barra de menús
|
||||
- Acceso al servidor con un solo clic
|
||||
- Modo compacto para una mínima distracción
|
||||
- Estilo nativo de la barra de menús de macOS
|
||||
|
||||
### Persistencia del Estado de la Ventana
|
||||
|
||||
- Recuerda la posición y el tamaño de la ventana
|
||||
- Restaura la sesión anterior al iniciar
|
||||
- Soporte para múltiples monitores
|
||||
|
||||
### Funciones Nativas
|
||||
|
||||
- **Barra de título**: Opción de barra de título personalizada o del sistema
|
||||
- **Modo pantalla completa**: Monitorización dedicada del servidor
|
||||
- **Atajos de teclado**: Atajos nativos de macOS
|
||||
- **Touch Bar** (dispositivos compatibles): Acciones rápidas
|
||||
|
||||
## Linux
|
||||
|
||||
### Integración Nativa
|
||||
|
||||
- Soporte para bandeja del sistema (systray)
|
||||
- Integración con notificaciones de escritorio
|
||||
- Integración con el selector de archivos
|
||||
|
||||
### Gestión de Ventanas
|
||||
|
||||
- Soporte para X11 y Wayland
|
||||
- Compatible con gestores de ventanas en mosaico (tiling)
|
||||
- Opción de decoraciones de ventana personalizadas
|
||||
|
||||
## Windows
|
||||
|
||||
### Funciones
|
||||
|
||||
- Integración en la bandeja del sistema
|
||||
- Acciones rápidas en la Jump List
|
||||
- Controles de ventana nativos
|
||||
- Opción de inicio automático al arrancar
|
||||
|
||||
## Funciones de Escritorio Multiplataforma
|
||||
|
||||
### Atajos de Teclado
|
||||
|
||||
- **Cmd/Ctrl + N**: Nuevo servidor
|
||||
- **Cmd/Ctrl + W**: Cerrar pestaña
|
||||
- **Cmd/Ctrl + T**: Nueva pestaña de terminal
|
||||
- **Cmd/Ctrl + ,**: Ajustes
|
||||
|
||||
### Temas
|
||||
|
||||
- Tema claro
|
||||
- Tema oscuro
|
||||
- Tema AMOLED (negro puro)
|
||||
- Tema del sistema (sigue al SO)
|
||||
|
||||
### Múltiples Ventanas
|
||||
|
||||
- Abrir varios servidores en ventanas separadas
|
||||
- Arrastrar pestañas a una nueva ventana
|
||||
- Comparar estadísticas de servidores en paralelo
|
||||
|
||||
### Ventajas sobre el Móvil
|
||||
|
||||
- Pantalla más grande para monitorización
|
||||
- Teclado completo para la terminal
|
||||
- Operaciones de archivos más rápidas
|
||||
- Mejor multitarea
|
||||
77
docs/src/content/docs/es/platforms/mobile.md
Normal file
77
docs/src/content/docs/es/platforms/mobile.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Funciones Móviles
|
||||
description: Funciones específicas para iOS y Android
|
||||
---
|
||||
|
||||
Server Box proporciona varias funciones específicas para dispositivos móviles iOS y Android.
|
||||
|
||||
## Autenticación Biométrica
|
||||
|
||||
Asegura tus servidores con autenticación biométrica:
|
||||
|
||||
- **iOS**: Face ID o Touch ID
|
||||
- **Android**: Autenticación por huella dactilar
|
||||
|
||||
Actívalo en Ajustes > Seguridad > Autenticación biométrica.
|
||||
|
||||
## Widgets de Pantalla de Inicio
|
||||
|
||||
Añade widgets de estado del servidor a tu pantalla de inicio para una monitorización rápida.
|
||||
|
||||
### iOS
|
||||
|
||||
- Mantén pulsada la pantalla de inicio
|
||||
- Toca en **+** para añadir un widget
|
||||
- Busca "Server Box"
|
||||
- Elige el tamaño del widget:
|
||||
- Pequeño: Estado de un solo servidor
|
||||
- Mediano: Múltiples servidores
|
||||
- Grande: Información detallada
|
||||
|
||||
### Android
|
||||
|
||||
- Mantén pulsada la pantalla de inicio
|
||||
- Toca en **Widgets**
|
||||
- Busca "Server Box"
|
||||
- Selecciona el tipo de widget
|
||||
|
||||
## Ejecución en Segundo Plano
|
||||
|
||||
### Android
|
||||
|
||||
Mantén las conexiones activas en segundo plano:
|
||||
|
||||
- Actívalo en Ajustes > Avanzado > Ejecución en segundo plano
|
||||
- Requiere exclusión de la optimización de batería
|
||||
- Notificaciones persistentes para conexiones activas
|
||||
|
||||
### iOS
|
||||
|
||||
Se aplican limitaciones de segundo plano:
|
||||
|
||||
- Las conexiones pueden pausarse en segundo plano
|
||||
- Reconexión rápida al volver a la app
|
||||
- Soporte para actualización en segundo plano
|
||||
|
||||
## Notificaciones Push
|
||||
|
||||
Recibe notificaciones para:
|
||||
|
||||
- Alertas de servidor fuera de línea
|
||||
- Avisos de alto uso de recursos
|
||||
- Alertas de finalización de tareas
|
||||
|
||||
Configúralo en Ajustes > Notificaciones.
|
||||
|
||||
## Funciones de UI Móvil
|
||||
|
||||
- **Deslizar para refrescar**: Actualiza el estado del servidor
|
||||
- **Acciones de deslizamiento**: Operaciones rápidas de servidor
|
||||
- **Modo horizontal**: Mejor experiencia de terminal
|
||||
- **Teclado virtual**: Atajos de terminal
|
||||
|
||||
## Integración de Archivos
|
||||
|
||||
- **App Archivos (iOS)**: Acceso directo SFTP desde Archivos
|
||||
- **Storage Access Framework (Android)**: Comparte archivos con otras apps
|
||||
- **Selector de documentos**: Selección de archivos sencilla
|
||||
214
docs/src/content/docs/es/principles/architecture.md
Normal file
214
docs/src/content/docs/es/principles/architecture.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
title: Descripción General de la Arquitectura
|
||||
description: Arquitectura de alto nivel de la aplicación
|
||||
---
|
||||
|
||||
Server Box sigue una arquitectura por capas con una clara separación de responsabilidades.
|
||||
|
||||
## Capas de la Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Capa de Presentación (UI) │
|
||||
│ lib/view/page/, lib/view/widget/ │
|
||||
│ - Páginas, Widgets, Controladores │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Capa de Lógica de Negocio │
|
||||
│ lib/data/provider/ │
|
||||
│ - Riverpod Providers, State Notifiers │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Capa de Acceso a Datos │
|
||||
│ lib/data/store/, lib/data/model/ │
|
||||
│ - Hive Stores, Modelos de Datos │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Capa de Integración Externa │
|
||||
│ - SSH (dartssh2), Terminal (xterm), SFTP │
|
||||
│ - Código específico de plataforma (iOS, etc.) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Fundamentos de la Aplicación
|
||||
|
||||
### Punto de Entrada Principal
|
||||
|
||||
`lib/main.dart` inicializa la aplicación:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
runApp(
|
||||
ProviderScope(
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Widget Raíz
|
||||
|
||||
`MyApp` proporciona:
|
||||
- **Gestión de Temas**: Cambio entre tema claro/oscuro
|
||||
- **Configuración de Rutas**: Estructura de navegación
|
||||
- **Provider Scope**: Raíz para la inyección de dependencias
|
||||
|
||||
### Página de Inicio
|
||||
|
||||
`HomePage` sirve como núcleo de navegación:
|
||||
- **Interfaz de Pestañas**: Servidor, Snippet, Contenedor, SSH
|
||||
- **Gestión de Estado**: Estado por pestaña
|
||||
- **Navegación**: Acceso a funciones
|
||||
|
||||
## Sistemas Principales
|
||||
|
||||
### Gestión de Estado: Riverpod
|
||||
|
||||
**¿Por qué Riverpod?**
|
||||
- Seguridad en tiempo de compilación
|
||||
- Facilidad para realizar pruebas
|
||||
- Sin dependencia del Build context
|
||||
- Funciona en todas las plataformas
|
||||
|
||||
**Tipos de Provider Utilizados:**
|
||||
- `StateProvider`: Estado mutable simple
|
||||
- `AsyncNotifierProvider`: Estados de carga/error/datos
|
||||
- `StreamProvider`: Flujos de datos en tiempo real
|
||||
- Future providers: Operaciones asíncronas únicas
|
||||
|
||||
### Persistencia de Datos: Hive CE
|
||||
|
||||
**¿Por qué Hive CE?**
|
||||
- Sin dependencias de código nativo
|
||||
- Almacenamiento clave-valor rápido
|
||||
- Tipado seguro con generación de código
|
||||
- Sin necesidad de anotaciones manuales de campos
|
||||
|
||||
**Almacenes (Stores):**
|
||||
- `SettingStore`: Preferencias de la app
|
||||
- `ServerStore`: Configuraciones de servidores
|
||||
- `SnippetStore`: Fragmentos de comandos
|
||||
- `KeyStore`: Claves SSH
|
||||
|
||||
### Modelos Inmutables: Freezed
|
||||
|
||||
**Beneficios:**
|
||||
- Inmutabilidad en tiempo de compilación
|
||||
- Tipos Union para el estado
|
||||
- Serialización JSON integrada
|
||||
- Extensiones CopyWith
|
||||
|
||||
## Estrategia Multiplataforma
|
||||
|
||||
### Sistema de Plugins
|
||||
|
||||
Los plugins de Flutter proporcionan la integración con la plataforma:
|
||||
|
||||
| Plataforma | Método de Integración |
|
||||
|------------|-----------------------|
|
||||
| iOS | CocoaPods, Swift/Obj-C |
|
||||
| Android | Gradle, Kotlin/Java |
|
||||
| macOS | CocoaPods, Swift |
|
||||
| Linux | CMake, C++ |
|
||||
| Windows | CMake, C# |
|
||||
|
||||
### Funciones Específicas por Plataforma
|
||||
|
||||
**Solo iOS:**
|
||||
- Widgets de pantalla de inicio
|
||||
- Actividades en Directo (Live Activities)
|
||||
- Compañero de Apple Watch
|
||||
|
||||
**Solo Android:**
|
||||
- Servicio en segundo plano
|
||||
- Notificaciones push
|
||||
- Acceso al sistema de archivos
|
||||
|
||||
**Solo Escritorio:**
|
||||
- Integración en la barra de menús
|
||||
- Múltiples ventanas
|
||||
- Barra de título personalizada
|
||||
|
||||
## Dependencias Personalizadas
|
||||
|
||||
### Rama (Fork) de dartssh2
|
||||
|
||||
Cliente SSH mejorado con:
|
||||
- Mejor soporte para móviles
|
||||
- Gestión de errores mejorada
|
||||
- Optimizaciones de rendimiento
|
||||
|
||||
### Rama (Fork) de xterm.dart
|
||||
|
||||
Emulador de terminal con:
|
||||
- Renderizado optimizado para móviles
|
||||
- Soporte para gestos táctiles
|
||||
- Integración con teclado virtual
|
||||
|
||||
### fl_lib
|
||||
|
||||
Paquete de utilidades compartidas con:
|
||||
- Widgets comunes
|
||||
- Extensiones
|
||||
- Funciones de ayuda
|
||||
|
||||
## Sistema de Compilación
|
||||
|
||||
### Paquete fl_build
|
||||
|
||||
Sistema de compilación personalizado para:
|
||||
- Compilaciones multiplataforma
|
||||
- Firma de código
|
||||
- Empaquetado de recursos (assets)
|
||||
- Gestión de versiones
|
||||
|
||||
### Proceso de Compilación
|
||||
|
||||
```
|
||||
make.dart (versión) → fl_build (compilación) → Salida de plataforma
|
||||
```
|
||||
|
||||
1. **Pre-compilación**: Cálculo de la versión desde Git
|
||||
2. **Compilación**: Compilar para la plataforma de destino
|
||||
3. **Post-compilación**: Empaquetado y firma
|
||||
|
||||
## Ejemplo de Flujo de Datos
|
||||
|
||||
### Actualización del Estado del Servidor
|
||||
|
||||
```
|
||||
1. El temporizador se activa →
|
||||
2. El Provider llama al servicio →
|
||||
3. El servicio ejecuta el comando SSH →
|
||||
4. La respuesta se analiza en el modelo →
|
||||
5. Se actualiza el estado →
|
||||
6. La UI se reconstruye con los nuevos datos
|
||||
```
|
||||
|
||||
### Flujo de Acción del Usuario
|
||||
|
||||
```
|
||||
1. El usuario toca un botón →
|
||||
2. El Widget llama al método del provider →
|
||||
3. El Provider actualiza el estado →
|
||||
4. El cambio de estado activa la reconstrucción →
|
||||
5. El nuevo estado se refleja en la UI
|
||||
```
|
||||
|
||||
## Arquitectura de Seguridad
|
||||
|
||||
### Protección de Datos
|
||||
|
||||
- **Contraseñas**: Cifradas con flutter_secure_storage
|
||||
- **Claves SSH**: Cifradas en reposo
|
||||
- **Huellas de Host**: Almacenadas de forma segura
|
||||
- **Datos de Sesión**: No se persisten
|
||||
|
||||
### Seguridad de Conexión
|
||||
|
||||
- **Verificación de Clave de Host**: Detección de MITM
|
||||
- **Cifrado**: Cifrado SSH estándar
|
||||
- **Sin Texto Plano**: Los datos sensibles nunca se almacenan en plano
|
||||
490
docs/src/content/docs/es/principles/sftp.md
Normal file
490
docs/src/content/docs/es/principles/sftp.md
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
title: Sistema SFTP
|
||||
description: Cómo funciona el explorador de archivos SFTP
|
||||
---
|
||||
|
||||
El sistema SFTP proporciona capacidades de gestión de archivos sobre SSH.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa UI de SFTP │
|
||||
│ - Explorador de archivos (remoto) │
|
||||
│ - Explorador de archivos (local) │
|
||||
│ - Cola de transferencia │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Gestión de Estado SFTP │
|
||||
│ - sftpProvider │
|
||||
│ - Gestión de rutas │
|
||||
│ - Cola de operaciones │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa de Protocolo SFTP │
|
||||
│ - Subsistema SSH │
|
||||
│ - Operaciones de archivos │
|
||||
│ - Listado de directorios │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Transporte SSH │
|
||||
│ - Canal seguro │
|
||||
│ - Streaming de datos │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Establecimiento de la Conexión
|
||||
|
||||
### Creación del Cliente SFTP
|
||||
|
||||
```dart
|
||||
Future<SftpClient> createSftpClient(Spi spi) async {
|
||||
// 1. Obtener cliente SSH (reutilizar si está disponible)
|
||||
final sshClient = await genClient(spi);
|
||||
|
||||
// 2. Abrir subsistema SFTP
|
||||
final sftp = await sshClient.openSftp();
|
||||
|
||||
return sftp;
|
||||
}
|
||||
```
|
||||
|
||||
### Reutilización de Conexiones
|
||||
|
||||
SFTP reutiliza las conexiones SSH existentes:
|
||||
|
||||
```dart
|
||||
class ServerProvider {
|
||||
SSHClient? _sshClient;
|
||||
SftpClient? _sftpClient;
|
||||
|
||||
Future<SftpClient> getSftpClient(String spiId) async {
|
||||
_sftpClient ??= await _sshClient!.openSftp();
|
||||
return _sftpClient!;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Operaciones del Sistema de Archivos
|
||||
|
||||
### Listado de Directorios
|
||||
|
||||
```dart
|
||||
Future<List<SftpFile>> listDirectory(String path) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Listar directorio
|
||||
final files = await sftp.listDir(path);
|
||||
|
||||
// Ordenar según ajustes
|
||||
files.sort((a, b) {
|
||||
switch (sortOption) {
|
||||
case SortOption.name:
|
||||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
case SortOption.size:
|
||||
return a.size.compareTo(b.size);
|
||||
case SortOption.time:
|
||||
return a.modified.compareTo(b.modified);
|
||||
}
|
||||
});
|
||||
|
||||
// Carpetas primero si está activado
|
||||
if (showFoldersFirst) {
|
||||
final dirs = files.where((f) => f.isDirectory);
|
||||
final regular = files.where((f) => !f.isDirectory);
|
||||
return [...dirs, ...regular];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
```
|
||||
|
||||
### Metadatos de Archivo
|
||||
|
||||
```dart
|
||||
class SftpFile {
|
||||
final String name;
|
||||
final String path;
|
||||
final int size; // Bytes
|
||||
final int modified; // Timestamp Unix
|
||||
final String permissions; // ej., "rwxr-xr-x"
|
||||
final String owner;
|
||||
final String group;
|
||||
final bool isDirectory;
|
||||
final bool isSymlink;
|
||||
|
||||
String get sizeFormatted => formatBytes(size);
|
||||
String get modifiedFormatted => formatDate(modified);
|
||||
}
|
||||
```
|
||||
|
||||
## Operaciones de Archivo
|
||||
|
||||
### Subida (Upload)
|
||||
|
||||
```dart
|
||||
Future<void> uploadFile(
|
||||
String localPath,
|
||||
String remotePath,
|
||||
) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Crear petición
|
||||
final req = SftpReq(
|
||||
spi: spi,
|
||||
remotePath: remotePath,
|
||||
localPath: localPath,
|
||||
type: SftpReqType.upload,
|
||||
);
|
||||
|
||||
// Añadir a la cola
|
||||
_transferQueue.add(req);
|
||||
|
||||
// Ejecutar transferencia con progreso
|
||||
final file = File(localPath);
|
||||
final size = await file.length();
|
||||
final stream = file.openRead();
|
||||
|
||||
await sftp.upload(
|
||||
stream: stream,
|
||||
toPath: remotePath,
|
||||
onProgress: (transferred) {
|
||||
_updateProgress(req, transferred, size);
|
||||
},
|
||||
);
|
||||
|
||||
// Completar
|
||||
_transferQueue.remove(req);
|
||||
}
|
||||
```
|
||||
|
||||
### Descarga (Download)
|
||||
|
||||
```dart
|
||||
Future<void> downloadFile(
|
||||
String remotePath,
|
||||
String localPath,
|
||||
) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Crear archivo local
|
||||
final file = File(localPath);
|
||||
final sink = file.openWrite();
|
||||
|
||||
// Descargar con progreso
|
||||
final stat = await sftp.stat(remotePath);
|
||||
|
||||
await sftp.download(
|
||||
fromPath: remotePath,
|
||||
toSink: sink,
|
||||
onProgress: (transferred) {
|
||||
_updateProgress(
|
||||
SftpReq(...),
|
||||
transferred,
|
||||
stat.size,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await sink.close();
|
||||
}
|
||||
```
|
||||
|
||||
### Edición de Permisos
|
||||
|
||||
```dart
|
||||
Future<void> setPermissions(
|
||||
String path,
|
||||
String permissions,
|
||||
) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// Analizar permisos (ej., "rwxr-xr-x" o "755")
|
||||
final mode = parsePermissions(permissions);
|
||||
|
||||
// Establecer vía comando SSH (más fiable que SFTP)
|
||||
final ssh = await getSshClient(spiId);
|
||||
await ssh.exec('chmod $mode "$path"');
|
||||
}
|
||||
```
|
||||
|
||||
## Gestión de Rutas
|
||||
|
||||
### Estructura de Rutas
|
||||
|
||||
```dart
|
||||
class PathWithPrefix {
|
||||
final String prefix; // ej., "/home/user"
|
||||
final String path; // Relativa o absoluta
|
||||
|
||||
String get fullPath {
|
||||
if (path.startsWith('/')) {
|
||||
return path; // Ruta absoluta
|
||||
}
|
||||
return '$prefix/$path'; // Ruta relativa
|
||||
}
|
||||
|
||||
PathWithPrefix cd(String subPath) {
|
||||
return PathWithPrefix(
|
||||
prefix: fullPath,
|
||||
path: subPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Historial de Navegación
|
||||
|
||||
```dart
|
||||
class PathHistory {
|
||||
final List<String> _history = [];
|
||||
int _index = -1;
|
||||
|
||||
void push(String path) {
|
||||
// Eliminar historial hacia adelante
|
||||
_history.removeRange(_index + 1, _history.length);
|
||||
_history.add(path);
|
||||
_index = _history.length - 1;
|
||||
}
|
||||
|
||||
String? back() {
|
||||
if (_index > 0) {
|
||||
_index--;
|
||||
return _history[_index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? forward() {
|
||||
if (_index < _history.length - 1) {
|
||||
_index++;
|
||||
return _history[_index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sistema de Transferencia
|
||||
|
||||
### Petición de Transferencia
|
||||
|
||||
```dart
|
||||
class SftpReq {
|
||||
final Spi spi;
|
||||
final String remotePath;
|
||||
final String localPath;
|
||||
final SftpReqType type;
|
||||
final DateTime createdAt;
|
||||
|
||||
int? totalBytes;
|
||||
int? transferredBytes;
|
||||
String? error;
|
||||
}
|
||||
```
|
||||
|
||||
### Seguimiento de Progreso
|
||||
|
||||
```dart
|
||||
class TransferProgress {
|
||||
final SftpReq request;
|
||||
final int total;
|
||||
final int transferred;
|
||||
final DateTime startTime;
|
||||
|
||||
double get percentage => (transferred / total) * 100;
|
||||
Duration get elapsed => DateTime.now().difference(startTime);
|
||||
|
||||
String get speedFormatted {
|
||||
final bytesPerSecond = transferred / elapsed.inSeconds;
|
||||
return formatSpeed(bytesPerSecond);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gestión de Colas
|
||||
|
||||
```dart
|
||||
class TransferQueue {
|
||||
final List<SftpReq> _queue = [];
|
||||
final Map<String, TransferProgress> _progress = {};
|
||||
int _concurrent = 3; // Transferencias concurrentes máx.
|
||||
|
||||
Future<void> process() async {
|
||||
final active = _progress.values.where((p) => p.isInProgress);
|
||||
if (active.length >= _concurrent) return;
|
||||
|
||||
final pending = _queue.where((r) => !_progress.containsKey(r.id));
|
||||
for (final req in pending.take(_concurrent - active.length)) {
|
||||
_executeTransfer(req);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _executeTransfer(SftpReq req) async {
|
||||
try {
|
||||
_progress[req.id] = TransferProgress.inProgress(req);
|
||||
|
||||
if (req.type == SftpReqType.upload) {
|
||||
await uploadFile(req.localPath, req.remotePath);
|
||||
} else {
|
||||
await downloadFile(req.remotePath, req.localPath);
|
||||
}
|
||||
|
||||
_progress[req.id] = TransferProgress.completed(req);
|
||||
} catch (e) {
|
||||
_progress[req.id] = TransferProgress.failed(req, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Patrón de Almacenamiento Local
|
||||
|
||||
### Caché de Descargas
|
||||
|
||||
Los archivos descargados se guardan en:
|
||||
|
||||
```dart
|
||||
String getLocalDownloadPath(String spiId, String remotePath) {
|
||||
final normalized = remotePath.replaceAll('/', '_');
|
||||
return 'Paths.file/$spiId/$normalized';
|
||||
}
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
- Remoto: `/var/log/nginx/access.log`
|
||||
- spiId: `server-123`
|
||||
- Local: `Paths.file/server-123/_var_log_nginx_access.log`
|
||||
|
||||
## Edición de Archivos
|
||||
|
||||
### Flujo de Trabajo de Edición
|
||||
|
||||
```dart
|
||||
Future<void> editFile(String path) async {
|
||||
final sftp = await getSftpClient(spiId);
|
||||
|
||||
// 1. Comprobar tamaño
|
||||
final stat = await sftp.stat(path);
|
||||
if (stat.size > editorMaxSize) {
|
||||
showWarning('Archivo demasiado grande para el editor integrado');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Descargar a temporal
|
||||
final temp = await downloadToTemp(path);
|
||||
|
||||
// 3. Abrir en editor
|
||||
final content = await openEditor(temp.path);
|
||||
|
||||
// 4. Subir de nuevo
|
||||
await uploadFile(temp.path, path);
|
||||
|
||||
// 5. Limpieza
|
||||
await temp.delete();
|
||||
}
|
||||
```
|
||||
|
||||
### Integración con Editor Externo
|
||||
|
||||
```dart
|
||||
Future<void> editInExternalEditor(String path) async {
|
||||
final ssh = await getSshClient(spiId);
|
||||
|
||||
// Abrir terminal con editor
|
||||
final editor = getSetting('sftpEditor', 'vim');
|
||||
await ssh.exec('$editor "$path"');
|
||||
|
||||
// El usuario edita en la terminal
|
||||
// Tras guardar, refrescar la vista SFTP
|
||||
}
|
||||
```
|
||||
|
||||
## Gestión de Errores
|
||||
|
||||
### Errores de Permiso
|
||||
|
||||
```dart
|
||||
try {
|
||||
await sftp.upload(...);
|
||||
} on SftpPermissionException {
|
||||
showError('Permiso denegado: ${stat.path}');
|
||||
showHint('Comprueba los permisos y la propiedad del archivo');
|
||||
}
|
||||
```
|
||||
|
||||
### Erreores de Conexión
|
||||
|
||||
```dart
|
||||
try {
|
||||
await sftp.listDir(path);
|
||||
} on SftpConnectionException {
|
||||
showError('Conexión perdida');
|
||||
await reconnect();
|
||||
}
|
||||
```
|
||||
|
||||
### Errores de Espacio
|
||||
|
||||
```dart
|
||||
try {
|
||||
await sftp.upload(...);
|
||||
} on SftpNoSpaceException {
|
||||
showError('Disco lleno en el servidor remoto');
|
||||
}
|
||||
```
|
||||
|
||||
## Optimizaciones de Rendimiento
|
||||
|
||||
### Caché de Directorios
|
||||
|
||||
```dart
|
||||
class DirectoryCache {
|
||||
final Map<String, CachedDirectory> _cache = {};
|
||||
final Duration ttl = Duration(minutes: 5);
|
||||
|
||||
Future<List<SftpFile>> list(String path) async {
|
||||
final cached = _cache[path];
|
||||
if (cached != null && !cached.isExpired) {
|
||||
return cached.files;
|
||||
}
|
||||
|
||||
final files = await sftp.listDir(path);
|
||||
_cache[path] = CachedDirectory(files);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Carga Perezosa (Lazy Loading)
|
||||
|
||||
Para directorios grandes (>1000 elementos):
|
||||
|
||||
```dart
|
||||
List<SftpFile> loadPage(String path, int page, int pageSize) {
|
||||
final all = cache[path] ?? [];
|
||||
final start = page * pageSize;
|
||||
final end = start + pageSize;
|
||||
return all.sublist(start, end.clamp(0, all.length));
|
||||
}
|
||||
```
|
||||
|
||||
### Paginación
|
||||
|
||||
```dart
|
||||
class PaginatedDirectory {
|
||||
static const pageSize = 100;
|
||||
|
||||
Future<List<SftpFile>> getPage(int page) async {
|
||||
final offset = page * pageSize;
|
||||
return await sftp.listDir(
|
||||
path,
|
||||
offset: offset,
|
||||
limit: pageSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
305
docs/src/content/docs/es/principles/ssh.md
Normal file
305
docs/src/content/docs/es/principles/ssh.md
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: Conexión SSH
|
||||
description: Cómo se establecen y gestionan las conexiones SSH
|
||||
---
|
||||
|
||||
Entendiendo las conexiones SSH en Server Box.
|
||||
|
||||
## Flujo de Conexión
|
||||
|
||||
```text
|
||||
Entrada de Usuario → Configuración Spi → genClient() → Cliente SSH → Sesión
|
||||
```
|
||||
|
||||
### Paso 1: Configuración
|
||||
|
||||
El modelo `Spi` (Server Parameter Info) contiene:
|
||||
|
||||
```dart
|
||||
class Spi {
|
||||
String id; // ID del servidor
|
||||
String name; // Nombre del servidor
|
||||
String ip; // Dirección IP
|
||||
int port; // Puerto SSH (por defecto 22)
|
||||
String user; // Usuario
|
||||
String? pwd; // Contraseña (cifrada)
|
||||
String? keyId; // ID de la clave SSH
|
||||
String? jumpId; // ID del servidor de salto (Jump server)
|
||||
String? alterUrl; // URL alternativa
|
||||
}
|
||||
```
|
||||
|
||||
### Paso 2: Generación del Cliente
|
||||
|
||||
`genClient(spi)` crea el cliente SSH:
|
||||
|
||||
```dart
|
||||
Future<SSHClient> genClient(Spi spi) async {
|
||||
// 1. Establecer socket
|
||||
var socket = await connect(spi.ip, spi.port);
|
||||
|
||||
// 2. Probar URL alternativa si falla
|
||||
if (socket == null && spi.alterUrl != null) {
|
||||
socket = await connect(spi.alterUrl, spi.port);
|
||||
}
|
||||
|
||||
if (socket == null) {
|
||||
throw ConnectionException('Unable to connect');
|
||||
}
|
||||
|
||||
// 3. Autenticar
|
||||
final client = SSHClient(
|
||||
socket: socket,
|
||||
username: spi.user,
|
||||
onPasswordRequest: () => spi.pwd,
|
||||
onIdentityRequest: () => loadKey(spi.keyId),
|
||||
);
|
||||
|
||||
// 4. Verificar clave de host
|
||||
await verifyHostKey(client, spi);
|
||||
|
||||
return client;
|
||||
}
|
||||
```
|
||||
|
||||
### Paso 3: Servidor de Salto (si está configurado)
|
||||
|
||||
Para servidores de salto, conexión recursiva:
|
||||
|
||||
```dart
|
||||
if (spi.jumpId != null) {
|
||||
final jumpClient = await genClient(getJumpSpi(spi.jumpId));
|
||||
final forwarded = await jumpClient.forwardLocal(
|
||||
spi.ip,
|
||||
spi.port,
|
||||
);
|
||||
// Conectar a través del socket reenviado
|
||||
}
|
||||
```
|
||||
|
||||
## Métodos de Autenticación
|
||||
|
||||
### Autenticación por Contraseña
|
||||
|
||||
```dart
|
||||
onPasswordRequest: () => spi.pwd
|
||||
```
|
||||
|
||||
- Contraseña almacenada cifrada en Hive
|
||||
- Descifrada al conectar
|
||||
- Enviada al servidor para verificación
|
||||
|
||||
### Autenticación por Clave Privada
|
||||
|
||||
```dart
|
||||
onIdentityRequest: () async {
|
||||
final key = await KeyStore.get(spi.keyId);
|
||||
return decyptPem(key.pem, key.password);
|
||||
}
|
||||
```
|
||||
|
||||
**Proceso de Carga de Clave:**
|
||||
1. Recuperar clave cifrada de `KeyStore`
|
||||
2. Descifrar contraseña (biometría/aviso)
|
||||
3. Analizar formato PEM
|
||||
4. Estandarizar finales de línea (LF)
|
||||
5. Retornar para autenticación
|
||||
|
||||
### Interacción por Teclado (Keyboard-Interactive)
|
||||
|
||||
```dart
|
||||
onUserInfoRequest: (instructions) async {
|
||||
// Gestionar desafío-respuesta
|
||||
return responses;
|
||||
}
|
||||
```
|
||||
|
||||
Soporta:
|
||||
- Autenticación por contraseña
|
||||
- Tokens OTP
|
||||
- Autenticación de doble factor (2FA)
|
||||
|
||||
## Verificación de Clave de Host
|
||||
|
||||
### ¿Por qué verificar las claves de host?
|
||||
|
||||
Evita ataques de **Hombre en el Medio (MITM)** asegurando que te conectas al mismo servidor.
|
||||
|
||||
### Formato de Almacenamiento
|
||||
|
||||
```text
|
||||
{spi.id}::{keyType}
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
```text
|
||||
mi-servidor::ssh-ed25519
|
||||
mi-servidor::ecdsa-sha2-nistp256
|
||||
```
|
||||
|
||||
### Formatos de Huella Digital (Fingerprint)
|
||||
|
||||
**MD5 Hex:**
|
||||
```text
|
||||
aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
|
||||
```
|
||||
|
||||
**Base64:**
|
||||
```text
|
||||
SHA256:AbCdEf1234567890...=
|
||||
```
|
||||
|
||||
### Flujo de Verificación
|
||||
|
||||
```dart
|
||||
Future<void> verifyHostKey(SSHClient client, Spi spi) async {
|
||||
final key = await client.hostKey;
|
||||
final keyType = key.type;
|
||||
final fingerprint = md5Hex(key); // o base64
|
||||
|
||||
final stored = SettingStore.sshKnownHostsFingerprints
|
||||
['${spi.id}::$keyType'];
|
||||
|
||||
if (stored == null) {
|
||||
// Nuevo host - preguntar al usuario
|
||||
final trust = await promptUser(
|
||||
'Host desconocido',
|
||||
'Huella: $fingerprint',
|
||||
);
|
||||
if (trust) {
|
||||
SettingStore.sshKnownHostsFingerprints
|
||||
['${spi.id}::$keyType'] = fingerprint;
|
||||
}
|
||||
} else if (stored != fingerprint) {
|
||||
// Ha cambiado - advertir al usuario
|
||||
await warnUser(
|
||||
'¡La clave de host ha cambiado!',
|
||||
'Posible ataque MITM',
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gestión de Sesiones
|
||||
|
||||
### Pool de Conexiones
|
||||
|
||||
Clientes activos mantenidos en `ServerProvider`:
|
||||
|
||||
```dart
|
||||
class ServerProvider {
|
||||
final Map<String, SSHClient> _clients = {};
|
||||
|
||||
SSHClient getClient(String spiId) {
|
||||
return _clients[spiId] ??= connect(spiId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Keep-Alive
|
||||
|
||||
Mantener la conexión durante la inactividad:
|
||||
|
||||
```dart
|
||||
Timer.periodic(
|
||||
Duration(seconds: 30),
|
||||
(_) => client.sendKeepAlive(),
|
||||
);
|
||||
```
|
||||
|
||||
### Reconexión Automática
|
||||
|
||||
Al perder la conexión:
|
||||
|
||||
```dart
|
||||
client.onError.listen((error) async {
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
reconnect();
|
||||
});
|
||||
```
|
||||
|
||||
## Ciclo de Vida de la Conexión
|
||||
|
||||
```text
|
||||
┌─────────────┐
|
||||
│ Inicial │
|
||||
└──────┬──────┘
|
||||
│ connect()
|
||||
↓
|
||||
┌─────────────┐
|
||||
│ Conectando │ ←──┐
|
||||
└──────┬──────┘ │
|
||||
│ éxito │
|
||||
↓ │ fallo (reintento)
|
||||
┌─────────────┐ │
|
||||
│ Conectado │───┘
|
||||
└──────┬──────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────┐
|
||||
│ Activo │ ──→ Enviar comandos
|
||||
└──────┬──────┘
|
||||
│
|
||||
↓ (error/desconexión)
|
||||
┌─────────────┐
|
||||
│ Desconectado│
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Gestión de Errores
|
||||
|
||||
### Tiempo de Espera Agotado (Timeout)
|
||||
|
||||
```dart
|
||||
try {
|
||||
await client.connect().timeout(
|
||||
Duration(seconds: 30),
|
||||
);
|
||||
} on TimeoutException {
|
||||
throw ConnectionException('Tiempo de espera de conexión agotado');
|
||||
}
|
||||
```
|
||||
|
||||
### Fallo de Autenticación
|
||||
|
||||
```dart
|
||||
onAuthFail: (error) {
|
||||
if (error.contains('password')) {
|
||||
return 'Contraseña no válida';
|
||||
} else if (error.contains('key')) {
|
||||
return 'Clave SSH no válida';
|
||||
}
|
||||
return 'Fallo de autenticación';
|
||||
}
|
||||
```
|
||||
|
||||
### Discrepancia en Clave de Host
|
||||
|
||||
```dart
|
||||
onHostKeyMismatch: (stored, current) {
|
||||
showSecurityWarning(
|
||||
'¡La clave de host ha cambiado!',
|
||||
'Posible ataque MITM',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Consideraciones de Rendimiento
|
||||
|
||||
### Reutilización de Conexiones
|
||||
|
||||
- Reutilizar clientes entre funciones
|
||||
- No desconectar/reconectar innecesariamente
|
||||
- Pool de conexiones para operaciones concurrentes
|
||||
|
||||
### Ajustes Óptimos
|
||||
|
||||
- **Timeout**: 30 segundos (ajustable)
|
||||
- **Keep-alive**: Cada 30 segundos
|
||||
- **Retraso de reintento**: 5 segundos
|
||||
|
||||
### Eficiencia de Red
|
||||
|
||||
- Conexión única para múltiples operaciones
|
||||
- Comandos en tubería (pipeline) cuando sea posible
|
||||
- Evitar abrir múltiples conexiones
|
||||
167
docs/src/content/docs/es/principles/state.md
Normal file
167
docs/src/content/docs/es/principles/state.md
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
title: Gestión de Estado
|
||||
description: Cómo se gestiona el estado con Riverpod
|
||||
---
|
||||
|
||||
Entendiendo la arquitectura de gestión de estado en Server Box.
|
||||
|
||||
## ¿Por qué Riverpod?
|
||||
|
||||
**Beneficios Clave:**
|
||||
- **Seguridad en tiempo de compilación**: Detecta errores al compilar
|
||||
- **Sin necesidad de BuildContext**: Accede al estado desde cualquier lugar
|
||||
- **Facilidad de pruebas**: Sencillo de probar providers de forma aislada
|
||||
- **Generación de código**: Menos código repetitivo, tipado seguro
|
||||
|
||||
## Arquitectura de Providers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa UI (Widgets) │
|
||||
│ - ConsumerWidget / ConsumerStatefulWidget │
|
||||
│ - ref.watch() / ref.read() │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓ observa (watches)
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa de Provider │
|
||||
│ - Anotaciones @riverpod │
|
||||
│ - Archivos *.g.dart generados │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓ usa (uses)
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa de Servicio / Store │
|
||||
│ - Lógica de negocio │
|
||||
│ - Acceso a datos │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Tipos de Provider Utilizados
|
||||
|
||||
### 1. StateProvider (Estado Simple)
|
||||
|
||||
Para estados simples y observables:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ThemeNotifier extends _$ThemeNotifier {
|
||||
@override
|
||||
ThemeMode build() {
|
||||
// Cargar desde ajustes
|
||||
return SettingStore.themeMode;
|
||||
}
|
||||
|
||||
void setTheme(ThemeMode mode) {
|
||||
state = mode;
|
||||
SettingStore.themeMode = mode; // Persistir
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
```dart
|
||||
class MyWidget extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themeNotifierProvider);
|
||||
return Text('Tema: $theme');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AsyncNotifierProvider (Estado Asíncrono)
|
||||
|
||||
Para datos que se cargan de forma asíncrona:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class ServerStatus extends _$ServerStatus {
|
||||
@override
|
||||
Future<StatusModel> build(Server server) async {
|
||||
// Carga inicial
|
||||
return await fetchStatus(server);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return await fetchStatus(server);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
```dart
|
||||
final status = ref.watch(serverStatusProvider(server));
|
||||
|
||||
status.when(
|
||||
data: (data) => StatusWidget(data),
|
||||
loading: () => LoadingWidget(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### 3. StreamProvider (Datos en Tiempo Real)
|
||||
|
||||
Para flujos de datos continuos:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
|
||||
final client = ref.watch(sshClientProvider(server));
|
||||
final stream = client.monitorCpu();
|
||||
|
||||
// Liberación automática cuando no se observa
|
||||
ref.onDispose(() {
|
||||
client.stopMonitoring();
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
```dart
|
||||
final cpu = ref.watch(cpuUsageProvider(server));
|
||||
|
||||
cpu.when(
|
||||
data: (usage) => CpuChart(usage),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
error: (error, stack) => ErrorWidget(error),
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Family Providers (Parametrizados)
|
||||
|
||||
Providers que aceptan parámetros:
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
Future<List<Container>> containers(ContainersRef ref, Server server) async {
|
||||
final client = await ref.watch(sshClientProvider(server).future);
|
||||
return await client.listContainers();
|
||||
}
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
```dart
|
||||
final containers = ref.watch(containersProvider(server));
|
||||
|
||||
// Diferentes servidores = diferentes estados en caché
|
||||
final containers2 = ref.watch(containersProvider(server2));
|
||||
```
|
||||
|
||||
## Optimizaciones de Rendimiento
|
||||
|
||||
- **Provider Keep-Alive**: Usa `@Riverpod(keepAlive: true)` para evitar que se destruya automáticamente cuando no haya escuchadores.
|
||||
- **Observación selectiva**: Usa `select` para observar solo una parte específica del estado.
|
||||
- **Caché de Providers**: Los Family providers cachean resultados por parámetro.
|
||||
|
||||
## Mejores Prácticas
|
||||
|
||||
1. **Co-localizar providers**: Colócalos cerca de los widgets que los consumen.
|
||||
2. **Usar generación de código**: Usa siempre `@riverpod`.
|
||||
3. **Mantener providers enfocados**: Responsabilidad única.
|
||||
4. **Gestionar estados de carga**: Maneja siempre los estados de AsyncValue.
|
||||
5. **Liberar recursos**: Usa `ref.onDispose()` para la limpieza.
|
||||
6. **Evitar árboles de providers profundos**: Mantén el grafo de providers plano.
|
||||
198
docs/src/content/docs/es/principles/terminal.md
Normal file
198
docs/src/content/docs/es/principles/terminal.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: Implementación de la Terminal
|
||||
description: Cómo funciona internamente la terminal SSH
|
||||
---
|
||||
|
||||
La terminal SSH es una de las funciones más complejas, construida sobre un fork personalizado de xterm.dart.
|
||||
|
||||
## Resumen de la Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa de UI de la Terminal │
|
||||
│ - Gestión de pestañas │
|
||||
│ - Teclado virtual │
|
||||
│ - Selección de texto │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Emulador xterm.dart │
|
||||
│ - PTY (Pseudo Terminal) │
|
||||
│ - Emulación VT100/ANSI │
|
||||
│ - Motor de renderizado │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Capa de Cliente SSH │
|
||||
│ - Sesión SSH │
|
||||
│ - Gestión de canales │
|
||||
│ - Streaming de datos │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Servidor Remoto │
|
||||
│ - Proceso de Shell │
|
||||
│ - Ejecución de comandos │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Ciclo de Vida de la Sesión de Terminal
|
||||
|
||||
### 1. Creación de la Sesión
|
||||
|
||||
```dart
|
||||
Future<TerminalSession> createSession(Spi spi) async {
|
||||
// 1. Obtener cliente SSH
|
||||
final client = await genClient(spi);
|
||||
|
||||
// 2. Crear PTY
|
||||
final pty = await client.openPty(
|
||||
term: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
);
|
||||
|
||||
// 3. Inicializar emulador de terminal
|
||||
final terminal = Terminal(
|
||||
backend: PtyBackend(pty),
|
||||
);
|
||||
|
||||
// 4. Configurar manejador de cambio de tamaño
|
||||
terminal.onResize.listen((size) {
|
||||
pty.resize(size.cols, size.rows);
|
||||
});
|
||||
|
||||
return TerminalSession(
|
||||
terminal: terminal,
|
||||
pty: pty,
|
||||
client: client,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Emulación de Terminal
|
||||
|
||||
El fork de xterm.dart proporciona:
|
||||
|
||||
**Emulación VT100/ANSI:**
|
||||
- Movimiento del cursor
|
||||
- Colores (soporte para 256 colores)
|
||||
- Atributos de texto (negrita, subrayado, etc.)
|
||||
- Regiones de desplazamiento
|
||||
- Búfer de pantalla alternativo
|
||||
|
||||
**Renderizado:**
|
||||
- Renderizado basado en líneas
|
||||
- Soporte para texto bidireccional
|
||||
- Soporte para Unicode/emoji
|
||||
- Redibujado optimizado
|
||||
|
||||
### 3. Flujo de Datos
|
||||
|
||||
```
|
||||
Entrada del Usuario
|
||||
↓
|
||||
Teclado Virtual / Teclado Físico
|
||||
↓
|
||||
Emulador de Terminal (tecla → secuencia de escape)
|
||||
↓
|
||||
Canal SSH (envío)
|
||||
↓
|
||||
PTY Remoto
|
||||
↓
|
||||
Shell Remoto
|
||||
↓
|
||||
Salida del Comando
|
||||
↓
|
||||
Canal SSH (recepción)
|
||||
↓
|
||||
Emulador de Terminal (analizar códigos ANSI)
|
||||
↓
|
||||
Renderizado en Pantalla
|
||||
```
|
||||
|
||||
## Sistema de Múltiples Pestañas
|
||||
|
||||
### Gestión de Pestañas
|
||||
|
||||
Las pestañas mantienen su estado durante la navegación:
|
||||
- La conexión SSH se mantiene activa
|
||||
- Se preserva el estado de la terminal
|
||||
- Se mantiene el búfer de desplazamiento
|
||||
- Se retiene el historial de entrada
|
||||
|
||||
## Teclado Virtual
|
||||
|
||||
### Implementación Específica por Plataforma
|
||||
|
||||
**iOS:**
|
||||
- Teclado personalizado basado en UIView
|
||||
- Conmutable con un botón de teclado
|
||||
- Mostrar/ocultar automáticamente basado en el enfoque
|
||||
|
||||
**Android:**
|
||||
- Método de entrada personalizado
|
||||
- Integrado con el teclado del sistema
|
||||
- Botones de acción rápida
|
||||
|
||||
### Botones del Teclado
|
||||
|
||||
| Botón | Acción |
|
||||
|--------|--------|
|
||||
| **Conmutar** | Mostrar/ocultar teclado del sistema |
|
||||
| **Ctrl** | Enviar modificador Ctrl |
|
||||
| **Alt** | Enviar modificador Alt |
|
||||
| **SFTP** | Abrir directorio actual |
|
||||
| **Portapapeles** | Copiar/Pegar sensible al contexto |
|
||||
| **Snippets** | Ejecutar fragmento de código |
|
||||
|
||||
## Selección de Texto
|
||||
|
||||
1. **Pulsación larga**: Entrar en modo selección
|
||||
2. **Arrastrar**: Extender la selección
|
||||
3. **Soltar**: Copiar al portapapeles
|
||||
|
||||
## Fuente y Dimensiones
|
||||
|
||||
### Cálculo de Tamaño
|
||||
|
||||
```dart
|
||||
class TerminalDimensions {
|
||||
static Size calculate(double fontSize, Size screenSize) {
|
||||
final charWidth = fontSize * 0.6; // Relación de aspecto monoespaciada
|
||||
final charHeight = fontSize * 1.2;
|
||||
|
||||
final cols = (screenSize.width / charWidth).floor();
|
||||
final rows = (screenSize.height / charHeight).floor();
|
||||
|
||||
return Size(cols.toDouble(), rows.toDouble());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pellizcar para Ampliar (Pinch-to-Zoom)
|
||||
|
||||
```dart
|
||||
GestureDetector(
|
||||
onScaleStart: () => _baseFontSize = currentFontSize,
|
||||
onScaleUpdate: (details) {
|
||||
final newFontSize = _baseFontSize * details.scale;
|
||||
resize(newFontSize);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Esquema de Colores
|
||||
|
||||
- **Claro (Light)**: Fondo claro, texto oscuro
|
||||
- **Oscuro (Dark)**: Fondo oscuro, texto claro
|
||||
- **AMOLED**: Fondo negro puro
|
||||
|
||||
## Optimizaciones de Rendimiento
|
||||
|
||||
- **Dirty rectangle**: Solo redibujar las regiones cambiadas
|
||||
- **Caché de líneas**: Cachear las líneas renderizadas
|
||||
- **Desplazamiento perezoso (Lazy scrolling)**: Desplazamiento virtual para búferes largos
|
||||
- **Actualizaciones por lotes**: Unificar múltiples escrituras
|
||||
- **Compresión**: Comprimir el búfer de desplazamiento
|
||||
- **Debouncing**: Antirrebote para entradas rápidas
|
||||
45
docs/src/content/docs/es/quick-start.mdx
Normal file
45
docs/src/content/docs/es/quick-start.mdx
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Inicio Rápido
|
||||
description: Comienza a usar Server Box en cuestión de minutos
|
||||
---
|
||||
|
||||
Sigue esta guía de inicio rápido para conectarte a tu primer servidor y comenzar la monitorización.
|
||||
|
||||
## Paso 1: Agregar un Servidor
|
||||
|
||||
1. Abre Server Box
|
||||
2. Toca el botón **+** para agregar un nuevo servidor
|
||||
3. Completa la información del servidor:
|
||||
- **Nombre**: Un nombre descriptivo para tu servidor
|
||||
- **Host**: Dirección IP o nombre de dominio
|
||||
- **Puerto**: Puerto SSH (por defecto: 22)
|
||||
- **Usuario**: Nombre de usuario SSH
|
||||
- **Contraseña o Llave**: Método de autenticación
|
||||
|
||||
4. Toca **Guardar** para agregar el servidor
|
||||
|
||||
## Paso 2: Conectar y Monitorear
|
||||
|
||||
1. Toca en la tarjeta de tu servidor para conectarte
|
||||
2. La aplicación establecerá una conexión SSH
|
||||
3. Verás el estado en tiempo real de:
|
||||
- Uso de CPU
|
||||
- Memoria (RAM) y Swap
|
||||
- Uso de disco
|
||||
- Velocidad de red
|
||||
|
||||
## Paso 3: Explorar Funcionalidades
|
||||
|
||||
Una vez conectado, puedes:
|
||||
|
||||
- **Abrir la Terminal**: Toca el botón de la terminal para obtener acceso SSH completo
|
||||
- **Explorar Archivos**: Usa SFTP para gestionar archivos
|
||||
- **Gestionar Contenedores**: Visualiza y controla contenedores Docker
|
||||
- **Ver Procesos**: Revisa los procesos en ejecución
|
||||
- **Ejecutar Snippets**: Ejecuta comandos guardados
|
||||
|
||||
## Consejos
|
||||
|
||||
- **Autenticación Biométrica**: Activa Face ID / Touch ID / Huella dactilar para un acceso rápido (móvil)
|
||||
- **Widgets en la Pantalla de Inicio**: Agrega widgets de estado del servidor a tu pantalla de inicio (iOS/Android)
|
||||
- **Ejecución en Segundo Plano**: Mantén las conexiones activas en segundo plano (Android)
|
||||
83
docs/src/content/docs/fr/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/fr/advanced/bulk-import.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Importation massive de serveurs
|
||||
description: Importer plusieurs serveurs à partir d'un fichier JSON
|
||||
---
|
||||
|
||||
Importez plusieurs configurations de serveur en une seule fois à l'aide d'un fichier JSON.
|
||||
|
||||
## Format JSON
|
||||
|
||||
:::danger[Avertissement de sécurité]
|
||||
**Ne stockez jamais de mots de passe en clair dans des fichiers !** Cet exemple JSON montre un champ de mot de passe à des fins de démonstration uniquement, mais vous devriez :
|
||||
|
||||
- **Préférer les clés SSH** (`keyId`) au lieu de `pwd` - elles sont plus sûres
|
||||
- **Utiliser des gestionnaires de mots de passe** ou des variables d'environnement si vous devez utiliser des mots de passe
|
||||
- **Supprimer le fichier immédiatement** après l'importation - ne laissez pas traîner des identifiants
|
||||
- **Ajouter au .gitignore** - ne validez jamais de fichiers d'identifiants dans le contrôle de version
|
||||
:::
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Mon serveur",
|
||||
"ip": "example.com",
|
||||
"port": 22,
|
||||
"user": "root",
|
||||
"pwd": "password",
|
||||
"keyId": "",
|
||||
"tags": ["production"],
|
||||
"autoConnect": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Champs
|
||||
|
||||
| Champ | Requis | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Oui | Nom d'affichage |
|
||||
| `ip` | Oui | Domaine ou adresse IP |
|
||||
| `port` | Oui | Port SSH (généralement 22) |
|
||||
| `user` | Oui | Nom d'utilisateur SSH |
|
||||
| `pwd` | Non | Mot de passe (à éviter - utilisez plutôt des clés SSH) |
|
||||
| `keyId` | Non | Nom de la clé SSH (à partir des clés privées - recommandé) |
|
||||
| `tags` | Non | Tags d'organisation |
|
||||
| `autoConnect` | Non | Connexion automatique au démarrage |
|
||||
|
||||
## Étapes d'importation
|
||||
|
||||
1. Créer un fichier JSON avec les configurations de serveur
|
||||
2. Paramètres → Sauvegarde → Importation massive de serveurs
|
||||
3. Sélectionnez votre fichier JSON
|
||||
4. Confirmez l'importation
|
||||
|
||||
## Exemple
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Production",
|
||||
"ip": "prod.example.com",
|
||||
"port": 22,
|
||||
"user": "admin",
|
||||
"keyId": "my-key",
|
||||
"tags": ["production", "web"]
|
||||
},
|
||||
{
|
||||
"name": "Développement",
|
||||
"ip": "dev.example.com",
|
||||
"port": 2222,
|
||||
"user": "dev",
|
||||
"keyId": "dev-key",
|
||||
"tags": ["development"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Conseils
|
||||
|
||||
- **Utilisez des clés SSH** au lieu de mots de passe lorsque cela est possible
|
||||
- **Testez la connexion** après l'importation
|
||||
- **Organisez avec des tags** pour une gestion plus facile
|
||||
- **Supprimez le fichier JSON** après l'importation
|
||||
- **Ne validez jamais** de fichiers JSON contenant des identifiants dans le contrôle de version
|
||||
72
docs/src/content/docs/fr/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/fr/advanced/custom-commands.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Commandes personnalisées
|
||||
description: Afficher la sortie des commandes personnalisées sur la page du serveur
|
||||
---
|
||||
|
||||
Ajoutez des commandes shell personnalisées pour afficher leur sortie sur la page de détails du serveur.
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Paramètres du serveur → Commandes personnalisées
|
||||
2. Entrez les commandes au format JSON
|
||||
|
||||
## Format de base
|
||||
|
||||
```json
|
||||
{
|
||||
"Nom d'affichage": "commande shell"
|
||||
}
|
||||
```
|
||||
|
||||
**Exemple :**
|
||||
```json
|
||||
{
|
||||
"Mémoire": "free -h",
|
||||
"Disque": "df -h",
|
||||
"Uptime": "uptime"
|
||||
}
|
||||
```
|
||||
|
||||
## Visualisation des résultats
|
||||
|
||||
Après la configuration, les commandes personnalisées apparaissent sur la page de détails du serveur et s'actualisent automatiquement.
|
||||
|
||||
## Noms de commandes spéciaux
|
||||
|
||||
### server_card_top_right
|
||||
|
||||
Affichage sur la carte du serveur de la page d'accueil (coin supérieur droit) :
|
||||
|
||||
```json
|
||||
{
|
||||
"server_card_top_right": "votre-commande-ici"
|
||||
}
|
||||
```
|
||||
|
||||
## Conseils
|
||||
|
||||
**Utilisez des chemins absolus :**
|
||||
```json
|
||||
{"Mon script": "/usr/local/bin/mon-script.sh"}
|
||||
```
|
||||
|
||||
**Commandes avec pipe :**
|
||||
```json
|
||||
{"Processus principal": "ps aux | sort -rk 3 | head -5"}
|
||||
```
|
||||
|
||||
**Formater la sortie :**
|
||||
```json
|
||||
{"Charge CPU": "uptime | awk -F'load average:' '{print $2}'"}
|
||||
```
|
||||
|
||||
**Gardez les commandes rapides :** Moins de 5 secondes pour une meilleure expérience.
|
||||
|
||||
**Limiter la sortie :**
|
||||
```json
|
||||
{"Logs": "tail -20 /var/log/syslog"}
|
||||
```
|
||||
|
||||
## Sécurité
|
||||
|
||||
Les commandes s'exécutent avec les permissions de l'utilisateur SSH. Évitez les commandes qui modifient l'état du système.
|
||||
54
docs/src/content/docs/fr/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/fr/advanced/custom-logo.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Logo de serveur personnalisé
|
||||
description: Utiliser des images personnalisées pour les cartes de serveur
|
||||
---
|
||||
|
||||
Affichez des logos personnalisés sur les cartes de serveur à l'aide d'URL d'images.
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Paramètres du serveur → Logo personnalisé
|
||||
2. Entrez l'URL de l'image
|
||||
|
||||
## Espaces réservés d'URL
|
||||
|
||||
### {DIST} - Distribution Linux
|
||||
|
||||
Remplacé automatiquement par la distribution détectée :
|
||||
|
||||
```
|
||||
https://example.com/{DIST}.png
|
||||
```
|
||||
|
||||
Devient : `debian.png`, `ubuntu.png`, `arch.png`, etc.
|
||||
|
||||
### {BRIGHT} - Thème
|
||||
|
||||
Remplacé automatiquement par le thème actuel :
|
||||
|
||||
```
|
||||
https://example.com/{BRIGHT}.png
|
||||
```
|
||||
|
||||
Devient : `light.png` ou `dark.png`
|
||||
|
||||
### Combiner les deux
|
||||
|
||||
```
|
||||
https://example.com/{DIST}-{BRIGHT}.png
|
||||
```
|
||||
|
||||
Devient : `debian-light.png`, `ubuntu-dark.png`, etc.
|
||||
|
||||
## Conseils
|
||||
|
||||
- Utilisez les formats PNG ou SVG
|
||||
- Taille recommandée : 64x64 à 128x128 pixels
|
||||
- Utilisez des URL HTTPS
|
||||
- Gardez des tailles de fichiers réduites
|
||||
|
||||
## Distributions supportées
|
||||
|
||||
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
|
||||
|
||||
Liste complète : [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)
|
||||
64
docs/src/content/docs/fr/advanced/json-settings.md
Normal file
64
docs/src/content/docs/fr/advanced/json-settings.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Paramètres cachés (JSON)
|
||||
description: Accéder aux paramètres avancés via l'éditeur JSON
|
||||
---
|
||||
|
||||
Certains paramètres sont masqués de l'interface utilisateur mais accessibles via l'éditeur JSON.
|
||||
|
||||
## Accès
|
||||
|
||||
Appuyez longuement sur **Paramètres** dans le menu latéral pour ouvrir l'éditeur JSON.
|
||||
|
||||
## Paramètres cachés courants
|
||||
|
||||
### timeOut
|
||||
|
||||
Délai d'attente de connexion en secondes.
|
||||
|
||||
```json
|
||||
{"timeOut": 10}
|
||||
```
|
||||
|
||||
**Type :** entier | **Par défaut :** 5 | **Plage :** 1-60
|
||||
|
||||
### recordHistory
|
||||
|
||||
Enregistrer l'historique (chemins SFTP, etc.).
|
||||
|
||||
```json
|
||||
{"recordHistory": true}
|
||||
```
|
||||
|
||||
**Type :** booléen | **Par défaut :** true
|
||||
|
||||
### textFactor
|
||||
|
||||
Facteur de mise à l'échelle du texte.
|
||||
|
||||
```json
|
||||
{"textFactor": 1.2}
|
||||
```
|
||||
|
||||
**Type :** double | **Par défaut :** 1.0 | **Plage :** 0.8-1.5
|
||||
|
||||
## Trouver plus de paramètres
|
||||
|
||||
Tous les paramètres sont définis dans [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).
|
||||
|
||||
Recherchez :
|
||||
```dart
|
||||
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
|
||||
```
|
||||
|
||||
## ⚠️ Important
|
||||
|
||||
**Avant d'éditer :**
|
||||
- **Créer une sauvegarde** - De mauvais paramètres peuvent empêcher l'ouverture de l'application
|
||||
- **Éditer avec soin** - Le JSON doit être valide
|
||||
|
||||
## Récupération
|
||||
|
||||
Si l'application ne s'ouvre plus après l'édition :
|
||||
1. Effacer les données de l'application (dernier recours)
|
||||
2. Réinstaller l'application
|
||||
3. Restaurer à partir d'une sauvegarde
|
||||
118
docs/src/content/docs/fr/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/fr/advanced/troubleshooting.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Problèmes courants
|
||||
description: Solutions aux problèmes fréquents
|
||||
---
|
||||
|
||||
## Problèmes de connexion
|
||||
|
||||
### SSH ne se connecte pas
|
||||
|
||||
**Symptômes :** Délai d'attente (timeout), connexion refusée, échec d'authentification
|
||||
|
||||
**Solutions :**
|
||||
|
||||
1. **Vérifier le type de serveur :** Seuls les systèmes de type Unix sont supportés (Linux, macOS, Android/Termux)
|
||||
2. **Tester manuellement :** `ssh utilisateur@serveur -p port`
|
||||
3. **Vérifier le pare-feu :** Le port 22 doit être ouvert
|
||||
4. **Vérifier les identifiants :** Nom d'utilisateur et mot de passe/clé corrects
|
||||
|
||||
### Déconnexions fréquentes
|
||||
|
||||
**Symptômes :** Le terminal se déconnecte après une période d'inactivité
|
||||
|
||||
**Solutions :**
|
||||
|
||||
1. **Keep-alive du serveur :**
|
||||
```bash
|
||||
# /etc/ssh/sshd_config
|
||||
ClientAliveInterval 60
|
||||
ClientAliveCountMax 3
|
||||
```
|
||||
|
||||
2. **Désactiver l'optimisation de la batterie :**
|
||||
- MIUI : Batterie → "Pas de restrictions"
|
||||
- Android : Paramètres → Applications → Désactiver l'optimisation
|
||||
- iOS : Activer l'actualisation en arrière-plan
|
||||
|
||||
## Problèmes de saisie
|
||||
|
||||
### Impossible de taper certains caractères
|
||||
|
||||
**Solution :** Paramètres → Type de clavier → Passer à `visiblePassword`
|
||||
|
||||
Note : La saisie CJK (Chinois, Japonais, Coréen) peut ne pas fonctionner après ce changement.
|
||||
|
||||
## Problèmes de l'application
|
||||
|
||||
### L'application plante au démarrage
|
||||
|
||||
**Symptômes :** L'application ne s'ouvre pas, écran noir
|
||||
|
||||
**Causes :** Paramètres corrompus, particulièrement via l'éditeur JSON
|
||||
|
||||
**Solutions :**
|
||||
|
||||
1. **Effacer les données de l'application :**
|
||||
- Android : Paramètres → Applications → ServerBox → Effacer les données
|
||||
- iOS : Supprimer et réinstaller
|
||||
|
||||
2. **Restaurer une sauvegarde :** Importer une sauvegarde créée avant de modifier les paramètres
|
||||
|
||||
### Problèmes de sauvegarde/restauration
|
||||
|
||||
**La sauvegarde ne fonctionne pas :**
|
||||
- Vérifier l'espace de stockage
|
||||
- Vérifier que l'application a les permissions de stockage
|
||||
- Essayer un autre emplacement
|
||||
|
||||
**La restauration échoue :**
|
||||
- Vérifier l'intégrité du fichier de sauvegarde
|
||||
- Vérifier la compatibilité de la version de l'application
|
||||
|
||||
## Problèmes de Widget
|
||||
|
||||
### Le widget ne se met pas à jour
|
||||
|
||||
**iOS :**
|
||||
- Attendre jusqu'à 30 minutes pour le rafraîchissement automatique
|
||||
- Supprimer et rajouter le widget
|
||||
- Vérifier que l'URL se termine par `/status`
|
||||
|
||||
**Android :**
|
||||
- Appuyer sur le widget pour forcer le rafraîchissement
|
||||
- Vérifier que l'ID du widget correspond à la configuration dans les paramètres de l'application
|
||||
|
||||
**watchOS :**
|
||||
- Redémarrer l'application sur la montre
|
||||
- Attendre quelques minutes après un changement de configuration
|
||||
- Vérifier le format de l'URL
|
||||
|
||||
### Le widget affiche une erreur
|
||||
|
||||
- Vérifier que ServerBox Monitor fonctionne sur le serveur
|
||||
- Tester l'URL dans un navigateur
|
||||
- Vérifier les identifiants d'authentification
|
||||
|
||||
## Problèmes de performance
|
||||
|
||||
### L'application est lente
|
||||
|
||||
**Solutions :**
|
||||
- Réduire la fréquence de rafraîchissement dans les paramètres
|
||||
- Vérifier la vitesse du réseau
|
||||
- Désactiver les serveurs inutilisés
|
||||
|
||||
### Utilisation élevée de la batterie
|
||||
|
||||
**Solutions :**
|
||||
- Augmenter les intervalles de rafraîchissement
|
||||
- Désactiver le rafraîchissement en arrière-plan
|
||||
- Fermer les sessions SSH inutilisées
|
||||
|
||||
## Obtenir de l'aide
|
||||
|
||||
Si les problèmes persistent :
|
||||
|
||||
1. **Rechercher dans les Issues GitHub :** https://github.com/lollipopkit/flutter_server_box/issues
|
||||
2. **Créer une nouvelle Issue :** Inclure la version de l'application, la plateforme et les étapes pour reproduire le problème
|
||||
3. **Consulter le Wiki :** Cette documentation et le Wiki GitHub
|
||||
90
docs/src/content/docs/fr/advanced/widgets.md
Normal file
90
docs/src/content/docs/fr/advanced/widgets.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Widgets de l'écran d'accueil
|
||||
description: Ajoutez des widgets d'état du serveur à votre écran d'accueil
|
||||
---
|
||||
|
||||
Nécessite l'installation de [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) sur vos serveurs.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Installez d'abord ServerBox Monitor sur votre serveur. Consultez le [Wiki de ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor/wiki/Home) pour les instructions de configuration.
|
||||
|
||||
Après l'installation, votre serveur doit avoir :
|
||||
- Un point de terminaison HTTP/HTTPS
|
||||
- Un point de terminaison API `/status`
|
||||
- Une authentification facultative
|
||||
|
||||
## Format de l'URL
|
||||
|
||||
```
|
||||
https://votre-serveur.com/status
|
||||
```
|
||||
|
||||
Doit se terminer par `/status`.
|
||||
|
||||
## Widget iOS
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Appuyez longuement sur l'écran d'accueil → Appuyez sur **+**
|
||||
2. Recherchez "ServerBox"
|
||||
3. Choisissez la taille du widget
|
||||
4. Appuyez longuement sur le widget → **Modifier le widget**
|
||||
5. Entrez l'URL se terminant par `/status`
|
||||
|
||||
### Notes
|
||||
|
||||
- Doit utiliser HTTPS (sauf pour les adresses IP locales)
|
||||
- Taux de rafraîchissement maximal : 30 minutes (limite iOS)
|
||||
- Ajoutez plusieurs widgets pour plusieurs serveurs
|
||||
|
||||
## Widget Android
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Appuyez longuement sur l'écran d'accueil → **Widgets**
|
||||
2. Trouvez "ServerBox" → Ajoutez à l'écran d'accueil
|
||||
3. Notez le numéro d'ID du widget affiché
|
||||
4. Ouvrez l'application ServerBox → Paramètres
|
||||
5. Appuyez sur **Configurer le lien du widget d'accueil**
|
||||
6. Ajoutez l'entrée : `Widget ID` = `URL d'état`
|
||||
|
||||
Exemple :
|
||||
- Clé : `17`
|
||||
- Valeur : `https://mon-serveur.com/status`
|
||||
|
||||
7. Appuyez sur le widget sur l'écran d'accueil pour le rafraîchir
|
||||
|
||||
## Widget watchOS
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Ouvrez l'application iPhone → Paramètres
|
||||
2. **Paramètres iOS** → **Application Watch**
|
||||
3. Appuyez sur **Ajouter une URL**
|
||||
4. Entrez l'URL se terminant par `/status`
|
||||
5. Attendez que l'application de la montre se synchronise
|
||||
|
||||
### Notes
|
||||
|
||||
- Essayez de redémarrer l'application de la montre si elle ne se met pas à jour
|
||||
- Vérifiez que le téléphone et la montre sont connectés
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Le widget ne se met pas à jour
|
||||
|
||||
**iOS :** Attendez jusqu'à 30 minutes, puis supprimez et rajoutez-le.
|
||||
**Android :** Appuyez sur le widget pour forcer le rafraîchissement, vérifiez l'ID dans les paramètres.
|
||||
**watchOS :** Redémarrez l'application de la montre, attendez quelques minutes.
|
||||
|
||||
### Le widget affiche une erreur
|
||||
|
||||
- Vérifiez que ServerBox Monitor fonctionne
|
||||
- Testez l'URL dans un navigateur
|
||||
- Vérifiez que l'URL se termine par `/status`
|
||||
|
||||
## Sécurité
|
||||
|
||||
- **Utilisez toujours HTTPS** si possible
|
||||
- **Adresses IP locales uniquement** sur les réseaux de confiance
|
||||
86
docs/src/content/docs/fr/development/architecture.md
Normal file
86
docs/src/content/docs/fr/development/architecture.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Architecture
|
||||
description: Modèles d'architecture et décisions de conception
|
||||
---
|
||||
|
||||
Server Box suit les principes de la Clean Architecture avec une séparation claire entre les couches de données, de domaine et de présentation.
|
||||
|
||||
## Architecture en couches
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Couche Présentation │
|
||||
│ (lib/view/page/) │
|
||||
│ - Pages, Widgets, Contrôleurs │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Couche Logique Métier │
|
||||
│ (lib/data/provider/) │
|
||||
│ - Providers Riverpod │
|
||||
│ - Gestion de l'état │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Couche Données │
|
||||
│ (lib/data/model/, store/) │
|
||||
│ - Modèles, Stockage, Services │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Modèles clés
|
||||
|
||||
### Gestion de l'état : Riverpod
|
||||
|
||||
- **Génération de code** : Utilise `riverpod_generator` pour des providers type-safe
|
||||
- **State Notifiers** : Pour un état mutable avec une logique métier
|
||||
- **Async Notifiers** : Pour les états de chargement et d'erreur
|
||||
- **Stream Providers** : Pour les données en temps réel
|
||||
|
||||
### Modèles immuables : Freezed
|
||||
|
||||
- Tous les modèles de données utilisent Freezed pour l'immuabilité
|
||||
- Types Union pour la représentation de l'état
|
||||
- Sérialisation JSON intégrée
|
||||
- Extensions CopyWith pour les mises à jour
|
||||
|
||||
### Stockage local : Hive
|
||||
|
||||
- **hive_ce** : Édition communautaire de Hive
|
||||
- Pas de `@HiveField` ou `@HiveType` manuel requis
|
||||
- Adaptateurs de type auto-générés
|
||||
- Stockage clé-valeur persistant
|
||||
|
||||
## Injection de dépendances
|
||||
|
||||
Les services et les stores sont injectés via :
|
||||
|
||||
1. **Providers** : Exposer les dépendances à l'UI
|
||||
2. **GetIt** : Localisation de services (le cas échéant)
|
||||
3. **Injection par constructeur** : Dépendances explicites
|
||||
|
||||
## Flux de données
|
||||
|
||||
```
|
||||
Action Utilisateur → Widget → Provider → Service/Store → Mise à jour Modèle → Reconstruction UI
|
||||
```
|
||||
|
||||
1. L'utilisateur interagit avec le widget
|
||||
2. Le widget appelle une méthode du provider
|
||||
3. Le provider met à jour l'état via le service/store
|
||||
4. Le changement d'état déclenche la reconstruction de l'UI
|
||||
5. Le nouvel état est reflété dans le widget
|
||||
|
||||
## Dépendances personnalisées
|
||||
|
||||
Le projet utilise plusieurs forks personnalisés pour étendre les fonctionnalités :
|
||||
|
||||
- **dartssh2** : Fonctionnalités SSH améliorées
|
||||
- **xterm** : Émulateur de terminal avec support mobile
|
||||
- **fl_lib** : Composants UI et utilitaires partagés
|
||||
|
||||
## Threading (Multi-processus)
|
||||
|
||||
- **Isolates** : Calculs lourds hors du thread principal
|
||||
- **paquet computer** : Utilitaires multi-threading
|
||||
- **Async/Await** : Opérations d'E/S non bloquantes
|
||||
116
docs/src/content/docs/fr/development/building.md
Normal file
116
docs/src/content/docs/fr/development/building.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Construction (Building)
|
||||
description: Instructions de construction pour différentes plateformes
|
||||
---
|
||||
|
||||
Server Box utilise un système de construction personnalisé (`fl_build`) pour les constructions multiplateformes.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Flutter SDK (canal stable)
|
||||
- Outils spécifiques à la plateforme (Xcode pour iOS, Android Studio pour Android)
|
||||
- Chaîne d'outils Rust (pour certaines dépendances natives)
|
||||
|
||||
## Construction pour le développement
|
||||
|
||||
```bash
|
||||
# Exécuter en mode développement
|
||||
flutter run
|
||||
|
||||
# Exécuter sur un appareil spécifique
|
||||
flutter run -d <id-appareil>
|
||||
```
|
||||
|
||||
## Construction pour la production
|
||||
|
||||
Le projet utilise `fl_build` pour la construction :
|
||||
|
||||
```bash
|
||||
# Construire pour une plateforme spécifique
|
||||
dart run fl_build -p <plateforme>
|
||||
|
||||
# Plateformes disponibles :
|
||||
# - ios
|
||||
# - android
|
||||
# - macos
|
||||
# - linux
|
||||
# - windows
|
||||
```
|
||||
|
||||
## Constructions spécifiques aux plateformes
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p ios
|
||||
```
|
||||
|
||||
Nécessite :
|
||||
- macOS avec Xcode
|
||||
- CocoaPods
|
||||
- Compte Apple Developer pour la signature
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
dart run fl_build -p android
|
||||
```
|
||||
|
||||
Nécessite :
|
||||
- Android SDK
|
||||
- Java Development Kit
|
||||
- Keystore pour la signature
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
dart run fl_build -p macos
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
dart run fl_build -p linux
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
dart run fl_build -p windows
|
||||
```
|
||||
|
||||
Nécessite Windows avec Visual Studio.
|
||||
|
||||
## Pré/Post Construction
|
||||
|
||||
Le script `make.dart` gère :
|
||||
|
||||
- La génération des métadonnées
|
||||
- Les mises à jour de la chaîne de version
|
||||
- Les configurations spécifiques aux plateformes
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Nettoyage de la construction (Clean Build)
|
||||
|
||||
```bash
|
||||
flutter clean
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### Incompatibilité de version
|
||||
|
||||
Assurez-vous que toutes les dépendances sont compatibles :
|
||||
```bash
|
||||
flutter pub upgrade
|
||||
```
|
||||
|
||||
## Liste de contrôle de publication (Release Checklist)
|
||||
|
||||
1. Mettre à jour la version dans `pubspec.yaml`
|
||||
2. Exécuter la génération de code
|
||||
3. Exécuter les tests
|
||||
4. Construire pour toutes les plateformes cibles
|
||||
5. Tester sur des appareils physiques
|
||||
6. Créer une version (release) GitHub
|
||||
98
docs/src/content/docs/fr/development/codegen.md
Normal file
98
docs/src/content/docs/fr/development/codegen.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Génération de code
|
||||
description: Utiliser build_runner pour la génération de code
|
||||
---
|
||||
|
||||
Server Box utilise intensivement la génération de code pour les modèles, la gestion de l'état et la sérialisation.
|
||||
|
||||
## Quand exécuter la génération de code
|
||||
|
||||
À exécuter après avoir modifié :
|
||||
|
||||
- Des modèles avec l'annotation `@freezed`
|
||||
- Des classes avec `@JsonSerializable`
|
||||
- Des modèles Hive
|
||||
- Des providers avec `@riverpod`
|
||||
- Des localisations (fichiers ARB)
|
||||
|
||||
## Exécuter la génération de code
|
||||
|
||||
```bash
|
||||
# Générer tout le code
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Nettoyer et régénérer
|
||||
dart run build_runner build --delete-conflicting-outputs --clean
|
||||
```
|
||||
|
||||
## Fichiers générés
|
||||
|
||||
### Freezed (`*.freezed.dart`)
|
||||
|
||||
Modèles de données immuables avec types Union :
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
class ServerState with _$ServerState {
|
||||
const factory ServerState.connected() = Connected;
|
||||
const factory ServerState.disconnected() = Disconnected;
|
||||
const factory ServerState.error(String message) = Error;
|
||||
}
|
||||
```
|
||||
|
||||
### Sérialisation JSON (`*.g.dart`)
|
||||
|
||||
Généré à partir de `json_serializable` :
|
||||
|
||||
```dart
|
||||
@JsonSerializable()
|
||||
class Server {
|
||||
final String id;
|
||||
final String name;
|
||||
final String host;
|
||||
|
||||
Server({required this.id, required this.name, required this.host});
|
||||
|
||||
factory Server.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ServerToJson(this);
|
||||
}
|
||||
```
|
||||
|
||||
### Providers Riverpod (`*.g.dart`)
|
||||
|
||||
Généré à partir de l'annotation `@riverpod` :
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class MyNotifier extends _$MyNotifier {
|
||||
@override
|
||||
int build() => 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Adaptateurs Hive (`*.g.dart`)
|
||||
|
||||
Auto-générés pour les modèles Hive (hive_ce) :
|
||||
|
||||
```dart
|
||||
@HiveType(typeId: 0)
|
||||
class ServerModel {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
}
|
||||
```
|
||||
|
||||
## Génération de localisation
|
||||
|
||||
```bash
|
||||
flutter gen-l10n
|
||||
```
|
||||
|
||||
Génère `lib/generated/l10n/` à partir des fichiers `lib/l10n/*.arb`.
|
||||
|
||||
## Conseils
|
||||
|
||||
- Utilisez `--delete-conflicting-outputs` pour éviter les conflits
|
||||
- Ajoutez les fichiers générés au `.gitignore`
|
||||
- Ne modifiez jamais manuellement les fichiers générés
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user