From 7e7f1f89f363c31bbb7aa5af3f34a8b5c32a7a19 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Wed, 27 May 2026 15:16:51 +0800 Subject: [PATCH] Fix macos performance issue Support custom global-ua Update core Optimize some details --- .agents/agent-config.md | 28 + .agents/architecture.md | 156 ++++ .agents/commands.md | 106 +++ .agents/project.md | 28 + .agents/rules.md | 47 ++ .agents/skills.md | 17 + .agents/skills/core-platform/SKILL.md | 33 + .agents/skills/localization/SKILL.md | 45 ++ .agents/skills/provider-tests/SKILL.md | 37 + .agents/skills/ui-work/SKILL.md | 32 + .claude/commands/i18n.md | 93 --- .codegraph/.gitignore | 5 + .gemini/settings.json | 12 + .github/homebrew_cask_template.rb | 39 + .github/release_template.md | 2 +- .github/workflows/build.yaml | 77 +- .gitmodules | 10 - .mcp.json | 12 + .pre-commit-config.yaml | 20 + .zed/settings.json | 13 + AGENTS.md | 246 +----- CLAUDE.md | 7 +- Makefile | 33 + README.md | 9 +- README_zh_CN.md | 7 + android/app/build.gradle.kts | 5 +- android/build.gradle.kts | 15 + android/common/build.gradle.kts | 3 +- android/common/consumer-rules.pro | 0 android/core/build.gradle.kts | 1 - android/gradle.properties | 4 + android/gradle/libs.versions.toml | 11 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/service/build.gradle.kts | 4 +- .../service/models/NotificationParams.kt | 31 +- .../follow/clash/service/models/VpnOptions.kt | 90 ++- android/settings.gradle.kts | 8 +- arb/intl_en.arb | 15 +- arb/intl_ja.arb | 15 +- arb/intl_ru.arb | 15 +- arb/intl_zh_CN.arb | 15 +- assets/images/icon/status_2.ico | Bin 2014 -> 2143 bytes assets/images/icon/status_2.png | Bin 8455 -> 10683 bytes assets/images/icon/status_3.ico | Bin 2353 -> 2579 bytes assets/images/icon/status_3.png | Bin 10748 -> 12676 bytes assets_source/images/icon/status_1.svg | 40 + assets_source/images/icon/status_2.svg | 36 + assets_source/images/icon/status_3.svg | 40 + core/Clash.Meta | 2 +- core/action.go | 16 +- core/common.go | 23 +- core/constant.go | 18 +- core/go.mod | 32 +- core/go.sum | 79 +- core/hub.go | 46 +- lib/common/common.dart | 2 +- lib/common/fixed.dart | 16 +- lib/common/input_limits.dart | 38 + lib/common/iterable.dart | 7 + lib/common/keyboard.dart | 168 ++-- lib/common/link.dart | 23 +- lib/common/package.dart | 8 +- lib/common/picker.dart | 20 +- lib/common/pipe.dart | 234 ------ lib/common/protocol.dart | 50 +- lib/common/request.dart | 60 +- lib/common/system.dart | 1 + lib/common/task.dart | 10 +- lib/common/text.dart | 9 +- lib/core/controller.dart | 4 +- lib/core/event.dart | 16 + lib/core/interface.dart | 6 +- lib/core/transport.dart | 84 +- lib/enum/enum.dart | 36 +- lib/features/overwrite/rule.dart | 5 +- lib/l10n/intl/messages_en.dart | 89 ++- lib/l10n/intl/messages_ja.dart | 87 +- lib/l10n/intl/messages_ru.dart | 89 ++- lib/l10n/intl/messages_zh_CN.dart | 87 +- lib/l10n/l10n.dart | 115 +++ lib/main.dart | 4 +- lib/manager/core_manager.dart | 23 +- lib/manager/hotkey_manager.dart | 6 +- lib/manager/theme_manager.dart | 55 +- lib/manager/window_manager.dart | 9 +- lib/models/clash_config.dart | 76 +- lib/models/common.dart | 3 +- lib/models/config.dart | 1 + lib/models/core.dart | 2 + .../generated/clash_config.freezed.dart | 354 +-------- lib/models/generated/clash_config.g.dart | 30 +- lib/models/generated/config.freezed.dart | 43 +- lib/models/generated/config.g.dart | 2 + lib/models/generated/core.freezed.dart | 46 +- lib/models/generated/core.g.dart | 5 + lib/pages/editor.dart | 10 +- lib/pages/home.dart | 48 +- lib/pages/scan.dart | 5 +- lib/plugins/app.dart | 8 +- lib/providers/action.dart | 35 +- lib/providers/database.dart | 44 +- lib/providers/generated/action.g.dart | 58 +- lib/providers/generated/app.g.dart | 2 +- lib/providers/generated/database.g.dart | 14 +- lib/providers/generated/state.g.dart | 4 +- lib/providers/state.dart | 2 + lib/state.dart | 13 +- lib/views/application_setting.dart | 5 +- lib/views/backup_and_restore.dart | 8 +- lib/views/config/dns.dart | 13 + lib/views/config/general.dart | 254 +++++- lib/views/config/network.dart | 2 + lib/views/config/on_demand.dart | 10 +- lib/views/config/rules.dart | 7 +- lib/views/config/scripts.dart | 1 + lib/views/dashboard/dashboard.dart | 5 +- lib/views/dashboard/widgets/widgets.dart | 2 +- lib/views/logs.dart | 4 +- lib/views/profiles/add.dart | 2 + lib/views/profiles/edit.dart | 9 +- .../profiles/overwrite/custom/groups.dart | 15 +- lib/views/profiles/overwrite/custom/icon.dart | 3 + .../profiles/overwrite/custom/proxies.dart | 12 +- .../overwrite/custom/proxy_providers.dart | 9 +- .../profiles/overwrite/custom/rules.dart | 5 +- .../profiles/overwrite/custom/widgets.dart | 4 +- lib/views/profiles/overwrite/standard.dart | 2 +- lib/views/profiles/preview.dart | 6 +- lib/views/profiles/profiles.dart | 8 +- lib/views/proxies/providers.dart | 6 +- lib/views/resources.dart | 383 +++++---- lib/views/theme.dart | 135 +--- lib/views/tools.dart | 6 +- lib/widgets/activate_box.dart | 11 +- lib/widgets/animate_grid.dart | 103 ++- lib/widgets/bar_chart.dart | 51 +- lib/widgets/chip.dart | 10 +- lib/widgets/container.dart | 16 +- lib/widgets/dialog.dart | 20 +- lib/widgets/disabled_mask.dart | 11 +- lib/widgets/dismissible.dart | 2 +- lib/widgets/donut_chart.dart | 13 +- lib/widgets/float_layout.dart | 13 +- lib/widgets/input.dart | 47 +- lib/widgets/keep_scope.dart | 6 +- lib/widgets/list.dart | 24 +- lib/widgets/notification.dart | 16 +- lib/widgets/palette.dart | 746 +++++++++--------- lib/widgets/scaffold.dart | 1 + lib/widgets/sheet.dart | 12 +- lib/widgets/theme.dart | 108 +++ lib/widgets/wave.dart | 47 +- macos/Podfile.lock | 24 - opencode.jsonc | 14 + plugins/flutter_distributor | 1 - plugins/rust_api/android/build.gradle | 15 +- plugins/setup/android/build.gradle | 8 +- .../build_tool/lib/src/rust_builder.dart | 7 +- .../buildkit/build_tool/lib/src/util.dart | 3 +- .../buildkit/build_tool/test/util_test.dart | 3 +- plugins/setup/buildkit/run_build_tool.cmd | 2 + plugins/setup/buildkit/run_build_tool.sh | 2 + plugins/tray_manager | 1 - plugins/wifi_ssid/android/build.gradle | 12 +- plugins/wifi_ssid/macos/wifi_ssid.podspec | 20 - .../wifi_ssid/macos/wifi_ssid/Package.swift | 30 + .../Sources/wifi_ssid}/WifiSsidPlugin.swift | 4 +- plugins/window_ext/lib/window_ext.dart | 2 +- plugins/window_ext/macos/window_ext.podspec | 23 - .../window_ext/macos/window_ext/Package.swift | 26 + .../Sources/window_ext}/WindowExtPlugin.swift | 12 +- pubspec.lock | 225 +++--- pubspec.yaml | 42 +- setup.dart | 37 +- test/common/converter_test.dart | 50 ++ test/common/datetime_test.dart | 29 + test/common/delay_test.dart | 28 +- test/common/iterable_test.dart | 22 + test/common/picker_test.dart | 29 + test/common/protocol_test.dart | 19 + test/core/controller_test.dart | 5 +- test/enum/enum_test.dart | 114 +++ test/models/common_test.dart | 150 ++++ test/models/config_test.dart | 111 +++ test/models/profile_test.dart | 107 +++ test/providers/action_test.dart | 25 + test/providers/app_test.dart | 112 ++- test/providers/config_test.dart | 100 ++- test/widgets/input_test.dart | 111 +++ test/widgets/state_test.dart | 95 +++ tool/bump_version.sh | 60 ++ tool/generate_status_icons.dart | 115 +++ 192 files changed, 4904 insertions(+), 2910 deletions(-) create mode 100644 .agents/agent-config.md create mode 100644 .agents/architecture.md create mode 100644 .agents/commands.md create mode 100644 .agents/project.md create mode 100644 .agents/rules.md create mode 100644 .agents/skills.md create mode 100644 .agents/skills/core-platform/SKILL.md create mode 100644 .agents/skills/localization/SKILL.md create mode 100644 .agents/skills/provider-tests/SKILL.md create mode 100644 .agents/skills/ui-work/SKILL.md delete mode 100644 .claude/commands/i18n.md create mode 100644 .codegraph/.gitignore create mode 100644 .gemini/settings.json create mode 100644 .github/homebrew_cask_template.rb create mode 100644 .mcp.json create mode 100644 .pre-commit-config.yaml create mode 100644 .zed/settings.json create mode 100644 Makefile create mode 100644 android/common/consumer-rules.pro create mode 100644 assets_source/images/icon/status_1.svg create mode 100644 assets_source/images/icon/status_2.svg create mode 100644 assets_source/images/icon/status_3.svg create mode 100644 lib/common/input_limits.dart delete mode 100644 lib/common/pipe.dart create mode 100644 opencode.jsonc delete mode 160000 plugins/flutter_distributor delete mode 160000 plugins/tray_manager delete mode 100644 plugins/wifi_ssid/macos/wifi_ssid.podspec create mode 100644 plugins/wifi_ssid/macos/wifi_ssid/Package.swift rename plugins/wifi_ssid/macos/{Classes => wifi_ssid/Sources/wifi_ssid}/WifiSsidPlugin.swift (100%) delete mode 100644 plugins/window_ext/macos/window_ext.podspec create mode 100644 plugins/window_ext/macos/window_ext/Package.swift rename plugins/window_ext/macos/{Classes => window_ext/Sources/window_ext}/WindowExtPlugin.swift (87%) mode change 100755 => 100644 pubspec.yaml create mode 100644 test/common/converter_test.dart create mode 100644 test/common/datetime_test.dart create mode 100644 test/common/picker_test.dart create mode 100644 test/common/protocol_test.dart create mode 100644 test/enum/enum_test.dart create mode 100644 test/models/common_test.dart create mode 100644 test/models/profile_test.dart create mode 100644 test/widgets/input_test.dart create mode 100644 test/widgets/state_test.dart create mode 100755 tool/bump_version.sh create mode 100644 tool/generate_status_icons.dart diff --git a/.agents/agent-config.md b/.agents/agent-config.md new file mode 100644 index 00000000..bcd0b158 --- /dev/null +++ b/.agents/agent-config.md @@ -0,0 +1,28 @@ +# Agent Configuration Model + +Use the smallest durable surface that matches the job. + +## Surfaces + +- `AGENTS.md`: auto-loaded repository entry point. Keep it small and reserve it for always-on rules, routing, and high-priority expectations. +- `.agents/*.md`: human- and agent-readable reference docs linked from `AGENTS.md`. Use these for detailed project context, commands, architecture, and conventions. +- `.agents/skills/*/SKILL.md`: repo-scoped Codex skills. Use these for repeatable workflows that should be discoverable by name and description. +- `.codex/config.toml`: trusted project Codex settings such as MCP, hooks, sandbox, approval, or model defaults. +- `.codex/rules/*.rules`: command permission rules for running commands outside the sandbox. Do not mix these with coding conventions in `.agents/rules.md`. +- `.codex/hooks.json` or `.codex/config.toml` hooks: lifecycle automation such as prompt checks, command validation, or final verification gates. + +## Placement Rules + +- Put stable team conventions in `AGENTS.md` only when every task must see them without opening another file. +- Put detailed explanations in `.agents/*.md` and link them from `AGENTS.md`. +- Put reusable task workflows in `.agents/skills//SKILL.md`. +- Put mechanical enforcement in linters, tests, hooks, or `.codex/rules`; do not rely on prose when tooling can enforce the rule. +- Keep user-specific preferences out of the repository. They belong in user-level Codex configuration or user-level skills. + +## Skill Authoring Rules + +- Use lowercase hyphenated names. +- Start descriptions with `Use when...`. +- Describe trigger conditions in the description, not the workflow. +- Keep `SKILL.md` lean; link to `.agents/*.md` for large reference material. +- Add scripts only when deterministic behavior is needed repeatedly. diff --git a/.agents/architecture.md b/.agents/architecture.md new file mode 100644 index 00000000..dc541683 --- /dev/null +++ b/.agents/architecture.md @@ -0,0 +1,156 @@ +# Architecture + +## Core Integration + +The Go proxy core in `core/` operates in two modes. + +Android lib mode: + +- Go core is compiled as a C shared library, `libclash.so`, through `go build -buildmode=c-shared` with CGO. +- Flutter calls it via FFI through the `service` plugin. +- Dart-side implementation: `lib/core/lib.dart` (`CoreLib`). + +Desktop core mode: + +- Go core runs as a separate process with `CGO_ENABLED=0`. +- Flutter communicates via JSON over socket, using a Unix socket on macOS/Linux and TCP on Windows. +- Dart-side implementation: `lib/core/service.dart` (`CoreService`). + +`lib/core/controller.dart` (`CoreController`) selects the implementation based on platform. `lib/core/interface.dart` defines the shared `CoreHandlerInterface`. + +Key Go core files: + +- `core/hub.go`: handler functions. +- `core/action.go`: dispatch. +- `core/lib.go`: CGO exports. +- `core/server.go`: socket server. + +## State Management + +Provider files in `lib/providers/`: + +- `app.dart`: runtime/UI state, logs, traffic, delays, loading, navigation. +- `config.dart`: persistent config providers, app settings, theme, VPN, proxy style. +- `state.dart`: derived/computed providers, navigation, proxy, tray, color scheme. +- `action.dart`: business logic notifiers, setup, backup, core lifecycle, proxy selection. +- `database.dart`: Drift database provider wrappers. + +`globalState` in `lib/state.dart` is a singleton holding app lifecycle, timers, theme, and start/stop state. Providers are generated into `lib/providers/generated/`. + +## Database + +The app uses Drift/SQLite in `lib/database/`. Current schema version is 2. + +Tables: + +- `Profiles` +- `Scripts` +- `Rules` +- `ProfileRuleLinks` (`profile_rule_mapping`) +- `ProxyGroups` +- `IconRecords` (`icon_records`) + +Rule scenes distinguish global added rules, profile added rules, profile custom rules, and disabled links. Rule and proxy-group ordering use fractional indexing. + +Generated Drift output lives in `lib/database/generated/database.g.dart`. After schema changes, run code generation and add or update focused database tests under `test/database/` when converter or migration behavior changes. + +## Manager Stack + +Managers are nested `InheritedWidget`/`StatefulWidget` components in `lib/application.dart`: + +```text +AppEnvManager > StatusManager > ThemeManager + > [Desktop: WindowManager > TrayManager > HotKeyManager > ProxyManager] + > ConnectivityManager > CoreManager > AppStateManager + > [Mobile: AndroidManager > VpnManager | Desktop: WindowHeaderContainer] +``` + +Each manager in `lib/manager/` handles a specific platform concern. Desktop-only managers are conditionally inserted. + +## Core Controller and Actions + +`lib/core/controller.dart` (`CoreController`) is a singleton facade over `CoreHandlerInterface`. Public methods delegate to the platform-specific interface, either Android FFI or desktop socket. It has an `@visibleForTesting` constructor and `resetInstance()` for test injection. + +Business logic lives in Riverpod notifier classes in `lib/providers/action.dart`: + +- `CommonAction`: update check and common UI operations. +- `SetupAction`: config setup and TUN management. +- `BackupAction`: backup/restore with WebDAV sync. +- `CoreAction`: core lifecycle, init, connect, restart, shutdown. +- `SystemAction`: system integration, tray, exit, brightness. +- `StoreAction`: profile storage operations. +- `ThemeAction`: theme state updates. +- `ProxiesAction`: group management and proxy selection. +- `ProfilesAction`: profile CRUD, auto-update, import. + +## Platform Managers + +Desktop: + +- `WindowManager` +- `TrayManager` +- `HotKeyManager` +- `ProxyManager` + +Mobile: + +- `AndroidManager` +- `TileManager` +- `VpnManager` + +Shared: + +- `ConnectivityManager` +- `CoreManager` +- `AppStateManager` +- `StatusManager` +- `ThemeManager` + +## Build System + +`setup.dart` is the release build orchestrator: + +1. On Windows, pre-builds Go core via `dart run build_tool windows` and reads `core_sha256.json`. +2. Writes `env.json` (`APP_ENV`). +3. Passes SHA256 as `--dart-define=CORE_SHA256=$val`, embedded at compile time for Windows. +4. Activates `flutter_distributor` for packaging. + +Go core building is handled by `build_tool`, a standalone Dart CLI in `plugins/setup/buildkit/build_tool/`. + +Platform build hooks inside `flutter build` trigger `build_tool` automatically: + +- macOS: podspec script phase, `build_pod.sh`, `build_tool macos`. +- Linux: CMake include, `buildkit/cmake/buildkit.cmake`, `build_tool linux`. +- Windows: CMake include, `buildkit/cmake/buildkit.cmake`, `build_tool windows`. Debug passes `--dev` via `CMAKE_BUILD_TYPE`. +- Android: Gradle include, `buildkit/gradle/plugin.gradle`, `build_tool android`. + +Windows helper auth: + +- Release: Core SHA256 is embedded in both the Flutter app and the Rust helper. The app pings the helper and verifies the token matches. +- Debug: The Rust helper skips token verification when built in debug mode, so `flutter run` works without the SHA256 flow. + +`plugins/setup/` is an FFI plugin that exists only as a build harness. It carries no Dart API, only platform build hooks that trigger Go compilation. Windows builds also compile a Rust helper in `services/helper/` through `RustBuilder`. + +Build configuration defaults live in `build_tool/lib/src/options.dart` and can be overridden via `build_config.yaml`. + +Architecture detection is automatic. The `--description` flag passed to `flutter_distributor` adds arch suffixes to artifact names, such as `FlClash-0.8.93-macos-arm64.dmg`. + +## Local Plugins + +- `setup`: build harness FFI plugin. +- `proxy`: system proxy configuration. +- `rust_api`: Flutter Rust Bridge FFI plugin. +- `tray_manager`: system tray fork/customization. +- `wifi_ssid`: Wi-Fi SSID detection. +- `window_ext`: window extensions. +- `flutter_distributor`: app packaging/distribution. + +## Rust Helper Service + +`services/helper/` is a Windows-only privileged helper for starting the core as admin and managing TUN. It is built with: + +```bash +cargo build --release --features windows-service +``` + +It uses token-based auth with the Flutter app. diff --git a/.agents/commands.md b/.agents/commands.md new file mode 100644 index 00000000..6de8c960 --- /dev/null +++ b/.agents/commands.md @@ -0,0 +1,106 @@ +# Commands + +## Building + +Update submodules first. The ClashMeta Go core lives in `core/Clash.Meta/`. + +```bash +git submodule update --init --recursive +``` + +Full package build, including Go core, Flutter, and packaging, runs through `setup.dart`: + +```bash +dart setup.dart macos +dart setup.dart linux +dart setup.dart windows +dart setup.dart android +``` + +Build only the Go core and skip Flutter packaging: + +```bash +make core-macos +make core-linux +make core-windows +make core-android +``` + +Pass `ARCH` or `TARGET_PLATFORM` through `make` when needed, for example: + +```bash +make core-macos ARCH=arm64 +make core-android TARGET_PLATFORM=android-arm64 +``` + +The Makefile wraps `plugins/setup/buildkit/run_build_tool.sh`; prefer the `make` entry points unless debugging the build tool itself. + +## Flutter Development + +The project is pinned with FVM. + +```bash +fvm flutter pub get +fvm flutter run +fvm flutter test +``` + +Plain Flutter also works when the global SDK matches project constraints: + +```bash +flutter pub get +flutter run +flutter test +``` + +Use `flutter test`, not `dart test`, because models pull in Flutter types. + +## Code Generation + +Run code generation after modifying models, providers, or database schema: + +```bash +dart run build_runner build --delete-conflicting-outputs +dart run build_runner watch +``` + +Code generation covers: + +- Riverpod providers through `riverpod_generator`. +- Models through `freezed` and `json_serializable`. +- Database tables through `drift_dev`. + +Generated output paths, configured in `build.yaml`: + +- `lib/models/generated/*.g.dart`, `*.freezed.dart`. +- `lib/providers/generated/*.g.dart`. +- `lib/database/generated/*.g.dart`. + +## Testing + +Tests use `package:test/test.dart` for pure Dart logic and `flutter_test` for provider and widget tests. `mocktail` is the mocking framework. + +```bash +flutter test test/models/ +flutter test test/core/ +flutter test test/providers/ +flutter test test/common/ +flutter test test/database/ +flutter test test/widgets/ +flutter test test/setup_test.dart +flutter test plugins/proxy/test/proxy_test.dart +``` + +Root `flutter test` only discovers the root package's `test/` directory by default. Include bundled plugin Dart tests by passing paths explicitly, or run `flutter test` from that plugin package directory. Native plugin tests under platform folders are not run by `flutter test`. + +## Verify + +CI runs these in order: + +```bash +flutter pub get +flutter analyze --no-fatal-infos +flutter test --reporter expanded +``` + +Run `flutter analyze` locally before committing when practical. diff --git a/.agents/project.md b/.agents/project.md new file mode 100644 index 00000000..9ca9529f --- /dev/null +++ b/.agents/project.md @@ -0,0 +1,28 @@ +# Project Context + +FlClash is a multi-platform proxy client based on ClashMeta (mihomo), built with Flutter. It supports Android, Windows, macOS, and Linux, using a Material You design with Surfboard-like UI. + +## Version Notes + +- `.fvmrc` pins Flutter 3.35.7 for local development. +- CI uses Flutter 3.41.9. These may diverge; trust CI as the source of truth for release builds. +- Dart SDK constraint: `>=3.8.0 <4.0.0`. + +## Build Dependencies + +Linux: + +```bash +sudo apt-get install libayatana-appindicator3-dev libkeybinder-3.0-dev +``` + +Windows: + +- GCC and Inno Setup. +- `ANDROID_NDK` env var for Android builds. + +macOS: + +```bash +npm install -g appdmg +``` diff --git a/.agents/rules.md b/.agents/rules.md new file mode 100644 index 00000000..fef46309 --- /dev/null +++ b/.agents/rules.md @@ -0,0 +1,47 @@ +# Rules + +These are repository coding and testing conventions. Codex command permission rules belong in `.codex/rules/*.rules`; see `.agents/agent-config.md` before adding those. + +## Dart and Flutter Style + +`analysis_options.yaml` enforces these non-default rules: + +- `prefer_single_quotes: true`: always use single quotes. +- `require_trailing_commas: true`: use trailing commas in multi-line argument lists. +- `sort_child_properties_last: true`: `child:` must be the last named parameter. +- `avoid_print: true`: do not use `print()` calls. +- `prefer_const_constructors: true` and `prefer_const_declarations: true`. +- `prefer_final_locals: true` and `prefer_final_in_for_each: true`. +- `always_declare_return_types: true`. + +Generated directories are excluded from analysis: + +- `build/**` +- `lib/l10n/intl/**` +- `lib/**/generated/**` +- `plugins/**` + +## Testing Rules + +Use `CoreController.test(mock)` to inject a mocked `CoreHandlerInterface`. Call `CoreController.resetInstance()` in `tearDown` to clean up the singleton between tests. + +Register fallback values for freezed params used with `any()` matchers. + +Use `ProviderContainer` directly for simple Riverpod provider tests. The generated Riverpod `update()` method takes a callback: + +```dart +notifier.update((state) => newValue); +``` + +When testing freezed models with nested objects, always round-trip through `jsonEncode` and `jsonDecode`. Direct `fromJson(toJson())` fails for nested freezed types because `toJson()` stores child objects directly instead of maps. + +## Generated Code + +Do not manually edit generated files under: + +- `lib/models/generated/` +- `lib/providers/generated/` +- `lib/database/generated/` +- `lib/l10n/intl/` + +After schema, model, or provider changes, run build generation and include focused tests when behavior changes. diff --git a/.agents/skills.md b/.agents/skills.md new file mode 100644 index 00000000..09ad53e6 --- /dev/null +++ b/.agents/skills.md @@ -0,0 +1,17 @@ +# Agent Skills + +Repo-scoped Codex skills live under `.agents/skills/*/SKILL.md`. Codex can discover these by skill `name` and `description`, then load the full instructions only when a task matches. + +## Available Repo Skills + +- `localization`: hardcoded UI text scans, ARB updates, locale generation, and localization verification. +- `provider-tests`: Riverpod provider, notifier, and state-management tests. +- `ui-work`: Flutter UI, widgets, Material You styling, navigation surfaces, and user-facing interactions. +- `core-platform`: core integration, platform managers, Go core communication, desktop/mobile behavior, and Windows helper flow. + +## Authoring Notes + +- Add new repeatable workflows as `.agents/skills//SKILL.md`. +- Keep skill descriptions trigger-focused and start them with `Use when...`. +- Keep long reference material in `.agents/*.md`; skills should link to it instead of duplicating it. +- Put command permission rules in `.codex/rules/*.rules`, not in this file. diff --git a/.agents/skills/core-platform/SKILL.md b/.agents/skills/core-platform/SKILL.md new file mode 100644 index 00000000..02c23612 --- /dev/null +++ b/.agents/skills/core-platform/SKILL.md @@ -0,0 +1,33 @@ +--- +name: core-platform +description: Use when changing FlClash core integration, platform managers, Go core communication, desktop/mobile platform behavior, or Windows helper flow. +--- + +# Core And Platform + +## When To Use + +Use this for changes touching `lib/core/`, `lib/manager/`, `core/`, `services/helper/`, build hooks, system proxy, tray, VPN, TUN, or platform-specific desktop/mobile behavior. + +## Workflow + +1. Identify which boundary owns the behavior: + - Android lib mode: `lib/core/lib.dart`. + - Desktop process/socket mode: `lib/core/service.dart`. + - Shared facade: `lib/core/controller.dart` and `lib/core/interface.dart`. + - Platform lifecycle: `lib/manager/`. +2. Route feature code through `CoreController` and `CoreHandlerInterface`; avoid direct calls to platform implementations outside their boundary. +3. Keep desktop and mobile paths explicit. +4. For action-layer behavior, inspect `lib/providers/action.dart` and relevant generated providers. +5. Add or update shared Dart tests for logic that can be isolated. +6. Manually verify native behavior when automated coverage is not practical. + +## Reference Files + +Read `.agents/architecture.md` for the current core modes, manager stack, build hooks, local plugins, and Windows helper notes. + +## Pitfalls + +- Debug Windows helper auth differs from release token verification. +- `plugins/setup/` is a build harness, not a Dart API plugin. +- Build hooks can trigger Go or Rust compilation indirectly through Flutter platform builds. diff --git a/.agents/skills/localization/SKILL.md b/.agents/skills/localization/SKILL.md new file mode 100644 index 00000000..95a3e404 --- /dev/null +++ b/.agents/skills/localization/SKILL.md @@ -0,0 +1,45 @@ +--- +name: localization +description: Use when changing FlClash UI text, scanning for hardcoded Chinese strings, updating ARB localization, or fixing generated locale output in this repository. +--- + +# Localization + +## When To Use + +Use this for localization work inside `/Users/Shared/follow/FlClash`, especially hardcoded UI text in `lib/`, ARB updates, missing translations, or generated `lib/l10n/` output that does not match source ARB values. + +Do not use this for README translation sync or manual edits to generated localization Dart. + +## Workflow + +1. Confirm `pubspec.yaml` still uses `flutter_intl`, source ARBs under `arb/`, and generated output under `lib/l10n/`. +2. Scan user-facing Dart text before opening many files: + + ```bash + rg -n "[\\p{Han}]" lib -g '!lib/l10n/intl/**' -g '!lib/**/generated/**' + ``` + +3. Inspect the smallest relevant call sites and nearby ARB keys. +4. Add or update every source ARB: + - `arb/intl_en.arb` + - `arb/intl_zh_CN.arb` + - `arb/intl_ja.arb` + - `arb/intl_ru.arb` +5. Replace inline strings with existing project accessors: + - Widgets with `BuildContext`: `context.appLocalizations.key` from `common.dart`. + - Controllers/providers/non-widget code: `currentAppLocalizations.key` from `app_localizations.dart`. +6. Regenerate: + + ```bash + dart run intl_utils:generate + ``` + +7. Verify changed Dart files with `flutter analyze` when practical. +8. Re-run the Han-text scan for targeted strings. + +## Pitfalls + +- If ja/ru still show English, fix ja/ru source ARB values and regenerate. Do not edit generated Dart. +- Ignore `lib/l10n/intl/**` and `lib/**/generated/**` during text scans. +- If generator or analyzer hits local cache permission friction, rerun serially before treating it as a code issue. diff --git a/.agents/skills/provider-tests/SKILL.md b/.agents/skills/provider-tests/SKILL.md new file mode 100644 index 00000000..6b6cfb41 --- /dev/null +++ b/.agents/skills/provider-tests/SKILL.md @@ -0,0 +1,37 @@ +--- +name: provider-tests +description: Use when adding or updating FlClash Riverpod provider tests, notifier tests, or state-management tests in this repository. +--- + +# Provider Tests + +## When To Use + +Use this for tests under `test/providers/` or any change that validates Riverpod providers, generated notifiers, app state defaults, or provider interactions. + +For broader test expansion, pair this with `.agents/rules.md` and `.agents/commands.md`. + +## Workflow + +1. Read the provider under test and its generated public API before writing assertions. +2. Use `ProviderContainer` directly when no widget tree is needed. +3. Dispose containers in teardown or with `addTearDown(container.dispose)`. +4. Prefer generated notifier APIs over implementation details. Generated `update()` takes a callback: + + ```dart + notifier.update((state) => newValue); + ``` + +5. Mock external dependencies with `mocktail`; register fallback values for freezed params used with `any()`. +6. Keep tests focused on behavior: defaults, state transitions, persistence boundaries, and side effects. +7. Run the narrowest relevant test first: + + ```bash + flutter test test/providers/ + ``` + +## Pitfalls + +- Do not use `dart test`; FlClash models and provider tests may depend on Flutter types. +- Re-check source defaults before asserting them; provider defaults can drift. +- If async provider timing matters, wait on provider futures or state changes instead of fixed sleeps. diff --git a/.agents/skills/ui-work/SKILL.md b/.agents/skills/ui-work/SKILL.md new file mode 100644 index 00000000..c2cab0a2 --- /dev/null +++ b/.agents/skills/ui-work/SKILL.md @@ -0,0 +1,32 @@ +--- +name: ui-work +description: Use when changing FlClash Flutter UI, widgets, screens, Material You styling, navigation surfaces, or user-facing interactions. +--- + +# UI Work + +## When To Use + +Use this for user-facing Flutter UI changes in `lib/`, including widgets, screens, navigation surfaces, settings rows, dialogs, and interaction behavior. + +## Workflow + +1. Locate existing nearby widgets and reuse their patterns before adding new abstractions. +2. Follow current Material You and Surfboard-like visual conventions. +3. Use existing providers, notifiers, and helpers where possible. +4. Keep `child:` last in widget constructors. +5. Prefer `const` constructors and final locals. +6. Localize user-facing text through ARB; use `localization` when text changes are non-trivial. +7. Add focused widget tests when behavior changes, especially for rendering states, taps, scrolling, and empty/error states. +8. Run targeted verification: + + ```bash + flutter analyze + flutter test test/widgets/ + ``` + +## Pitfalls + +- Do not introduce a new visual system for one screen. +- Do not manually edit generated localization or provider files. +- Avoid broad layout rewrites unless the requested change requires them. diff --git a/.claude/commands/i18n.md b/.claude/commands/i18n.md deleted file mode 100644 index d85b8148..00000000 --- a/.claude/commands/i18n.md +++ /dev/null @@ -1,93 +0,0 @@ -Scan `lib/` for hardcoded Chinese strings and internationalize them following the project's ARB conventions. - -## Instructions - -You are performing i18n extraction for a Flutter project that uses the Flutter Intl plugin with ARB files. - -### Project localization context - -- **ARB files**: `arb/intl_en.arb`, `arb/intl_zh_CN.arb`, `arb/intl_ja.arb`, `arb/intl_ru.arb` -- **Key conventions**: camelCase, flat keys (no nesting, no `@` metadata), semantic naming - - Short labels: `rule`, `global`, `direct`, `edit`, `confirm` - - Descriptive suffix `Desc`: `themeDesc`, `tunDesc`, `logsDesc` - - Validation suffix: `profileNameNullValidationDesc` -- **Generated code**: `lib/l10n/l10n.dart` (DO NOT edit generated files) -- **Access patterns**: - - In widgets with BuildContext: `context.appLocalizations.key` (import `common.dart`) - - In controllers/providers/non-widget code: `currentAppLocalizations.key` (import `app_localizations.dart`) - -### Step 1: Find hardcoded Chinese strings - -Use Grep to search `lib/` for Chinese characters. The regex pattern for Chinese characters is `[\x{4e00}-\x{9fff}]`. - -Important filters: -- **Exclude** `lib/l10n/` (generated localization files) -- **Exclude** lines that already use `appLocalizations` or `currentAppLocalizations` -- **Exclude** false positives that are NOT Chinese text: - - Arrow symbols: `↑` `↓` `←` `→` (Unicode arrows, not CJK) - - Middle dot: `·` (U+00B7, not CJK) - - Command symbol: `⌘` (U+2318, not CJK) - - Other CJK punctuation that is used as UI symbols, not translatable text - -For each match, read the surrounding context (5-10 lines) to understand: -- What the string represents (label, error message, tooltip, dialog text, etc.) -- Whether it's inside a widget (has `BuildContext` available) or non-widget code -- Whether it appears in a `Text()`, `TextSpan()`, `title:`, `label:`, or other text position - -### Step 2: Generate ARB keys - -For each hardcoded Chinese string: - -1. **Choose a key name** following project conventions: - - Use the Chinese meaning to derive an English camelCase key - - Keep it concise: `createConfig` not `createAConfigurationForTheProfile` - - Use `Desc` suffix for descriptive/explanatory text - - Use existing similar keys as reference (check `arb/intl_en.arb` for patterns) - -2. **Determine the English translation** (the value for `intl_en.arb`) - -3. **Determine the Chinese translation** (the value for `intl_zh_CN.arb`) — this is the original hardcoded string - -4. **For ja and ru**: use the English translation as a placeholder (translators will fill these in later) - -### Step 3: Update ARB files - -For each new key, add it to all 4 ARB files. Read each ARB file first to find the right insertion point (alphabetical order is not required, but appending near the end is clean). - -Format: add a new line like `"keyName": "value",` before the closing `}`. - -### Step 4: Replace hardcoded strings in Dart files - -Replace each hardcoded Chinese string with the appropriate localization call: - -- If inside a widget BuildContext: `context.appLocalizations.keyName` -- If in controller/provider code: `currentAppLocalizations.keyName` - -Ensure the correct import exists: -- For `context.appLocalizations`: the file should import `common.dart` (check if it already does) -- For `currentAppLocalizations`: the file should import `app_localizations.dart` (check if it already does) - -If the file uses string interpolation (e.g., `'创建 $name 配置'`), convert to the ARB parameterized format: -- In ARB: `"keyName": "Create $name config"` -- In Dart: `currentAppLocalizations.keyName(name)` - -### Step 5: Report changes - -After all modifications, output a summary: -1. List each file modified and what changed -2. List each new ARB key added with its translations -3. Remind the user to run code generation if needed: - ``` - dart run build_runner build --delete-conflicting-outputs - ``` - or trigger Flutter Intl regeneration in their IDE. - -### Important notes - -- Only extract strings that are user-facing UI text. Do NOT extract: - - Comments (lines starting with `//`) - - Log messages (unless they appear in the UI) - - Variable names or identifiers - - String literals used as keys/identifiers (not displayed to users) -- If a Chinese string is split across multiple lines or concatenations, merge them into a single ARB entry -- If a string contains dynamic content (variables, URLs), use ARB parameter syntax `$variableName` diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 00000000..d20c0fe4 --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 00000000..87ca7dea --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "codegraph": { + "type": "stdio", + "command": "codegraph", + "args": [ + "serve", + "--mcp" + ] + } + } +} diff --git a/.github/homebrew_cask_template.rb b/.github/homebrew_cask_template.rb new file mode 100644 index 00000000..3a9a493a --- /dev/null +++ b/.github/homebrew_cask_template.rb @@ -0,0 +1,39 @@ +cask "flclash" do + version "VERSION" + + on_macos do + arch arm: "arm64", intel: "amd64" + + sha256 arm: "ARM_SHA256", + intel: "AMD_SHA256" + + url "https://github.com/chen08209/FlClash/releases/download/v#{version}/FlClash-#{version}-macos-#{arch}.dmg" + end + + name "FlClash" + desc "Multi-platform proxy client based on ClashMeta" + homepage "https://github.com/chen08209/FlClash" + + livecheck do + url :url + strategy :github_latest + end + + depends_on :macos + + app "FlClash.app" + + postflight do + system_command "xattr", + args: ["-rd", "com.apple.quarantine", "#{appdir}/FlClash.app"] + end + + uninstall quit: "com.follow.clash" + + zap trash: [ + "~/Library/Application Support/com.follow.clash", + "~/Library/Caches/com.follow.clash", + "~/Library/Preferences/com.follow.clash.plist", + "~/Library/Saved Application State/com.follow.clash.savedState", + ] +end diff --git a/.github/release_template.md b/.github/release_template.md index d1300848..c52b363d 100644 --- a/.github/release_template.md +++ b/.github/release_template.md @@ -42,7 +42,7 @@

- + diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index abb91b98..a33fba3d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,6 +6,7 @@ on: - 'v*' env: IS_STABLE: ${{ !contains(github.ref, '-') }} + FLUTTER_VERSION: '3.44.4' jobs: test: @@ -21,7 +22,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.41.9 + flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Install dependencies @@ -63,6 +64,7 @@ jobs: steps: - name: Setup rust if: startsWith(matrix.os, 'windows-11-arm') + shell: pwsh run: | Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe .\rustup-init.exe -y --default-toolchain stable @@ -79,14 +81,16 @@ jobs: run: | echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks echo "${{ secrets.SERVICE_JSON }}" | base64 --decode > android/app/google-services.json - echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties - echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties - echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties + { + echo "keyAlias=${{ secrets.KEY_ALIAS }}" + echo "storePassword=${{ secrets.STORE_PASSWORD }}" + echo "keyPassword=${{ secrets.KEY_PASSWORD }}" + } >> android/local.properties - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.24.0' + go-version: '1.26.4' cache-dependency-path: | core/go.sum @@ -95,20 +99,18 @@ jobs: run: git config --global core.longpaths true - name: Setup Flutter - if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }} - uses: subosito/flutter-action@v2 - with: - channel: stable - flutter-version: 3.41.9 - cache: true - - name: Setup Flutter With Other - if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm') uses: subosito/flutter-action@v2 with: channel: master - flutter-version: 3.44.0-0.1.pre + flutter-version: ${{ env.FLUTTER_VERSION }} cache: true + - name: Setup Xcode + if: startsWith(matrix.platform,'macos') + run: | + sudo xcode-select -s /Applications/Xcode_26.3.app || sudo xcode-select -s /Applications/Xcode_26.2.app + xcodebuild -version + - name: Setup NDK if: startsWith(matrix.platform,'android') uses: nttld/setup-ndk@v1 @@ -133,17 +135,18 @@ jobs: overwrite: true changelog: + if: ${{ !contains(github.ref, '-') }} + permissions: + contents: write runs-on: ubuntu-latest needs: [ build ] steps: - name: Checkout uses: actions/checkout@v4 - if: ${{ env.IS_STABLE == 'true' }} with: fetch-depth: 0 ref: refs/heads/main - name: Generate - if: ${{ env.IS_STABLE == 'true' }} run: | last_ver=$(grep -m1 '^## ' CHANGELOG.md 2>/dev/null | sed 's/^## //') @@ -167,7 +170,6 @@ jobs: mv "$temp" CHANGELOG.md - name: Commit - if: ${{ env.IS_STABLE == 'true' }} run: | git add CHANGELOG.md if ! git diff --cached --quiet; then @@ -175,8 +177,7 @@ jobs: git config --local user.email "chen08209@gmail.com" git config --local user.name "chen08209" git commit -m "Update changelog" - git push - if [ $? -eq 0 ]; then + if git push; then echo "Push succeeded" else echo "Push failed" @@ -188,7 +189,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} upload: - permissions: write-all + permissions: + contents: write needs: [ build ] runs-on: ubuntu-latest services: @@ -251,7 +253,7 @@ jobs: if: ${{ env.IS_STABLE == 'true' }} run: | cd ./dist - sha256sum * > SHA256SUMS + sha256sum ./* > SHA256SUMS - name: Release if: ${{ env.IS_STABLE == 'true' }} @@ -260,6 +262,35 @@ jobs: files: ./dist/* body_path: './release.md' + - name: Create Homebrew Cask Source Dir + if: ${{ env.IS_STABLE == 'true' }} + run: | + version=$(echo "${{ github.ref_name }}" | sed 's/^v//') + arm_sha=$(sha256sum "./dist/FlClash-${version}-macos-arm64.dmg" | awk '{print $1}') + amd_sha=$(sha256sum "./dist/FlClash-${version}-macos-amd64.dmg" | awk '{print $1}') + + mkdir -p ./brew/Casks + sed \ + -e "s|VERSION|$version|g" \ + -e "s|ARM_SHA256|$arm_sha|g" \ + -e "s|AMD_SHA256|$amd_sha|g" \ + ./.github/homebrew_cask_template.rb > ./brew/Casks/flclash.rb + + - name: Push to Homebrew tap + if: ${{ env.IS_STABLE == 'true' }} + uses: cpina/github-action-push-to-another-repository@v1.7.2 + env: + SSH_DEPLOY_KEY: ${{ secrets.HOMEBREW_SSH_DEPLOY_KEY }} + with: + source-directory: ./brew/ + destination-github-username: chen08209 + destination-repository-name: homebrew-tap + user-name: 'chen08029' + user-email: 'chen08209@gmail.com' + target-branch: main + commit-message: Update Homebrew cask from ${{ github.ref_name }} + target-directory: / + - name: Create Fdroid Source Dir if: ${{ env.IS_STABLE == 'true' }} run: | @@ -276,8 +307,8 @@ jobs: source-directory: ./tmp/ destination-github-username: chen08209 destination-repository-name: FlClash-fdroid-repo - user-name: 'github-actions[bot]' - user-email: 'github-actions[bot]@users.noreply.github.com' + user-name: 'chen08029' + user-email: 'chen08209@gmail.com' target-branch: main commit-message: Update from ${{ github.ref_name }} target-directory: /tmp/ diff --git a/.gitmodules b/.gitmodules index 70bf7cd8..f373ba78 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,13 +2,3 @@ path = core/Clash.Meta url = git@github.com:chen08209/Clash.Meta.git branch = FlClash -[submodule "plugins/flutter_distributor"] - path = plugins/flutter_distributor - url = git@github.com:chen08209/flutter_distributor.git - branch = FlClash -[submodule "plugins/tray_manager"] - path = plugins/tray_manager - url = git@github.com:chen08209/tray_manager.git - branch = main - - diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..87ca7dea --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "codegraph": { + "type": "stdio", + "command": "codegraph", + "args": [ + "serve", + "--mcp" + ] + } + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..1d977681 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: local + hooks: + - id: dart-format + name: dart format + entry: dart format + language: system + files: \.dart$ + - id: flutter-analyze + name: flutter analyze + entry: flutter analyze --no-fatal-infos + language: system + pass_filenames: false + files: \.dart$ + - id: flutter-test + name: flutter test + entry: flutter test --reporter expanded + language: system + pass_filenames: false + files: \.dart$ diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..1bd257a7 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,13 @@ +{ + "lsp": { + "gopls": { + "binary": { + "env": { + "CGO_ENABLED": "0", + // "GOOS": "android", + // "GOARCH": "arm64", + }, + }, + }, + }, +} diff --git a/AGENTS.md b/AGENTS.md index fba9e5d9..9a555441 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,233 +1,35 @@ # AGENTS.md -This file provides guidance for AI coding agents working with code in this repository. +This file is the entry point for AI coding agents working in this repository. Keep it small: detailed guidance lives under +`.agents/`, and discoverable repo skills live under `.agents/skills/*/SKILL.md`. -## Project Overview +## Start Here -FlClash is a multi-platform proxy client based on ClashMeta (mihomo), built with Flutter. Supports Android, Windows, -macOS, and Linux. Material You design with Surfboard-like UI. +Read these files before making changes: -## Common Development Commands +- [.agents/project.md](.agents/project.md): project overview, versions, and build dependencies. +- [.agents/commands.md](.agents/commands.md): build, development, code generation, and test commands. +- [.agents/rules.md](.agents/rules.md): lint, testing, generated-code, and workflow rules. -### Building +Read these only when the task touches their area: -```bash -# Update submodules first (ClashMeta Go core lives in core/Clash.Meta/) -git submodule update --init --recursive +- [.agents/architecture.md](.agents/architecture.md): core integration, providers, database, managers, build system, and + local plugins. +- [.agents/agent-config.md](.agents/agent-config.md): how to choose between `AGENTS.md`, `.agents`, skills, Codex config, + command rules, and hooks. +- [.agents/skills.md](.agents/skills.md): index of repo-scoped skills in `.agents/skills/`. -# Full package build (Go core + Flutter + packaging) via setup.dart -dart setup.dart macos -dart setup.dart linux -dart setup.dart windows -dart setup.dart android +## Highest Priority Rules -# Build only the Go core (skip Flutter packaging) -bash plugins/setup/buildkit/run_build_tool.sh macos -bash plugins/setup/buildkit/run_build_tool.sh linux -bash plugins/setup/buildkit/run_build_tool.sh windows -bash plugins/setup/buildkit/run_build_tool.sh android -``` +- Use `flutter test`, not `dart test`, because models pull in Flutter types. +- Run code generation after modifying models, providers, or database schema. +- Do not manually edit generated files. +- Follow `analysis_options.yaml`, especially single quotes, trailing commas, `child:` last, no `print()`, const/final + preferences, and declared return types. +- For CI parity, verify with `flutter pub get`, `flutter analyze --no-fatal-infos`, and + `flutter test --reporter expanded` when practical. -### Flutter Development +## Repo Skills -```bash -# Project is pinned with FVM (.fvmrc currently uses Flutter 3.35.7) -fvm flutter pub get -fvm flutter run -fvm flutter test - -# Plain Flutter also works when your global SDK matches the project constraints -flutter pub get -flutter run # Run on connected device/desktop -flutter test # Run all tests (use flutter test, not dart test — models pull in Flutter types) -``` - -### Code Generation - -Required after modifying models, providers, or database schema: - -```bash -dart run build_runner build --delete-conflicting-outputs -dart run build_runner watch # Continuous regeneration -``` - -Code generation covers: Riverpod providers (`riverpod_generator`), models (`freezed`, `json_serializable`), and database -tables (`drift_dev`). - -### Testing - -Tests use `package:test/test.dart` for pure Dart logic (common utils, models) and `flutter_test` for provider/widget tests. -`mocktail` is the mocking framework. - -```bash -flutter test test/models/ # Model serialization & extension round-trip tests -flutter test test/core/ # CoreController tests (mocked CoreHandlerInterface) -flutter test test/providers/ # Riverpod provider tests (config & app state notifiers) -flutter test test/common/ # Utility function tests (utils, string, iterable, fixed, etc.) -flutter test test/database/ # Database type converter tests -flutter test test/widgets/ # Widget-level rendering/interaction tests -flutter test test/setup_test.dart -flutter test plugins/proxy/test/proxy_test.dart # Dart tests for bundled plugin packages -``` - -Root `flutter test` only discovers the root package's `test/` directory by default. Include bundled plugin Dart tests by -passing their paths explicitly, or run `flutter test` from that plugin package directory. Native plugin tests under -platform folders (for example Windows C++ tests) are not run by `flutter test`. - -**Mocking `CoreHandlerInterface`:** Use `CoreController.test(mock)` to inject a mock interface. Call -`CoreController.resetInstance()` in `tearDown` to clean up the singleton between tests. Remember to -`registerFallbackValue()` for freezed params used with `any()` matchers. - -**Provider tests:** Use `ProviderContainer` directly (no widget tree needed for simple notifiers). The Riverpod -generated `update()` method takes a callback: `notifier.update((state) => newValue)`. - -**Model round-trip tests:** Always go through `jsonEncode`/`jsonDecode` when testing freezed models with -nested objects — `toJson()` stores child objects directly (not as maps), so direct `fromJson(toJson())` -fails for nested freezed types. - -### Build Dependencies - -**Linux:** `sudo apt-get install libayatana-appindicator3-dev libkeybinder-3.0-dev` - -**Windows:** GCC and Inno Setup. `ANDROID_NDK` env var for Android builds. - -**macOS:** `npm install -g appdmg` for DMG creation. - -## Architecture - -### Core Integration (Go ClashMeta <-> Flutter) - -This is the most important architectural concept. The Go proxy core (`core/`) operates in two modes: - -- **Android (lib mode):** Go core compiled as C shared library (`libclash.so`) via `go build -buildmode=c-shared` with - CGO. Flutter calls it via FFI through the `service` plugin. Dart-side: `lib/core/lib.dart` (`CoreLib` class). - -- **Desktop (core mode):** Go core runs as a separate process with `CGO_ENABLED=0`. Flutter communicates via - JSON-over-socket (Unix socket on macOS/Linux, TCP on Windows). Dart-side: `lib/core/service.dart` (`CoreService` - class). - -`lib/core/controller.dart` (`CoreController`) selects the implementation based on platform. `lib/core/interface.dart` -defines the shared `CoreHandlerInterface`. - -Go core key files: `core/hub.go` (handler functions), `core/action.go` (dispatch), `core/lib.go` (CGO exports), -`core/server.go` (socket server). - -### State Management (Riverpod) - -Provider files in `lib/providers/`: - -- `app.dart` - Runtime/UI state (logs, traffic, delays, loading, navigation) -- `config.dart` - Persistent config providers (app settings, theme, VPN, proxy style) -- `state.dart` - Derived/computed providers (navigation, proxy, tray, color scheme) -- `action.dart` - Business logic notifiers (setup, backup, core lifecycle, proxy selection) -- `database.dart` - Drift database provider wrappers - -`globalState` (`lib/state.dart`) is a singleton holding app lifecycle, timers, theme, and the start/stop state. -Providers are generated into `lib/providers/generated/`. - -### Database (Drift/SQLite) - -Type-safe SQLite via Drift in `lib/database/`. Current schema version is 2. Tables are `Profiles`, `Scripts`, `Rules`, -`ProfileRuleLinks` (`profile_rule_mapping`), `ProxyGroups`, and `IconRecords` (`icon_records`). Rule scenes distinguish -global added rules, profile added rules, profile custom rules, and disabled links. Uses fractional indexing for rule and -proxy-group ordering. - -Generated Drift output lives in `lib/database/generated/database.g.dart`. After schema changes, run code generation and -add/update focused database tests under `test/database/` when converter or migration behavior changes. - -### Manager Stack (Widget Tree) - -Managers are nested InheritedWidgets/StatefulWidgets in `lib/application.dart`: - -``` -AppEnvManager > StatusManager > ThemeManager - > [Desktop: WindowManager > TrayManager > HotKeyManager > ProxyManager] - > ConnectivityManager > CoreManager > AppStateManager - > [Mobile: AndroidManager > VpnManager | Desktop: WindowHeaderContainer] -``` - -Each manager in `lib/manager/` handles a specific platform concern. Desktop-only managers are conditionally inserted. - -### Core Controller + Actions - -`lib/core/controller.dart` (`CoreController`) is a singleton facade over `CoreHandlerInterface`. All 25+ public methods -delegate to the platform-specific interface (Android FFI or desktop socket). Has `@visibleForTesting` constructor and -`resetInstance()` for test injection. - -Business logic lives in Riverpod notifier classes in `lib/providers/action.dart` (~960 lines, should be split): - -- `CommonAction` — update check, common UI operations -- `SetupAction` — config setup, TUN management -- `BackupAction` — backup/restore with WebDAV sync -- `CoreAction` — core lifecycle (init, connect, restart, shutdown) -- `SystemAction` — system integration (tray, exit, brightness) -- `StoreAction` — profile storage operations -- `ThemeAction` — theme state updates -- `ProxiesAction` — group management, proxy selection -- `ProfilesAction` — profile CRUD, auto-update, import - -### Platform Managers (`lib/manager/`) - -Desktop: `WindowManager`, `TrayManager`, `HotKeyManager`, `ProxyManager` -Mobile: `AndroidManager`, `TileManager`, `VpnManager` -Shared: `ConnectivityManager`, `CoreManager`, `AppStateManager`, `StatusManager`, `ThemeManager` - -### Build System - -`setup.dart` (project root) is the release build orchestrator: - -1. On Windows, pre-builds Go core via `dart run build_tool windows` and reads `core_sha256.json` -2. Writes `env.json` (APP_ENV) -3. Passes SHA256 as `--dart-define=CORE_SHA256=$val` (compile-time embedded, secure; Windows only) -4. Activates `flutter_distributor` for packaging - -Go core building is handled by `build_tool`, a standalone Dart CLI in `plugins/setup/buildkit/build_tool/`. -Platform build hooks inside `flutter build` trigger `build_tool` automatically: - -- **macOS:** podspec script phase → `build_pod.sh` → `build_tool macos` -- **Linux:** CMake include → `buildkit/cmake/buildkit.cmake` → `build_tool linux` -- **Windows:** CMake include → `buildkit/cmake/buildkit.cmake` → `build_tool windows` (debug: `--dev` via `CMAKE_BUILD_TYPE`) -- **Android:** Gradle include → `buildkit/gradle/plugin.gradle` → `build_tool android` - -**Windows helper auth (release):** Core SHA256 is embedded in both the Flutter app (`--dart-define`) and the Rust -helper (`TOKEN` env var during cargo build). The Dart app pings the helper and verifies the token matches. - -**Windows helper auth (debug):** The Rust helper skips token verification when built in debug mode -(`cfg!(debug_assertions)`), so `flutter run` works without any SHA256 dance. - -`plugins/setup/` is an FFI plugin that exists solely as a build harness — it carries no Dart API, only platform build hooks -(podspec, CMake, Gradle) that trigger Go compilation. Windows builds also compile a Rust helper (`services/helper/`) via -`RustBuilder`. - -Build configuration defaults live in `build_tool/lib/src/options.dart` and can be overridden via `build_config.yaml` -in the project root. - -Architecture detection is automatic (host arch via `uname -m` on Unix, `PROCESSOR_ARCHITECTURE` on Windows). The -`--description` flag passed to flutter_distributor adds arch suffix to artifact names (e.g., -`FlClash-0.8.93-macos-arm64.dmg`). - -### Local Plugins (`plugins/`) - -- `setup` - Build harness FFI plugin (triggers Go/Rust compilation per platform) -- `proxy` - System proxy configuration -- `rust_api` - Flutter Rust Bridge FFI plugin (named pipe / local socket communication) -- `tray_manager` - System tray (forked/custom) -- `wifi_ssid` - Wi-Fi SSID detection -- `window_ext` - Window extensions -- `flutter_distributor` - App packaging/distribution - -### Rust Helper Service (`services/helper/`) - -Windows-only privileged helper for starting the core as admin and managing TUN. Built with -`cargo build --release --features windows-service`. Token-based auth with Flutter app. - -### Localization - -ARB files in `arb/`. Generated via `flutter_intl` into `lib/l10n/`. Use `AppLocalizations.of(context)!` for strings. - -**Supported locales:** `en`, `zh_CN`, `ja`, `ru` - -**Access patterns:** - -- In widgets with BuildContext: `context.appLocalizations.key` (import `common.dart`) -- In controllers/providers/non-widget code: `currentAppLocalizations.key` (import `app_localizations.dart`) +Use repo skills from `.agents/skills/` when a task matches their descriptions. Current skills cover localization, +provider tests, UI work, and core/platform changes. diff --git a/CLAUDE.md b/CLAUDE.md index 71993a20..8de06f90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,4 @@ -# CLAUDE.md - Claude Code instructions +# CLAUDE.md -Please read AGENTS.md for all project guidelines and conventions. - -@AGENTS.md \ No newline at end of file +This repository uses [AGENTS.md](AGENTS.md) as the canonical agent entry point. +Read that file first, then follow the `.agents/` references it routes to. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c1bef1db --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +SHELL := /bin/bash + +PLATFORM ?= macos +BUILDKIT := plugins/setup/buildkit/run_build_tool.sh +ARCH_ARG := $(if $(ARCH),--arch $(ARCH),) +TARGET_PLATFORM_ARG := $(if $(TARGET_PLATFORM),--target-platform $(TARGET_PLATFORM),) + +.PHONY: help submodules core core-macos core-linux core-windows core-android + +help: + @echo 'make core # build macOS core by default' + @echo 'make core PLATFORM=linux ARCH=amd64' + @echo 'make core-macos ARCH=arm64' + @echo 'make core-android ARCH=arm64' + @echo 'make core-android TARGET_PLATFORM=android-arm64' + +submodules: + git submodule update --init --recursive + +core: + bash $(BUILDKIT) $(PLATFORM) $(ARCH_ARG) $(TARGET_PLATFORM_ARG) + +core-macos: + $(MAKE) core PLATFORM=macos + +core-linux: + $(MAKE) core PLATFORM=linux + +core-windows: + $(MAKE) core PLATFORM=windows + +core-android: + $(MAKE) core PLATFORM=android diff --git a/README.md b/README.md index 2e148be1..26b6b975 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,13 @@ Support the following actions Get it on F-Droid Get it on GitHub +### Homebrew + +```bash +brew tap chen08209/tap +brew install --cask flclash +``` + ## Build 1. Update submodules @@ -129,4 +136,4 @@ The easiest way to support developers is to click on the star (⭐) at the top o start -

\ No newline at end of file +

diff --git a/README_zh_CN.md b/README_zh_CN.md index df192ebb..eb517a60 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -61,6 +61,13 @@ on Mobile: Get it on F-Droid Get it on GitHub +### Homebrew + +```bash +brew tap chen08209/tap +brew install --cask flclash +``` + ## Build 1. 更新 submodules diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4cef3e8a..b64ccea8 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -3,14 +3,13 @@ import java.util.Properties plugins { id("com.android.application") - id("kotlin-android") id("dev.flutter.flutter-gradle-plugin") id("com.google.gms.google-services") id("com.google.firebase.crashlytics") } -val localPropertiesFile = rootProject.file("local.properties") val localProperties = Properties().apply { + val localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { localPropertiesFile.inputStream().use { load(it) } } @@ -106,4 +105,4 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.crashlytics.ndk) implementation(libs.firebase.analytics) -} \ No newline at end of file +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbee657b..18fecbd9 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -15,10 +15,25 @@ subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } + subprojects { project.evaluationDependsOn(":app") } +gradle.projectsEvaluated { + allprojects { + tasks.withType().configureEach { + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() + } + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } + } +} + tasks.register("clean") { delete(rootProject.layout.buildDirectory) } diff --git a/android/common/build.gradle.kts b/android/common/build.gradle.kts index b46b3a60..35e2c7c6 100644 --- a/android/common/build.gradle.kts +++ b/android/common/build.gradle.kts @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.library") - id("org.jetbrains.kotlin.android") } android { @@ -42,4 +41,4 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.crashlytics.ndk) implementation(libs.firebase.analytics) -} \ No newline at end of file +} diff --git a/android/common/consumer-rules.pro b/android/common/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/android/core/build.gradle.kts b/android/core/build.gradle.kts index 593a503c..915d5c96 100644 --- a/android/core/build.gradle.kts +++ b/android/core/build.gradle.kts @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.library") - id("org.jetbrains.kotlin.android") } android { diff --git a/android/gradle.properties b/android/gradle.properties index 598d13fe..f2122b21 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index f425f9a8..ed7237ad 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] #agp = "8.10.1" -firebaseBom = "34.2.0" +firebaseBom = "34.15.0" minSdk = "23" targetSdk = "36" compileSdk = "36" @@ -9,13 +9,12 @@ coreKtx = "1.17.0" annotationJvm = "1.9.1" coreSplashscreen = "1.0.1" gson = "2.13.1" -kotlin = "2.2.10" +kotlin = "2.2.20" smaliDexlib2 = "3.0.9" -firebaseCrashlyticsKtx = "20.0.1" -firebaseCommonKtx = "22.0.0" +firebaseCrashlyticsKtx = "20.0.6" +firebaseCommonKtx = "22.1.0" [libraries] -build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } annotation-jvm = { module = "androidx.annotation:annotation-jvm", version.ref = "annotationJvm" } core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } @@ -25,4 +24,4 @@ firebase-crashlytics-ndk = { module = "com.google.firebase:firebase-crashlytics- gson = { module = "com.google.code.gson:gson", version.ref = "gson" } smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" } firebase-crashlytics-ktx = { group = "com.google.firebase", name = "firebase-crashlytics-ktx", version.ref = "firebaseCrashlyticsKtx" } -firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx", version.ref = "firebaseCommonKtx" } \ No newline at end of file +firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx", version.ref = "firebaseCommonKtx" } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6064aebd..c1501a7e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip diff --git a/android/service/build.gradle.kts b/android/service/build.gradle.kts index 8d9637b8..ac715317 100644 --- a/android/service/build.gradle.kts +++ b/android/service/build.gradle.kts @@ -2,8 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-parcelize") } android { @@ -45,4 +43,4 @@ dependencies { implementation(project(":common")) implementation(libs.gson) implementation(libs.androidx.core) -} \ No newline at end of file +} diff --git a/android/service/src/main/java/com/follow/clash/service/models/NotificationParams.kt b/android/service/src/main/java/com/follow/clash/service/models/NotificationParams.kt index 7196005d..dc18eeb4 100644 --- a/android/service/src/main/java/com/follow/clash/service/models/NotificationParams.kt +++ b/android/service/src/main/java/com/follow/clash/service/models/NotificationParams.kt @@ -1,11 +1,36 @@ package com.follow.clash.service.models +import android.os.Parcel import android.os.Parcelable -import kotlinx.parcelize.Parcelize -@Parcelize data class NotificationParams( val title: String = "FlClash", val stopText: String = "STOP", val onlyStatisticsProxy: Boolean = false, -) : Parcelable \ No newline at end of file +) : Parcelable { + constructor(parcel: Parcel) : this( + title = parcel.readString() ?: "FlClash", + stopText = parcel.readString() ?: "STOP", + onlyStatisticsProxy = parcel.readByte() != 0.toByte(), + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(title) + parcel.writeString(stopText) + parcel.writeByte(if (onlyStatisticsProxy) 1.toByte() else 0.toByte()) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): NotificationParams { + return NotificationParams(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/android/service/src/main/java/com/follow/clash/service/models/VpnOptions.kt b/android/service/src/main/java/com/follow/clash/service/models/VpnOptions.kt index 0e2b4acf..27264aa0 100644 --- a/android/service/src/main/java/com/follow/clash/service/models/VpnOptions.kt +++ b/android/service/src/main/java/com/follow/clash/service/models/VpnOptions.kt @@ -1,19 +1,45 @@ package com.follow.clash.service.models +import android.os.Parcel import android.os.Parcelable import com.follow.clash.common.AccessControlMode -import kotlinx.parcelize.Parcelize import java.net.InetAddress -@Parcelize data class AccessControlProps( val enable: Boolean, val mode: AccessControlMode, val acceptList: List, val rejectList: List, -) : Parcelable +) : Parcelable { + constructor(parcel: Parcel) : this( + enable = parcel.readByte() != 0.toByte(), + mode = AccessControlMode.valueOf(parcel.readString() ?: AccessControlMode.ACCEPT_SELECTED.name), + acceptList = parcel.createStringArrayList() ?: emptyList(), + rejectList = parcel.createStringArrayList() ?: emptyList(), + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeByte(if (enable) 1.toByte() else 0.toByte()) + parcel.writeString(mode.name) + parcel.writeStringList(acceptList) + parcel.writeStringList(rejectList) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): AccessControlProps { + return AccessControlProps(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} -@Parcelize data class VpnOptions( val enable: Boolean, val port: Int, @@ -25,7 +51,59 @@ data class VpnOptions( val bypassDomain: List, val stack: String, val routeAddress: List, -) : Parcelable +) : Parcelable { + constructor(parcel: Parcel) : this( + enable = parcel.readByte() != 0.toByte(), + port = parcel.readInt(), + ipv6 = parcel.readByte() != 0.toByte(), + dnsHijacking = parcel.readByte() != 0.toByte(), + accessControlProps = readAccessControlProps(parcel), + allowBypass = parcel.readByte() != 0.toByte(), + systemProxy = parcel.readByte() != 0.toByte(), + bypassDomain = parcel.createStringArrayList() ?: emptyList(), + stack = parcel.readString() ?: "", + routeAddress = parcel.createStringArrayList() ?: emptyList(), + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeByte(if (enable) 1.toByte() else 0.toByte()) + parcel.writeInt(port) + parcel.writeByte(if (ipv6) 1.toByte() else 0.toByte()) + parcel.writeByte(if (dnsHijacking) 1.toByte() else 0.toByte()) + parcel.writeParcelable(accessControlProps, flags) + parcel.writeByte(if (allowBypass) 1.toByte() else 0.toByte()) + parcel.writeByte(if (systemProxy) 1.toByte() else 0.toByte()) + parcel.writeStringList(bypassDomain) + parcel.writeString(stack) + parcel.writeStringList(routeAddress) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + @Suppress("DEPRECATION") + private fun readAccessControlProps(parcel: Parcel): AccessControlProps { + return parcel.readParcelable( + AccessControlProps::class.java.classLoader, + ) ?: AccessControlProps( + enable = false, + mode = AccessControlMode.ACCEPT_SELECTED, + acceptList = emptyList(), + rejectList = emptyList(), + ) + } + + override fun createFromParcel(parcel: Parcel): VpnOptions { + return VpnOptions(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} data class CIDR(val address: InetAddress, val prefixLength: Int) @@ -80,4 +158,4 @@ fun String.toCIDR(): CIDR { } return CIDR(address, prefixLength) -} \ No newline at end of file +} diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index dd51c864..5564c35e 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,10 +19,10 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.12.2" apply false - id("org.jetbrains.kotlin.android") version "2.2.10" apply false - id("com.google.gms.google-services") version ("4.3.15") apply false - id("com.google.firebase.crashlytics") version ("2.8.1") apply false + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.4.0" apply false + id("com.google.gms.google-services") version ("4.5.0") apply false + id("com.google.firebase.crashlytics") version ("3.0.7") apply false } diff --git a/arb/intl_en.arb b/arb/intl_en.arb index 82ef0f95..6951fb3a 100644 --- a/arb/intl_en.arb +++ b/arb/intl_en.arb @@ -133,6 +133,7 @@ "app": "App", "general": "General", "systemProxyDesc": "Attach HTTP proxy to VpnService", + "userAgent": "User-Agent", "unifiedDelay": "Unified delay", "unifiedDelayDesc": "Remove extra delays such as handshaking", "tcpConcurrent": "TCP concurrent", @@ -537,5 +538,17 @@ "editSsid": "Edit SSID", "authorized": "Authorized", "tapToAuthorize": "Tap to authorize", - "suspended": "Suspended..." + "suspended": "Suspended...", + "geoOptions": "Geo Options", + "geoAutoUpdate": "Auto Update", + "geoAutoUpdateInterval": "Auto Update Interval", + "geoAutoUpdateIntervalTip": "Auto update interval must be greater than 0", + "hours": "hours", + "hoursCount": "{count} hours", + "geoResources": "Geo Resources", + "geoUpdating": "Updating {name}...", + "geoSkipped": "{name} skipped", + "geoUpdated": "{name} updated", + "secondsCount": "{count} seconds", + "entriesCount": "{count} entries" } diff --git a/arb/intl_ja.arb b/arb/intl_ja.arb index 4f8aa0cb..a310126c 100644 --- a/arb/intl_ja.arb +++ b/arb/intl_ja.arb @@ -133,6 +133,7 @@ "app": "アプリ", "general": "一般", "systemProxyDesc": "HTTPプロキシをVpnServiceに接続", + "userAgent": "ユーザーエージェント", "unifiedDelay": "統一遅延", "unifiedDelayDesc": "ハンドシェイクなどの余分な遅延を削除", "tcpConcurrent": "TCP並列処理", @@ -537,5 +538,17 @@ "editSsid": "SSIDを編集", "authorized": "許可済み", "tapToAuthorize": "タップして許可", - "suspended": "一時停止中..." + "suspended": "一時停止中...", + "geoOptions": "Geoオプション", + "geoAutoUpdate": "自動更新", + "geoAutoUpdateInterval": "自動更新間隔", + "geoAutoUpdateIntervalTip": "自動更新間隔は0より大きくなければなりません", + "hours": "時間", + "hoursCount": "{count} 時間", + "geoResources": "Geoリソース", + "geoUpdating": "{name}を更新中...", + "geoSkipped": "{name} スキップ済み", + "geoUpdated": "{name} 更新済み", + "secondsCount": "{count} 秒", + "entriesCount": "{count} エントリ" } diff --git a/arb/intl_ru.arb b/arb/intl_ru.arb index e43f9078..4cdfdf30 100644 --- a/arb/intl_ru.arb +++ b/arb/intl_ru.arb @@ -133,6 +133,7 @@ "app": "Приложение", "general": "Общие", "systemProxyDesc": "Прикрепить HTTP-прокси к VpnService", + "userAgent": "User-Agent", "unifiedDelay": "Унифицированная задержка", "unifiedDelayDesc": "Убрать дополнительные задержки, такие как рукопожатие", "tcpConcurrent": "TCP параллелизм", @@ -537,5 +538,17 @@ "editSsid": "Изменить SSID", "authorized": "Разрешено", "tapToAuthorize": "Нажмите, чтобы разрешить", - "suspended": "Приостановлено..." + "suspended": "Приостановлено...", + "geoOptions": "Настройки Geo", + "geoAutoUpdate": "Автообновление", + "geoAutoUpdateInterval": "Интервал автообновления", + "geoAutoUpdateIntervalTip": "Интервал автообновления должен быть больше 0", + "hours": "часов", + "hoursCount": "{count} часов", + "geoResources": "Ресурсы Geo", + "geoUpdating": "Обновление {name}...", + "geoSkipped": "{name} пропущено", + "geoUpdated": "{name} обновлено", + "secondsCount": "{count} секунд", + "entriesCount": "{count} записей" } diff --git a/arb/intl_zh_CN.arb b/arb/intl_zh_CN.arb index e6df9df8..28435c10 100644 --- a/arb/intl_zh_CN.arb +++ b/arb/intl_zh_CN.arb @@ -133,6 +133,7 @@ "app": "应用", "general": "常规", "systemProxyDesc": "设置系统代理", + "userAgent": "用户代理", "unifiedDelay": "统一延迟", "unifiedDelayDesc": "去除握手等额外延迟", "tcpConcurrent": "TCP并发", @@ -537,5 +538,17 @@ "editSsid": "编辑SSID", "authorized": "已授权", "tapToAuthorize": "点击授权", - "suspended": "挂起中..." + "suspended": "挂起中...", + "geoOptions": "Geo 选项", + "geoAutoUpdate": "自动更新", + "geoAutoUpdateInterval": "自动更新间隔", + "geoAutoUpdateIntervalTip": "自动更新间隔必须大于0", + "hours": "小时", + "hoursCount": "{count} 小时", + "geoResources": "Geo 资源", + "geoUpdating": "正在更新 {name}...", + "geoSkipped": "{name} 已跳过", + "geoUpdated": "{name} 已更新", + "secondsCount": "{count} 秒", + "entriesCount": "{count} 个条目" } diff --git a/assets/images/icon/status_2.ico b/assets/images/icon/status_2.ico index 517c818482bffcc280a89108ebee7d0a494e6d08..c13a30654d95855b7d44a251c467632a5af6f2ee 100644 GIT binary patch delta 2108 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 h;p1g{jwF9l_P@et&fLbai!goE~t` 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 diff --git a/assets/images/icon/status_3.ico b/assets/images/icon/status_3.ico index 5626c15e4d854af32ca1292c4af10b4abcfab5eb..5989d61132063a004c6189d7ed8fc3f01921a36e 100644 GIT binary patch delta 2547 zcmV; zNFv!f0TKr!5rPWHFe2NqMv)po21P_r77=7X3^+4d3N#=VjvK_luml8ABcdQOILNMH zku?c~z?eW-1L+VFvvnu^?)~ORcgIA7gY&9h)vJ2%-gC}(zU4l`ZC>(0Un?TYlAv?~ zh+2PwBmy8Chy)N9G|C`S1TyjwRCR_17u4R)zVttVzwQtMCB3D}g|*+yS3e5$8gP%=|c)*rD$WWdrRN2-x z6^&mIkS*<6LW0I0vEH9ODRjv!X!K)6XaHWuT6VCBA4v0c)*wScw8Fm3k)e4nkCuP% zx;rqQl#Ug}8Sk#9tl~|BlkI%v0a!h2XwaE!w2 z#<@GS`t<}bCxCR6cOlQRz{Csgw1DL0QXy2A@EuP*5T>|o+YL?dLiI7!YP-T0+ zVI(Y#Jpb|LxFgp4vm1kp%MhNr_1=H^8)KZsqfx`}fpHpI<-tH90Bp`2H6Xj7`%(g0 z>Ba%I0yJjpWJ<9tb&v1FN7lD*mNP+C{C+7-KJIBm02Ma}x+%DHQs|Od4U}KAm>7E4 zK8;aAZ^ObF)M4E!|B`9Xy4qOT`j#+8G?hyLK%z?l5NYW!yJY2z^>KaISz~|P{Q-Pa zZ-IQ&EexTql-?2ls;ofRJ3eqX3;G<5nGpje-B4Y#ka}!%0l?J9CBzn~UjcZgS7pZ6UmkN8O zxGru=Oz4$?05-9F(ANqr9Y=<(Y1g3Cj&8em{^u}gJgRFJ5;^p&Np*jiIR*RKzy<=Xe`o&jJb>!S zFCVX9(1vL9{3t_VF8g|hsRD-rbPjQ$?HSMsdjtEHu)>4B(3f{_BmsU^Lam%O+oe!p^?uT zUb z{q=RWz-0+5@rwO0XvivV;hN6vp>k6}#%Nlepu0YG%2v-rYC zjV&;uKqz6M2gzrFmFk=jie*4b@03eb^%e*vLhAw$Cjrm^>HySW{+T3CK+p#8Ex}EA zT&tW%jz5I7p;yr$5kZ^;u@taEqThlRLFa1#DrJZiO*GR>SVOTUkj9{SWsY+Y__<%`%)Los(; zA3#nxtJ*zGnwtBX-GRHq58ENCfDv4s2hs^vRr`OH*9HP<%qzCXd;;Q8a`|x^0D+@x zCTeTuK%^L=3*mA(oZqMRx$T$VWV*_w^ZA&B*r@U{_#!U zM_?_>hJ3$_z{Vk*Jinmm*pMIq)xg0rAhSIBpqgiI9+7J|0*=HzJU0`2C8Uw^+zSh! z^r2}P%Qa~%@$VbLWs4$hS2_dko$x0WjXZx{1uaL`hYr7Cn6%qF6JRw=+A`w1v|(ZTM@;258W<`Le zopbsODu<>+H0(c>^#lL_;Ssy6aEC7neeONdDDX|LSZMzCu|5C1?^ZeR4YnM6GL9hVxwp5|yCzNfxI z8~NrtlzvkET-F!m6PG|Eq23a~2}5CSV$GuM!}}L?e9H2VPn~>6A^-%yQ~6Bgh9?g$ zT>Z`6U3Wcf(KMe$y7~=MyT^*!6u9!es=0hXBR^(ll|H54|_`2ajADk zWOst{tf3LVax8BYiDvF++}c_`{lb>;YP0#vEY!l=#Q@egTzm8SOBqXkmkob_Mo-R) z4Qtlk!4xpM0eDdeS1g*-C%HU;Ja54DbBez(WWJwX{^%9`l59TQM2ElDjuSQYK`A(ktqs|Qz&(}$aD zDQwAVph<#m>Y@n-0}fxq%rG<0bN7##fw9Egzk1Gj|9anhf4_Tw_ultDz%P90c0U|e z%>jSbklU%psSNF5PN6jWpr_T}1 z?F)%1Tqpnv5R%{l2ZX{P@;vd7sE-V_)d0A5I|Oc-fnmp6W@(c0g`#LLkO!~_+#(b~ zD1?*bT?V4XhyM^_&YQa+;e~v5_B0SZh7Ma6 zB@~ryOkrcze7VdKg!6z%?+u*52sJjI_`bF@4M|C?ftCnGfTRr>>p0{m5@TzzD6!C2?LvwrJTKD#jpm@!_IggnOL5ur`H*Ck zK%jSU#VroIb;X9lr4!%YG4Sx=Z_mAUSF|a;c=M4!og2AA0u7EkG01T7z6-^3`gRl0 zH?@r=g>YZ1eLX3p6c&5h53PR#OL9KgyAgo_RRAX6B4(FzJKnuqd*H#hie_vE@>>De zGnZDjYns@LCdboz78}11Oyw6k??IcZ4_Ui{m-N&C({$gEAxQ_B^MrJ2iGSI>YB^ZU z2cY$yZP$_d=JUy$-h1NZ{r`NT4me^1-~^xmFoy3a1t^gjL~cGvig16b{Sky{mKogy zG>IhNgwx#gDXK0dR9(8Sg@A{8Pq)0-VQWm=R5)qoUVYhju`Ac7M6aw~J!DPwcmV8u zKm-Pf8l0XJuSxn;C_-K55QOMj2Xqr4#vR`A%7$Q5tz;V7)}^cQIl1w0o5OBhas1o0 z$%Q-ne;m7V%cSUGV=8}~nzj|1merxOL(!|NGjA}_PyMN4keo*(05-J*@Z$&dzYvF?DhSk1ONyHPYZwIut0;_vh9f%I;{;) zI-RXK=BBEL3bsAI=ukpeXW!Lt=SOR%uZ*BEH&cg+)C~k&iU7}lKq#U>;Q*N7Bz>n2 zL`)yIHwr~}3Wdl~#cb3lqbVXx_5nVres|Y^PrS}#zg0EQv7pxg3hT8t02R<>FStEZym0XP8I`>w8@%R&^~ z5?sj(`VPxebt{wh!qWv_O+-9cchPQi4iO6I7kCw&t}rJ(da=txK%$Z)AQXX_9>wnl zp95nUJVmGga3I9YTSVPT4(luC4>ymkUpx8`0K3-~%piZTO2F{<&XW?q7m_d}0`Z)s72XrP(69H zDl^GNOa(#_ganAi`qVGCc7bR+EXL(|dC7!=F{jPvzPw*=ZJupz{$XOrE6>lsmMLEY zpIHHPd86(D=RASd;N@^>@26%JJB1SKgaVRd_Mm^RE&gOo#@?&l1OPyapp8PJ!P}1a z+1l-EY50z>A++)pGX}kOv^ZH{*W9}LH4pE{rg6vd`9}#MMxih_vrc%_??A4|KmS6y<*gi+0Q%kqv{+dMW18zF$*bPmA7kDRs8k z4+u9s=C=)3=-kNNv)K4WN(wYbR{5M0G;*82U94>QYv{58^Nt;ZdqIwCf=6>s4AogM zcSJS$B(gYc`->Xus=hR@`XhI zo?Vf@vwnXz`9R?c2a49j!DupJ&-ycWoT#@xK6C$D#A3El*f4YGDkSvzN?mOqzJI8z zGL14TZ+cbx*}GwSJ_jr5%qE2wg+h0Pr#ARIx1(cKSU>AbD1?`m+5(U_-0AxV=Q1Bw1Z0h*!)QlgF& pP6@>&k&(L6ygYK>uJ2#;{12A^J|c{iAjtp#002ovPDHLkV1jg$Ye@hA diff --git a/assets/images/icon/status_3.png b/assets/images/icon/status_3.png index 183f9148333cf664d3fccb629660266f86f08135..f4e7ae0cf107875229338c6f8b1a2be7c78cc02c 100644 GIT binary patch literal 12676 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%0D9OWY@gStb-H~=2~0QNzMS15D(@)&AZj;HRq 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 + + status_1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets_source/images/icon/status_2.svg b/assets_source/images/icon/status_2.svg new file mode 100644 index 00000000..75df10e9 --- /dev/null +++ b/assets_source/images/icon/status_2.svg @@ -0,0 +1,36 @@ + + + status_2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets_source/images/icon/status_3.svg b/assets_source/images/icon/status_3.svg new file mode 100644 index 00000000..091d256a --- /dev/null +++ b/assets_source/images/icon/status_3.svg @@ -0,0 +1,40 @@ + + + status_3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/Clash.Meta b/core/Clash.Meta index 7bdcd60d..80362fc1 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 7bdcd60da9db10b681d7507af96d4fad797f3774 +Subproject commit 80362fc1895dcf60b79b562896653046e0687413 diff --git a/core/action.go b/core/action.go index 41a3b680..a7d6c8f9 100644 --- a/core/action.go +++ b/core/action.go @@ -129,19 +129,11 @@ func handleAction(action *Action, result ActionResult) { case getExternalProviderMethod: externalProviderName := action.Data.(string) result.success(handleGetExternalProvider(externalProviderName)) + return case updateGeoDataMethod: - paramsString := action.Data.(string) - var params = map[string]string{} - err := json.Unmarshal([]byte(paramsString), ¶ms) - if err != nil { - result.success(err.Error()) - return - } - geoType := params["geo-type"] - geoName := params["geo-name"] - handleUpdateGeoData(geoType, geoName, func(value string) { - result.success(value) - }) + geoType := action.Data.(string) + handleUpdateGeoData(geoType) + result.success("") return case updateExternalProviderMethod: providerName := action.Data.(string) diff --git a/core/common.go b/core/common.go index bdde7616..46d9bdfd 100644 --- a/core/common.go +++ b/core/common.go @@ -6,6 +6,11 @@ import ( "encoding/json" "errors" "fmt" + "os" + "path/filepath" + "runtime" + "sync" + "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outboundgroup" @@ -13,6 +18,7 @@ import ( "github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" @@ -24,10 +30,6 @@ import ( "github.com/metacubex/mihomo/log" rp "github.com/metacubex/mihomo/rules/provider" "github.com/metacubex/mihomo/tunnel" - "os" - "path/filepath" - "runtime" - "sync" ) var ( @@ -234,7 +236,17 @@ func updateConfig(params *UpdateParams) { general.Tun.Stack = *params.Tun.Stack } + if params.GeoAutoUpdate != nil { + updater.SetGeoAutoUpdate(*params.GeoAutoUpdate) + } + if params.GeoUpdateInterval != nil { + updater.SetGeoUpdateInterval(*params.GeoUpdateInterval) + } + updateListeners() + if updater.GeoAutoUpdate() { + updater.RegisterGeoUpdaterWithCancel() + } } func applyConfig(params *SetupParams) error { @@ -250,6 +262,9 @@ func applyConfig(params *SetupParams) error { hub.ApplyConfig(currentConfig) patchSelectGroup(params.SelectedMap) updateListeners() + if updater.GeoAutoUpdate() { + updater.RegisterGeoUpdaterWithCancel() + } return err } diff --git a/core/constant.go b/core/constant.go index 5177fe4c..ed16fea2 100644 --- a/core/constant.go +++ b/core/constant.go @@ -34,6 +34,8 @@ type UpdateParams struct { ExternalController *string `json:"external-controller"` Interface *string `json:"interface-name"` UnifiedDelay *bool `json:"unified-delay"` + GeoAutoUpdate *bool `json:"geo-auto-update"` + GeoUpdateInterval *int `json:"geo-update-interval"` } type tunSchema struct { @@ -123,12 +125,20 @@ type Message struct { } const ( - LogMessage MessageType = "log" - DelayMessage MessageType = "delay" - RequestMessage MessageType = "request" - LoadedMessage MessageType = "loaded" + LogMessage MessageType = "log" + DelayMessage MessageType = "delay" + RequestMessage MessageType = "request" + LoadedMessage MessageType = "loaded" + GeoUpdateMessage MessageType = "geoUpdate" ) +type GeoUpdateStatus struct { + Type string `json:"type"` + Updating bool `json:"updating"` + Skipped bool `json:"skipped,omitempty"` + Error string `json:"error,omitempty"` +} + func (message *Message) Json() (string, error) { data, err := json.Marshal(message) return string(data), err diff --git a/core/go.mod b/core/go.mod index fd557185..e04990f8 100644 --- a/core/go.mod +++ b/core/go.mod @@ -17,13 +17,15 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect - github.com/andybalholm/brotli v1.0.6 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-iptables v0.8.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dunglas/httpsfv v1.0.2 // indirect - github.com/enfein/mieru/v3 v3.31.0 // indirect + github.com/enfein/mieru/v3 v3.33.0 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect @@ -42,6 +44,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect github.com/josharian/native v1.1.0 // indirect @@ -51,6 +54,7 @@ require ( github.com/klauspost/reedsolomon v1.12.3 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect + github.com/metacubex/age v0.0.0-20260603010618-28d156b4ea78 // indirect github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d // indirect github.com/metacubex/ascon v0.1.0 // indirect github.com/metacubex/bart v0.26.0 // indirect @@ -67,30 +71,31 @@ require ( github.com/metacubex/hkdf v0.1.0 // indirect github.com/metacubex/hpke v0.1.0 // indirect github.com/metacubex/http v0.1.6 // indirect - github.com/metacubex/jsonv2 v0.0.0-20260513175203-1c6abea7534c // indirect + github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df // indirect github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect github.com/metacubex/mhurl v0.1.0 // indirect github.com/metacubex/mlkem v0.1.0 // indirect github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb // indirect github.com/metacubex/qpack v0.6.0 // indirect - github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306 // indirect + github.com/metacubex/quic-go v0.59.1-0.20260520020949-fcd18c7b6ace // indirect github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/restls-client-go v0.1.7 // indirect + github.com/metacubex/sevenzip v1.6.4 // indirect github.com/metacubex/sing v0.5.7 // indirect github.com/metacubex/sing-mux v0.3.9 // indirect - github.com/metacubex/sing-quic v0.0.0-20260512151354-8475655be853 // indirect + github.com/metacubex/sing-quic v0.0.0-20260527143057-68e10a6afdc3 // indirect github.com/metacubex/sing-shadowsocks v0.2.12 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect - github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect - github.com/metacubex/sing-tun v0.4.18 // indirect + github.com/metacubex/sing-shadowtls v0.0.0-20260517015314-c11c36474edc // indirect + github.com/metacubex/sing-tun v0.4.20 // indirect github.com/metacubex/sing-vmess v0.2.5 // indirect - github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947 // indirect + github.com/metacubex/sing-wireguard v0.0.0-20260520151737-7e7c7c1b854c // indirect github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect github.com/metacubex/ssh v0.1.0 // indirect - github.com/metacubex/tailscale v0.0.0-20260516120020-a21c2c99dcbe // indirect - github.com/metacubex/tailscale-wireguard-go v0.0.0-20260513233728-8bc7ee255d04 // indirect + github.com/metacubex/tailscale v0.0.0-20260520011538-f23132fac4b7 // indirect + github.com/metacubex/tailscale-wireguard-go v0.0.0-20260521124654-e1bf77ef79af // indirect github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // indirect - github.com/metacubex/tls v0.1.5 // indirect + github.com/metacubex/tls v0.1.6 // indirect github.com/metacubex/utls v1.8.4 // indirect github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect @@ -100,8 +105,9 @@ require ( github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/openacid/low v0.1.21 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect - github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/pierrec/lz4/v4 v4.1.27 // indirect github.com/pires/go-proxyproto v0.8.0 // indirect + github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/samber/lo v1.53.0 // indirect @@ -109,11 +115,13 @@ require ( github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sirupsen/logrus v1.9.4 // indirect + github.com/stangelandcl/ppmd v0.1.1 // indirect github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/core/go.sum b/core/go.sum index c63494ac..f9206d68 100644 --- a/core/go.sum +++ b/core/go.sum @@ -12,11 +12,15 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= @@ -30,8 +34,8 @@ github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= -github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= -github.com/enfein/mieru/v3 v3.31.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= +github.com/enfein/mieru/v3 v3.33.0 h1:hv2jK8nqYHwpSG86U2rpZR2I8Aff1/J3ifRmd9NBbFc= +github.com/enfein/mieru/v3 v3.33.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= @@ -75,6 +79,8 @@ github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQu github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= @@ -90,10 +96,16 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4 github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc= github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/metacubex/age v0.0.0-20260603010618-28d156b4ea78 h1:LqWr0vb9zDNuQS+jJd4fnRYk/SEI7KJ7TDe/L4WFK48= +github.com/metacubex/age v0.0.0-20260603010618-28d156b4ea78/go.mod h1:BTBG/iVY7rg3qq5WdVCg0GFk58CSvCDSbjy8I7kEx/c= github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c= github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY= github.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM= @@ -126,8 +138,8 @@ github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ= github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U= github.com/metacubex/http v0.1.6 h1:xvXuvXMCMxCWMF5nEJF4yiKvXL+p2atWMzs37e80m1I= github.com/metacubex/http v0.1.6/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= -github.com/metacubex/jsonv2 v0.0.0-20260513175203-1c6abea7534c h1:KGhBHDe6FveU0ury+9RyX329nclM1CHODa0Fi+uOAYM= -github.com/metacubex/jsonv2 v0.0.0-20260513175203-1c6abea7534c/go.mod h1:F4sVXat6QjPXkNsKRDyyG3BhSkxPFFnRPEIwmmyCgbg= +github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df h1:S0vBzqjXok24VopstOgPd1JdgglW9tXehrqvwpQWbQ8= +github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df/go.mod h1:F4sVXat6QjPXkNsKRDyyG3BhSkxPFFnRPEIwmmyCgbg= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc= github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE= @@ -138,42 +150,44 @@ github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb h1:wk6mHYPURSUv github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb/go.mod h1:73ZrCfhdkW4F2E2GAlta3km/S2RHhFNogCMtWZV2anQ= github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw= github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA= -github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306 h1:HlGLmLsWJMLSu0CMI9z/BmEnithB4oXM5Rom6/0Qxtg= -github.com/metacubex/quic-go v0.59.1-0.20260413153657-53bb22f2c306/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk= +github.com/metacubex/quic-go v0.59.1-0.20260520020949-fcd18c7b6ace h1:KXacx7dp1GYVMgxezwXRt5BMsEbvAYuA6rPFUmdAvcQ= +github.com/metacubex/quic-go v0.59.1-0.20260520020949-fcd18c7b6ace/go.mod h1:2YEQEvFrZ5V76oynMBDTlN+4fdnSHCa2uNJxv3cm1HU= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= +github.com/metacubex/sevenzip v1.6.4 h1:OIL+DeOeSAbKNsjqxcYUMiarRmX6Kaxakb0GT7E9Oik= +github.com/metacubex/sevenzip v1.6.4/go.mod h1:FP3X9bzFKj9wPxifGN9B3w2fIEicMjzKYIGIhnu+1pw= github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM= github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.9 h1:/aoBD2+sK2qsXDlNDe3hkR0GZuFDtwIZhOeGUx9W0Yk= github.com/metacubex/sing-mux v0.3.9/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc= -github.com/metacubex/sing-quic v0.0.0-20260512151354-8475655be853 h1:nZ5WNU6kjj6kBu4+2eMySFkUVGCop64rZnLMm+HPh8w= -github.com/metacubex/sing-quic v0.0.0-20260512151354-8475655be853/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= +github.com/metacubex/sing-quic v0.0.0-20260527143057-68e10a6afdc3 h1:PnMby5+kZXTl/CFDHfxMbMTaSRD+uMKMsrDYVQyAmX8= +github.com/metacubex/sing-quic v0.0.0-20260527143057-68e10a6afdc3/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A= github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= -github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= -github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= -github.com/metacubex/sing-tun v0.4.18 h1:WRzAosG0YkT3aZq5RJWtF+RdCgeJ8EpooS5ZM1lkXo0= -github.com/metacubex/sing-tun v0.4.18/go.mod h1:g4I/JNplDBhXLF+aQWgFbhNeJPSXQOWS9HvLeNvkgeA= +github.com/metacubex/sing-shadowtls v0.0.0-20260517015314-c11c36474edc h1:8wLoFfYQ88iGPL+krQ5tJsI8IAmkFjKpQL2q+y3pvss= +github.com/metacubex/sing-shadowtls v0.0.0-20260517015314-c11c36474edc/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= +github.com/metacubex/sing-tun v0.4.20 h1:xdupzizRoZKyDzP0l68WAx5Sk4ooiuT1GiWsiJyOGPw= +github.com/metacubex/sing-tun v0.4.20/go.mod h1:g4I/JNplDBhXLF+aQWgFbhNeJPSXQOWS9HvLeNvkgeA= github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE= github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q= -github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947 h1:IB03BvRQtvjWScyOK5jSQVJYY8osmZXHL+4VCEFMWcM= -github.com/metacubex/sing-wireguard v0.0.0-20260507084707-690d479ec947/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= +github.com/metacubex/sing-wireguard v0.0.0-20260520151737-7e7c7c1b854c h1:tH9FuQW357zp2xAGzkoZTGpNGMVmEFZov0iV5M2S5ew= +github.com/metacubex/sing-wireguard v0.0.0-20260520151737-7e7c7c1b854c/go.mod h1:eQZDJTx+IH3k4mXqaOJ3VJ9h9ZqOl60F7TLi5wAU51Q= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg= github.com/metacubex/ssh v0.1.0 h1:iGfr99qk/eMHzUnQ/0bTxXT8+8SWqLSHBWDHoAhngzw= github.com/metacubex/ssh v0.1.0/go.mod h1:NUtl0d+/f2cG9ECEpMM8iCVOpmggQlC13oLeDUONDlU= -github.com/metacubex/tailscale v0.0.0-20260516120020-a21c2c99dcbe h1:ZdAKshacNruZGuKTE8WMTuxyGgpv/LySLoE/EEmgF9c= -github.com/metacubex/tailscale v0.0.0-20260516120020-a21c2c99dcbe/go.mod h1:2G1V82OGXgxT7m7046GA80I9SlcvczljCK0C7NQ3c10= -github.com/metacubex/tailscale-wireguard-go v0.0.0-20260513233728-8bc7ee255d04 h1:zk+mDDSBl5lv80WWtaFUbpj8XLb7AhjCUbn2pB37N0U= -github.com/metacubex/tailscale-wireguard-go v0.0.0-20260513233728-8bc7ee255d04/go.mod h1:pKUKBy7IcQ5r0i66gWENHgxKvBn8tlgAGx0DZMq8h5M= +github.com/metacubex/tailscale v0.0.0-20260520011538-f23132fac4b7 h1:LoJR4NMyNKHeEJoeGDtcsao7sV0NRkzMeV5H/0J0MIE= +github.com/metacubex/tailscale v0.0.0-20260520011538-f23132fac4b7/go.mod h1:MAo3HhE7968rIwmDvYTYE8xCsV4x+hLnkChdXeP3X4c= +github.com/metacubex/tailscale-wireguard-go v0.0.0-20260521124654-e1bf77ef79af h1:c60IbBMUq2h1M2m7+grMJJmBmrObxL8SwvNtm6Ozbwk= +github.com/metacubex/tailscale-wireguard-go v0.0.0-20260521124654-e1bf77ef79af/go.mod h1:i3zLKytWkOnyT1i9OmiLevWvrN5J5HE1+yjE7UYNfcQ= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= -github.com/metacubex/tls v0.1.5 h1:ECcB83dj+zadnhlKcLnUUf1Sq6+vU0f/zoyU0+9oPTc= -github.com/metacubex/tls v0.1.5/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM= +github.com/metacubex/tls v0.1.6 h1:t2ubLneYa4ceyIC++54a57BLqZFA/QYUrhdjLk2GPwo= +github.com/metacubex/tls v0.1.6/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM= github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk= @@ -195,13 +209,16 @@ github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= -github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= +github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs= +github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= @@ -216,10 +233,19 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIF github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8= +github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE= @@ -232,6 +258,8 @@ github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+y github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -245,6 +273,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= @@ -302,8 +332,9 @@ golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/hub.go b/core/hub.go index 1d2894a6..aad92083 100644 --- a/core/hub.go +++ b/core/hub.go @@ -347,36 +347,22 @@ func handleGetExternalProvider(externalProviderName string) string { return string(data) } -func handleUpdateGeoData(geoType string, geoName string, fn func(value string)) { +func handleUpdateGeoData(geoType string) { go func() { - path := constant.Path.Resolve(geoName) switch geoType { case "MMDB": - err := updater.UpdateMMDBWithPath(path) - if err != nil { - fn(err.Error()) - return - } + updater.UpdateMMDB() + return case "ASN": - err := updater.UpdateASNWithPath(path) - if err != nil { - fn(err.Error()) - return - } + updater.UpdateASN() + return case "GEOIP": - err := updater.UpdateGeoIpWithPath(path) - if err != nil { - fn(err.Error()) - return - } + updater.UpdateGeoIp() + return case "GEOSITE": - err := updater.UpdateGeoSiteWithPath(path) - if err != nil { - fn(err.Error()) - return - } + updater.UpdateGeoSite() + return } - fn("") }() } @@ -568,4 +554,18 @@ func init() { Data: providerName, }) } + updater.GeoUpdateHook = func(geoType string, updating bool, skipped bool, updateErr error) { + status := GeoUpdateStatus{ + Type: geoType, + Updating: updating, + Skipped: skipped, + } + if updateErr != nil { + status.Error = updateErr.Error() + } + sendMessage(Message{ + Type: GeoUpdateMessage, + Data: status, + }) + } } diff --git a/lib/common/common.dart b/lib/common/common.dart index 20909fd9..72fc85dc 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -12,6 +12,7 @@ export 'future.dart'; export 'http.dart'; export 'icons.dart'; export 'indexing.dart'; +export 'input_limits.dart'; export 'iterable.dart'; export 'javascript.dart'; export 'keyboard.dart'; @@ -29,7 +30,6 @@ export 'package.dart'; export 'path.dart'; export 'permission.dart'; export 'picker.dart'; -export 'pipe.dart'; export 'preferences.dart'; export 'print.dart'; export 'protocol.dart'; diff --git a/lib/common/fixed.dart b/lib/common/fixed.dart index 55e1429a..e1f81e9b 100644 --- a/lib/common/fixed.dart +++ b/lib/common/fixed.dart @@ -7,7 +7,7 @@ class FixedList { final List _list; FixedList(this.maxLength, {List? list}) - : _list = (list ?? [])..truncate(maxLength); + : _list = (list ?? [])..truncate(maxLength); void add(T item) { _list.add(item); @@ -25,10 +25,7 @@ class FixedList { T operator [](int index) => _list[index]; FixedList copyWith() { - return FixedList( - maxLength, - list: List.of(_list), - ); + return FixedList(maxLength, list: List.of(_list)); } } @@ -41,10 +38,7 @@ class FixedMap { } V updateCacheValue(K key, ValueCallback callback) { - final realValue = _map.updateCacheValue( - key, - callback, - ); + final realValue = _map.updateCacheValue(key, callback); _adjustMap(); return realValue; } @@ -65,9 +59,7 @@ class FixedMap { void _adjustMap() { if (_map.length > maxLength) { - _map = Map.fromEntries( - map.entries.toList()..truncate(maxLength), - ); + _map = Map.fromEntries(map.entries.toList()..truncate(maxLength)); } } diff --git a/lib/common/input_limits.dart b/lib/common/input_limits.dart new file mode 100644 index 00000000..9a2a4bdf --- /dev/null +++ b/lib/common/input_limits.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart'; + +abstract final class TextInputLimits { + static const name = 64; + static const groupName = 64; + static const url = 2048; + static const uri = url; + static const iconUrl = url; + static const dnsServer = url; + static const hostValue = url; + static const userName = 128; + static const password = 512; + static const userAgent = 512; + static const fileName = 255; + static const port = 5; + static const number = 10; + static const interval = number; + static const search = 256; + static const rule = 1024; + static const filter = 1024; + static const status = 128; + static const dnsListen = 255; + static const domain = 512; + static const geoSite = 128; + static const geoIpCode = 16; + static const cidr = 64; + + static List limit(int maxLength) { + return [LengthLimitingTextInputFormatter(maxLength)]; + } + + static List digitsOnly(int maxLength) { + return [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(maxLength), + ]; + } +} diff --git a/lib/common/iterable.dart b/lib/common/iterable.dart index 41324c39..d0fffcf4 100644 --- a/lib/common/iterable.dart +++ b/lib/common/iterable.dart @@ -80,6 +80,13 @@ extension ListExt on List { return newList; } + List copyAndReorder(int oldIndex, int newIndex) { + final newList = List.from(this); + final item = newList.removeAt(oldIndex); + newList.insert(newIndex, item); + return newList; + } + List safeSublist(int start, [int? end]) { if (start <= 0) return this; if (start > length) return []; diff --git a/lib/common/keyboard.dart b/lib/common/keyboard.dart index be2ee42d..c6970bdb 100644 --- a/lib/common/keyboard.dart +++ b/lib/common/keyboard.dart @@ -5,90 +5,90 @@ import 'system.dart'; final Map _knownKeyLabels = { - PhysicalKeyboardKey.keyA: 'A', - PhysicalKeyboardKey.keyB: 'B', - PhysicalKeyboardKey.keyC: 'C', - PhysicalKeyboardKey.keyD: 'D', - PhysicalKeyboardKey.keyE: 'E', - PhysicalKeyboardKey.keyF: 'F', - PhysicalKeyboardKey.keyG: 'G', - PhysicalKeyboardKey.keyH: 'H', - PhysicalKeyboardKey.keyI: 'I', - PhysicalKeyboardKey.keyJ: 'J', - PhysicalKeyboardKey.keyK: 'K', - PhysicalKeyboardKey.keyL: 'L', - PhysicalKeyboardKey.keyM: 'M', - PhysicalKeyboardKey.keyN: 'N', - PhysicalKeyboardKey.keyO: 'O', - PhysicalKeyboardKey.keyP: 'P', - PhysicalKeyboardKey.keyQ: 'Q', - PhysicalKeyboardKey.keyR: 'R', - PhysicalKeyboardKey.keyS: 'S', - PhysicalKeyboardKey.keyT: 'T', - PhysicalKeyboardKey.keyU: 'U', - PhysicalKeyboardKey.keyV: 'V', - PhysicalKeyboardKey.keyW: 'W', - PhysicalKeyboardKey.keyX: 'X', - PhysicalKeyboardKey.keyY: 'Y', - PhysicalKeyboardKey.keyZ: 'Z', - PhysicalKeyboardKey.digit1: '1', - PhysicalKeyboardKey.digit2: '2', - PhysicalKeyboardKey.digit3: '3', - PhysicalKeyboardKey.digit4: '4', - PhysicalKeyboardKey.digit5: '5', - PhysicalKeyboardKey.digit6: '6', - PhysicalKeyboardKey.digit7: '7', - PhysicalKeyboardKey.digit8: '8', - PhysicalKeyboardKey.digit9: '9', - PhysicalKeyboardKey.digit0: '0', - PhysicalKeyboardKey.enter: 'ENTER', - PhysicalKeyboardKey.escape: 'ESCAPE', - PhysicalKeyboardKey.backspace: 'BACKSPACE', - PhysicalKeyboardKey.tab: 'TAB', - PhysicalKeyboardKey.space: 'SPACE', - PhysicalKeyboardKey.minus: '-', - PhysicalKeyboardKey.equal: '=', - PhysicalKeyboardKey.bracketLeft: '[', - PhysicalKeyboardKey.bracketRight: ']', - PhysicalKeyboardKey.backslash: '\\', - PhysicalKeyboardKey.semicolon: ';', - PhysicalKeyboardKey.quote: '"', - PhysicalKeyboardKey.backquote: '`', - PhysicalKeyboardKey.comma: ',', - PhysicalKeyboardKey.period: '.', - PhysicalKeyboardKey.slash: '/', - PhysicalKeyboardKey.capsLock: 'CAPSLOCK', - PhysicalKeyboardKey.f1: 'F1', - PhysicalKeyboardKey.f2: 'F2', - PhysicalKeyboardKey.f3: 'F3', - PhysicalKeyboardKey.f4: 'F4', - PhysicalKeyboardKey.f5: 'F5', - PhysicalKeyboardKey.f6: 'F6', - PhysicalKeyboardKey.f7: 'F7', - PhysicalKeyboardKey.f8: 'F8', - PhysicalKeyboardKey.f9: 'F9', - PhysicalKeyboardKey.f10: 'F10', - PhysicalKeyboardKey.f11: 'F11', - PhysicalKeyboardKey.f12: 'F12', - PhysicalKeyboardKey.home: 'HOME', - PhysicalKeyboardKey.pageUp: 'PAGEUP', - PhysicalKeyboardKey.delete: 'DELETE', - PhysicalKeyboardKey.end: 'END', - PhysicalKeyboardKey.pageDown: 'PAGEDOWN', - PhysicalKeyboardKey.arrowRight: '→', - PhysicalKeyboardKey.arrowLeft: '←', - PhysicalKeyboardKey.arrowDown: '↓', - PhysicalKeyboardKey.arrowUp: '↑', - PhysicalKeyboardKey.controlLeft: 'CTRL', - PhysicalKeyboardKey.shiftLeft: 'SHIFT', - PhysicalKeyboardKey.altLeft: 'ALT', - PhysicalKeyboardKey.metaLeft: system.isMacOS ? '⌘' : 'WIN', - PhysicalKeyboardKey.controlRight: 'CTRL', - PhysicalKeyboardKey.shiftRight: 'SHIFT', - PhysicalKeyboardKey.altRight: 'ALT', - PhysicalKeyboardKey.metaRight: system.isMacOS ? '⌘' : 'WIN', - PhysicalKeyboardKey.fn: 'FN', -}; + PhysicalKeyboardKey.keyA: 'A', + PhysicalKeyboardKey.keyB: 'B', + PhysicalKeyboardKey.keyC: 'C', + PhysicalKeyboardKey.keyD: 'D', + PhysicalKeyboardKey.keyE: 'E', + PhysicalKeyboardKey.keyF: 'F', + PhysicalKeyboardKey.keyG: 'G', + PhysicalKeyboardKey.keyH: 'H', + PhysicalKeyboardKey.keyI: 'I', + PhysicalKeyboardKey.keyJ: 'J', + PhysicalKeyboardKey.keyK: 'K', + PhysicalKeyboardKey.keyL: 'L', + PhysicalKeyboardKey.keyM: 'M', + PhysicalKeyboardKey.keyN: 'N', + PhysicalKeyboardKey.keyO: 'O', + PhysicalKeyboardKey.keyP: 'P', + PhysicalKeyboardKey.keyQ: 'Q', + PhysicalKeyboardKey.keyR: 'R', + PhysicalKeyboardKey.keyS: 'S', + PhysicalKeyboardKey.keyT: 'T', + PhysicalKeyboardKey.keyU: 'U', + PhysicalKeyboardKey.keyV: 'V', + PhysicalKeyboardKey.keyW: 'W', + PhysicalKeyboardKey.keyX: 'X', + PhysicalKeyboardKey.keyY: 'Y', + PhysicalKeyboardKey.keyZ: 'Z', + PhysicalKeyboardKey.digit1: '1', + PhysicalKeyboardKey.digit2: '2', + PhysicalKeyboardKey.digit3: '3', + PhysicalKeyboardKey.digit4: '4', + PhysicalKeyboardKey.digit5: '5', + PhysicalKeyboardKey.digit6: '6', + PhysicalKeyboardKey.digit7: '7', + PhysicalKeyboardKey.digit8: '8', + PhysicalKeyboardKey.digit9: '9', + PhysicalKeyboardKey.digit0: '0', + PhysicalKeyboardKey.enter: 'ENTER', + PhysicalKeyboardKey.escape: 'ESCAPE', + PhysicalKeyboardKey.backspace: 'BACKSPACE', + PhysicalKeyboardKey.tab: 'TAB', + PhysicalKeyboardKey.space: 'SPACE', + PhysicalKeyboardKey.minus: '-', + PhysicalKeyboardKey.equal: '=', + PhysicalKeyboardKey.bracketLeft: '[', + PhysicalKeyboardKey.bracketRight: ']', + PhysicalKeyboardKey.backslash: '\\', + PhysicalKeyboardKey.semicolon: ';', + PhysicalKeyboardKey.quote: '"', + PhysicalKeyboardKey.backquote: '`', + PhysicalKeyboardKey.comma: ',', + PhysicalKeyboardKey.period: '.', + PhysicalKeyboardKey.slash: '/', + PhysicalKeyboardKey.capsLock: 'CAPSLOCK', + PhysicalKeyboardKey.f1: 'F1', + PhysicalKeyboardKey.f2: 'F2', + PhysicalKeyboardKey.f3: 'F3', + PhysicalKeyboardKey.f4: 'F4', + PhysicalKeyboardKey.f5: 'F5', + PhysicalKeyboardKey.f6: 'F6', + PhysicalKeyboardKey.f7: 'F7', + PhysicalKeyboardKey.f8: 'F8', + PhysicalKeyboardKey.f9: 'F9', + PhysicalKeyboardKey.f10: 'F10', + PhysicalKeyboardKey.f11: 'F11', + PhysicalKeyboardKey.f12: 'F12', + PhysicalKeyboardKey.home: 'HOME', + PhysicalKeyboardKey.pageUp: 'PAGEUP', + PhysicalKeyboardKey.delete: 'DELETE', + PhysicalKeyboardKey.end: 'END', + PhysicalKeyboardKey.pageDown: 'PAGEDOWN', + PhysicalKeyboardKey.arrowRight: '→', + PhysicalKeyboardKey.arrowLeft: '←', + PhysicalKeyboardKey.arrowDown: '↓', + PhysicalKeyboardKey.arrowUp: '↑', + PhysicalKeyboardKey.controlLeft: 'CTRL', + PhysicalKeyboardKey.shiftLeft: 'SHIFT', + PhysicalKeyboardKey.altLeft: 'ALT', + PhysicalKeyboardKey.metaLeft: system.isMacOS ? '⌘' : 'WIN', + PhysicalKeyboardKey.controlRight: 'CTRL', + PhysicalKeyboardKey.shiftRight: 'SHIFT', + PhysicalKeyboardKey.altRight: 'ALT', + PhysicalKeyboardKey.metaRight: system.isMacOS ? '⌘' : 'WIN', + PhysicalKeyboardKey.fn: 'FN', + }; extension KeyboardKeyExt on KeyboardKey { String get label { diff --git a/lib/common/link.dart b/lib/common/link.dart index 719e8b22..6f1775f6 100644 --- a/lib/common/link.dart +++ b/lib/common/link.dart @@ -16,21 +16,20 @@ class LinkManager { } Future initAppLinksListen( - Function(String url) installConfigCallBack) async { + Function(String url) installConfigCallBack, + ) async { commonPrint.log('initAppLinksListen'); destroy(); - subscription = _appLinks.uriLinkStream.listen( - (uri) { - commonPrint.log('onAppLink: $uri'); - if (uri.host == 'install-config') { - final parameters = uri.queryParameters; - final url = parameters['url']; - if (url != null) { - installConfigCallBack(url); - } + subscription = _appLinks.uriLinkStream.listen((uri) { + commonPrint.log('onAppLink: $uri'); + if (uri.host == 'install-config') { + final parameters = uri.queryParameters; + final url = parameters['url']; + if (url != null) { + installConfigCallBack(url); } - }, - ); + } + }); } void destroy() { diff --git a/lib/common/package.dart b/lib/common/package.dart index 2560187d..972fced1 100644 --- a/lib/common/package.dart +++ b/lib/common/package.dart @@ -6,8 +6,8 @@ import 'common.dart'; extension PackageInfoExtension on PackageInfo { String get ua => [ - '$appName/v$version', - 'clash-verge', - 'Platform/${Platform.operatingSystem}', - ].join(' '); + '$appName/v$version', + 'clash-verge', + 'Platform/${Platform.operatingSystem}', + ].join(' '); } diff --git a/lib/common/picker.dart b/lib/common/picker.dart index 52f2514a..fbb08f18 100644 --- a/lib/common/picker.dart +++ b/lib/common/picker.dart @@ -7,13 +7,8 @@ import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class Picker { - Future pickerFile({bool withData = true}) async { - final filePickerResult = await FilePicker.pickFiles( - withData: withData, - allowMultiple: false, - initialDirectory: await appPath.downloadDirPath, - ); - return filePickerResult?.files.first; + Future pickerFile() async { + return FilePicker.pickFile(initialDirectory: await appPath.downloadDirPath); } Future saveFile(String fileName, Uint8List bytes) async { @@ -34,15 +29,12 @@ class Picker { if (!await localFile.exists()) { await localFile.create(recursive: true); } - final bytes = Platform.isAndroid ? await localFile.readAsBytes() : null; + final bytes = await localFile.readAsBytes(); final path = await FilePicker.saveFile( fileName: fileName, initialDirectory: await appPath.downloadDirPath, bytes: bytes, ); - if (path != null && bytes == null) { - await localFile.copy(path); - } await localFile.safeDelete(); return path; } @@ -65,4 +57,10 @@ class Picker { } } +extension PlatformFileExt on PlatformFile { + Future readBytes() { + return readAsBytes(); + } +} + final picker = Picker(); diff --git a/lib/common/pipe.dart b/lib/common/pipe.dart deleted file mode 100644 index 54e6a53a..00000000 --- a/lib/common/pipe.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/enum/enum.dart'; -import 'package:win32/win32.dart'; - -const _pipeBufferSize = 256 * 1024; - -class _PipeCmd { - final int type; // 0 = write, 1 = shutdown - final Uint8List? data; - - const _PipeCmd(this.type, this.data); -} - -void _pipeIsolate(List args) { - final pipeName = args[0] as String; - final dataPort = args[1] as SendPort; - - final namePtr = pipeName.toNativeUtf16(); - - final handle = CreateNamedPipe( - namePtr, - PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, - PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, - 1, - _pipeBufferSize, - _pipeBufferSize, - 0, - nullptr, - ); - - calloc.free(namePtr); - - if (handle == INVALID_HANDLE_VALUE) { - dataPort.send( - 'error:CreateNamedPipe failed, Win32 error=${GetLastError()}', - ); - return; - } - - final writeRx = ReceivePort(); - - final readBuf = calloc(_pipeBufferSize); - final bytesRead = calloc(); - final overlapped = calloc(); - final readEvent = CreateEvent(nullptr, 1, 0, nullptr); - overlapped.ref.hEvent = readEvent; - - var running = true; - var clientConnected = false; - - writeRx.listen((msg) { - if (msg is _PipeCmd) { - if (msg.type == 0) { - if (clientConnected && msg.data != null) { - final buf = calloc(msg.data!.length); - final written = calloc(); - buf.asTypedList(msg.data!.length).setAll(0, msg.data!); - WriteFile(handle, buf, msg.data!.length, written, nullptr); - calloc.free(written); - calloc.free(buf); - } - } else if (msg.type == 1) { - running = false; - if (clientConnected) { - DisconnectNamedPipe(handle); - clientConnected = false; - } - writeRx.close(); - } - } - }); - - final connectEvent = CreateEvent(nullptr, 1, 0, nullptr); - final connectOverlapped = calloc(); - connectOverlapped.ref.hEvent = connectEvent; - - while (running) { - final cr = ConnectNamedPipe(handle, connectOverlapped); - if (cr == 0) { - final err = GetLastError(); - if (err == ERROR_IO_PENDING) { - while (running) { - final wr = WaitForSingleObject(connectEvent, 100); - if (wr == WAIT_OBJECT_0) break; - } - if (!running) break; - } else if (err != ERROR_PIPE_CONNECTED) { - dataPort.send('error:ConnectNamedPipe failed, Win32 error=$err'); - break; - } - } - if (!running) break; - - clientConnected = true; - dataPort.send(writeRx.sendPort); - - var readPending = false; - while (running && clientConnected) { - if (!readPending) { - bytesRead.value = 0; - final readResult = ReadFile( - handle, - readBuf, - _pipeBufferSize, - bytesRead, - overlapped, - ); - if (readResult != 0) { - if (bytesRead.value > 0) { - dataPort.send( - Uint8List.fromList(readBuf.asTypedList(bytesRead.value)), - ); - } else { - break; - } - } else { - final err = GetLastError(); - if (err == ERROR_IO_PENDING) { - readPending = true; - } else if (bytesRead.value > 0) { - dataPort.send( - Uint8List.fromList(readBuf.asTypedList(bytesRead.value)), - ); - } else { - break; - } - } - } - - if (readPending) { - final wr = WaitForSingleObject(readEvent, 100); - if (wr == WAIT_OBJECT_0) { - final got = GetOverlappedResult(handle, overlapped, bytesRead, 0); - if (got != 0 && bytesRead.value > 0) { - dataPort.send( - Uint8List.fromList(readBuf.asTypedList(bytesRead.value)), - ); - readPending = false; - } else { - break; - } - } else if (wr != WAIT_TIMEOUT) { - break; - } - } - } - - if (running && clientConnected) { - DisconnectNamedPipe(handle); - clientConnected = false; - dataPort.send('closed'); - } - } - - CloseHandle(readEvent); - CloseHandle(connectEvent); - calloc.free(connectOverlapped); - calloc.free(overlapped); - calloc.free(bytesRead); - calloc.free(readBuf); - CloseHandle(handle); -} - -class NamedPipeServer { - final String pipeName; - Isolate? _isolate; - SendPort? _writePort; - ReceivePort? _responsePort; - StreamSubscription? _responseSub; - - final _dataController = StreamController.broadcast(); - Completer _connectionCompleter = Completer(); - - void Function()? onDisconnect; - - NamedPipeServer._(this.pipeName); - - Stream get dataStream => _dataController.stream; - Completer get connectionCompleter => _connectionCompleter; - - static Future bind(String pipeName) async { - final server = NamedPipeServer._(pipeName); - server._responsePort = ReceivePort(); - - server._responseSub = server._responsePort!.listen((message) { - if (message is SendPort) { - server._writePort = message; - if (server._connectionCompleter.isCompleted) { - server._connectionCompleter = Completer(); - } - server._connectionCompleter.complete(); - } else if (message is Uint8List) { - server._dataController.add(message); - } else if (message == 'closed') { - server._connectionCompleter = Completer(); - server.onDisconnect?.call(); - } else if (message is String && message.startsWith('error:')) { - commonPrint.log('[NamedPipeServer] $message', logLevel: LogLevel.error); - } - }); - - server._isolate = await Isolate.spawn(_pipeIsolate, [ - pipeName, - server._responsePort!.sendPort, - ]); - - return server; - } - - void writeln(String message) { - final wp = _writePort; - if (wp == null) return; - final str = '$message\n'; - wp.send(_PipeCmd(0, Uint8List.fromList(str.codeUnits))); - } - - Future close() async { - final wp = _writePort; - if (wp == null) return; - _writePort = null; - wp.send(const _PipeCmd(1, null)); - _responseSub?.cancel(); - _responsePort?.close(); - await Future.delayed(const Duration(milliseconds: 50)); - _isolate?.kill(); - _isolate = null; - } -} diff --git a/lib/common/protocol.dart b/lib/common/protocol.dart index 45fa1800..f3e241fb 100644 --- a/lib/common/protocol.dart +++ b/lib/common/protocol.dart @@ -2,6 +2,26 @@ import 'dart:io'; import 'package:win32_registry/win32_registry.dart'; +class ProtocolRegistrationPlan { + final String scheme; + final String executable; + + const ProtocolRegistrationPlan({ + required this.scheme, + required this.executable, + }); + + String get protocolKey => 'Software\\Classes\\$scheme'; + + String get commandKey => 'shell\\open\\command'; + + String get protocolValueName => 'URL Protocol'; + + String get protocolValue => ''; + + String get command => '"$executable" "%1"'; +} + class Protocol { static Protocol? _instance; @@ -13,19 +33,25 @@ class Protocol { } void register(String scheme) { - final String protocolRegKey = 'Software\\Classes\\$scheme'; - const RegistryValue protocolRegValue = RegistryValue.string( - 'URL Protocol', - '', + final plan = ProtocolRegistrationPlan( + scheme: scheme, + executable: Platform.resolvedExecutable, ); - const String protocolCmdRegKey = 'shell\\open\\command'; - final RegistryValue protocolCmdRegValue = RegistryValue.string( - '', - '"${Platform.resolvedExecutable}" "%1"', - ); - final regKey = Registry.currentUser.createKey(protocolRegKey); - regKey.createValue(protocolRegValue); - regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); + final regKey = CURRENT_USER.create(plan.protocolKey); + try { + regKey.setValue( + plan.protocolValueName, + RegistryValue.string(plan.protocolValue), + ); + final commandKey = regKey.create(plan.commandKey); + try { + commandKey.setValue('', RegistryValue.string(plan.command)); + } finally { + commandKey.close(); + } + } finally { + regKey.close(); + } } } diff --git a/lib/common/request.dart b/lib/common/request.dart index 2ae19edf..1bbe9f3c 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -113,30 +113,30 @@ class Request { final future = dio .get>( - source.key, - cancelToken: token, - options: Options(responseType: ResponseType.json), - ) + source.key, + cancelToken: token, + options: Options(responseType: ResponseType.json), + ) .timeout(const Duration(seconds: 10)); future .then((res) { - if (res.statusCode == HttpStatus.ok && res.data != null) { - completer.complete(Result.success(source.value(res.data!))); - return; - } - commonPrint.log('checkIp data empty', logLevel: LogLevel.info); - failureCount++; - handleFailRes(); - }) + if (res.statusCode == HttpStatus.ok && res.data != null) { + completer.complete(Result.success(source.value(res.data!))); + return; + } + commonPrint.log('checkIp data empty', logLevel: LogLevel.info); + failureCount++; + handleFailRes(); + }) .catchError((e) { - failureCount++; - if (e is DioException && e.type == DioExceptionType.cancel) { - completer.complete(Result.error('cancelled')); - return; - } - commonPrint.log('checkIp error $e', logLevel: LogLevel.warning); - handleFailRes(); - }); + failureCount++; + if (e is DioException && e.type == DioExceptionType.cancel) { + completer.complete(Result.error('cancelled')); + return; + } + commonPrint.log('checkIp error $e', logLevel: LogLevel.warning); + handleFailRes(); + }); return completer.future; }); final res = await Future.any(futures); @@ -149,9 +149,9 @@ class Request { try { final response = await dio .get( - 'http://$localhost:$helperPort/ping', - options: Options(responseType: ResponseType.plain), - ) + 'http://$localhost:$helperPort/ping', + options: Options(responseType: ResponseType.plain), + ) .timeout(const Duration(milliseconds: 2000)); if (response.statusCode != HttpStatus.ok) { return false; @@ -166,10 +166,10 @@ class Request { try { final response = await dio .post( - 'http://$localhost:$helperPort/start', - data: json.encode({'path': appPath.corePath, 'arg': arg}), - options: Options(responseType: ResponseType.plain), - ) + 'http://$localhost:$helperPort/start', + data: json.encode({'path': appPath.corePath, 'arg': arg}), + options: Options(responseType: ResponseType.plain), + ) .timeout(const Duration(milliseconds: 2000)); if (response.statusCode != HttpStatus.ok) { return false; @@ -185,9 +185,9 @@ class Request { try { final response = await dio .post( - 'http://$localhost:$helperPort/stop', - options: Options(responseType: ResponseType.plain), - ) + 'http://$localhost:$helperPort/stop', + options: Options(responseType: ResponseType.plain), + ) .timeout(const Duration(milliseconds: 2000)); if (response.statusCode != HttpStatus.ok) { return false; diff --git a/lib/common/system.dart b/lib/common/system.dart index 0844fc5f..f86178b5 100644 --- a/lib/common/system.dart +++ b/lib/common/system.dart @@ -104,6 +104,7 @@ class System { obscureText: true, title: currentAppLocalizations.pleaseInputAdminPassword, value: '', + inputFormatters: TextInputLimits.limit(TextInputLimits.password), ), ); if (password == null || password.isEmpty) { diff --git a/lib/common/task.dart b/lib/common/task.dart index 13b8a744..ea29017a 100644 --- a/lib/common/task.dart +++ b/lib/common/task.dart @@ -183,7 +183,7 @@ Future> _makeRealProfileTask( } } rawConfig['profile']['store-selected'] = false; - rawConfig['geox-url'] = realPatchConfig.geoXUrl.toJson(); + rawConfig['geox-url'] = realPatchConfig.geoXUrl.raw; rawConfig['global-ua'] = realPatchConfig.globalUa ?? defaultUA; if (rawConfig['hosts'] == null) { rawConfig['hosts'] = {}; @@ -331,10 +331,10 @@ Future _encodeLogsTask(List data) async { Future oldToNowTask(Map data) async { final homeDir = await appPath.homeDirPath; - return compute< - VM3, String, String>, - MigrationData - >(_oldToNowTask, VM3(data, homeDir, homeDir)); + return compute, String, String>, MigrationData>( + _oldToNowTask, + VM3(data, homeDir, homeDir), + ); } Future _oldToNowTask( diff --git a/lib/common/text.dart b/lib/common/text.dart index 043df405..ecfb28ca 100644 --- a/lib/common/text.dart +++ b/lib/common/text.dart @@ -11,11 +11,8 @@ extension TextStyleExtension on TextStyle { TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); - TextStyle get toJetBrainsMono => copyWith( - fontFamily: FontFamily.jetBrainsMono.value, - ); + TextStyle get toJetBrainsMono => + copyWith(fontFamily: FontFamily.jetBrainsMono.value); - TextStyle adjustSize(int size) => copyWith( - fontSize: fontSize! + size, - ); + TextStyle adjustSize(int size) => copyWith(fontSize: fontSize! + size); } diff --git a/lib/core/controller.dart b/lib/core/controller.dart index 5dd04ce1..c6b3bd54 100644 --- a/lib/core/controller.dart +++ b/lib/core/controller.dart @@ -177,8 +177,8 @@ class CoreController { return ExternalProvider.fromJson(json.decode(externalProvidersRawString)); } - Future updateGeoData(UpdateGeoDataParams params) { - return _interface.updateGeoData(params); + Future updateGeoData(String type) { + return _interface.updateGeoData(type); } Future sideLoadExternalProvider({ diff --git a/lib/core/event.dart b/lib/core/event.dart index 51f4d990..a6719f7d 100644 --- a/lib/core/event.dart +++ b/lib/core/event.dart @@ -14,6 +14,13 @@ abstract mixin class CoreEventListener { void onLoaded(String providerName) {} void onCrash(String message) {} + + void onGeoUpdate( + String geoType, + bool updating, + bool skipped, + String? error, + ) {} } class CoreEventManager { @@ -38,6 +45,15 @@ class CoreEventManager { case CoreEventType.crash: listener.onCrash(event.data); break; + case CoreEventType.geoUpdate: + final data = event.data as Map; + listener.onGeoUpdate( + data['type'] as String, + data['updating'] as bool, + data['skipped'] as bool? ?? false, + data['error'] as String?, + ); + break; } } }); diff --git a/lib/core/interface.dart b/lib/core/interface.dart index 4123cb63..81540172 100644 --- a/lib/core/interface.dart +++ b/lib/core/interface.dart @@ -38,7 +38,7 @@ mixin CoreInterface { Future? getExternalProvider(String externalProviderName); - Future updateGeoData(UpdateGeoDataParams params); + Future updateGeoData(String type); Future sideLoadExternalProvider({ required String providerName, @@ -214,10 +214,10 @@ abstract class CoreHandlerInterface with CoreInterface { } @override - Future updateGeoData(UpdateGeoDataParams params) async { + Future updateGeoData(String type) async { return await _invoke( method: ActionMethod.updateGeoData, - data: json.encode(params), + data: type, ) ?? ''; } diff --git a/lib/core/transport.dart b/lib/core/transport.dart index 369c2018..e7481bca 100644 --- a/lib/core/transport.dart +++ b/lib/core/transport.dart @@ -34,50 +34,50 @@ class IPCCoreTransport { try { final stream = restartIpcServer(name: address); _subscription = stream.listen( - (data) { - if (data.isEmpty) return; - final type = data[0]; - final payload = data.length > 1 ? data.sublist(1) : Uint8List(0); - switch (type) { - case _typeReady: - commonPrint.log('IPC Ready'); - if (_readyCompleter.isCompleted) { + (data) { + if (data.isEmpty) return; + final type = data[0]; + final payload = data.length > 1 ? data.sublist(1) : Uint8List(0); + switch (type) { + case _typeReady: + commonPrint.log('IPC Ready'); + if (_readyCompleter.isCompleted) { + break; + } + _readyCompleter.complete(); break; - } - _readyCompleter.complete(); - break; - case _typeConnected: - commonPrint.log('IPC Connected'); - if (_completer.isCompleted) { + case _typeConnected: + commonPrint.log('IPC Connected'); + if (_completer.isCompleted) { + break; + } + _completer.complete(); break; - } - _completer.complete(); - break; - case _typeDisconnected: - commonPrint.log('IPC Disconnected'); - _completer = Completer(); - onDisconnect?.call(); - break; - case _typeData: - _dataController.add(payload); - break; - case _typeError: - final msg = utf8.decode(payload); - commonPrint.log('IPC error: $msg', logLevel: LogLevel.error); - break; - default: - commonPrint.log( - 'IPC unknown frame type: $type', - logLevel: LogLevel.warning, - ); - } - }, - onError: (error) { - commonPrint.log('IPC error: $error', logLevel: LogLevel.error); - }, - cancelOnError: false, - ); - await _readyCompleter.future; + case _typeDisconnected: + commonPrint.log('IPC Disconnected'); + _completer = Completer(); + onDisconnect?.call(); + break; + case _typeData: + _dataController.add(payload); + break; + case _typeError: + final msg = utf8.decode(payload); + commonPrint.log('IPC error: $msg', logLevel: LogLevel.error); + break; + default: + commonPrint.log( + 'IPC unknown frame type: $type', + logLevel: LogLevel.warning, + ); + } + }, + onError: (error) { + commonPrint.log('IPC error: $error', logLevel: LogLevel.error); + }, + cancelOnError: false, + ); + await _readyCompleter.future; } catch (e) { commonPrint.log( 'Failed to start IPC server: $e', diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 3a58c4f3..6a25b645 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -135,7 +135,7 @@ enum ResultType { error, } -enum CoreEventType { log, delay, request, loaded, crash } +enum CoreEventType { log, delay, request, loaded, crash, geoUpdate } enum InvokeMessageType { protect, process } @@ -331,6 +331,40 @@ enum DashboardWidget { enum GeodataLoader { standard, memconservative } +enum GeoResource { + @JsonValue('mmdb') + MMDB, + @JsonValue('asn') + ASN, + @JsonValue('geo-ip') + GEOIP, + @JsonValue('geo-site') + GEOSITE; + + static GeoResource fromJson(String value) { + return switch (value) { + 'mmdb' => GeoResource.MMDB, + 'asn' => GeoResource.ASN, + 'geo-ip' || 'geoip' => GeoResource.GEOIP, + 'geo-site' || 'geosite' => GeoResource.GEOSITE, + _ => throw ArgumentError.value(value, 'value', 'Invalid geo resource'), + }; + } +} + +extension GeoResourceExt on GeoResource { + String get value { + return switch (this) { + GeoResource.MMDB => 'mmdb', + GeoResource.ASN => 'asn', + GeoResource.GEOIP => 'geo-ip', + GeoResource.GEOSITE => 'geo-site', + }; + } + + String get updatingKey => 'geo_resource_$name'; +} + enum PageLabel { dashboard, proxies, diff --git a/lib/features/overwrite/rule.dart b/lib/features/overwrite/rule.dart index 5ef08faa..8262b27e 100644 --- a/lib/features/overwrite/rule.dart +++ b/lib/features/overwrite/rule.dart @@ -251,7 +251,7 @@ class _AddOrEditRuleDialogState extends State { return; } final rule = Rule( - id: widget.rule?.id ?? -1, + id: widget.rule?.id ?? snowflake.id, ruleAction: _ruleAction, content: _contentController.text, ruleTarget: _ruleTargetController.text, @@ -310,6 +310,9 @@ class _AddOrEditRuleDialogState extends State { const SizedBox(height: 24), TextFormField( keyboardType: TextInputType.text, + inputFormatters: TextInputLimits.limit( + TextInputLimits.rule, + ), onFieldSubmitted: (_) { _handleSubmit(); }, diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 3782f141..c4da298d 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -33,40 +33,52 @@ class MessageLookup extends MessageLookupByLibrary { static String m4(label) => "${label} cannot be empty"; - static String m5(label) => "Current ${label} already exists"; + static String m5(count) => "${count} entries"; - static String m6(count) => + static String m6(label) => "Current ${label} already exists"; + + static String m7(name) => "${name} skipped"; + + static String m8(name) => "${name} updated"; + + static String m9(name) => "Updating ${name}..."; + + static String m10(count) => "${Intl.plural(count, one: '1 hour ago', other: '${count} hours ago')}"; - static String m7(target) => "${target} is an invalid policy"; + static String m11(count) => "${count} hours"; - static String m8(proxyName) => "${proxyName} is an invalid proxy"; + static String m12(target) => "${target} is an invalid policy"; - static String m9(providerName) => + static String m13(proxyName) => "${proxyName} is an invalid proxy"; + + static String m14(providerName) => "${providerName} is an invalid proxy provider"; - static String m10(subRule) => "${subRule} is an invalid SUB_RULE"; + static String m15(subRule) => "${subRule} is an invalid SUB_RULE"; - static String m11(appName) => + static String m16(appName) => "1. Open System Settings > Privacy & Security\n2. Choose Location Services\n3. Find and check ${appName} in the right list\n\nAfter completing the setup, return to the app and use it normally. Thank you for your cooperation."; - static String m12(count) => + static String m17(count) => "${Intl.plural(count, one: '1 minute ago', other: '${count} minutes ago')}"; - static String m13(count) => + static String m18(count) => "${Intl.plural(count, one: '1 month ago', other: '${count} months ago')}"; - static String m14(label) => "No ${label} yet"; + static String m19(label) => "No ${label} yet"; - static String m15(label) => "${label} must be a number"; + static String m20(label) => "${label} must be a number"; - static String m16(label) => "${label} must be between 1024 and 49151"; + static String m21(label) => "${label} must be between 1024 and 49151"; - static String m17(count) => "${count} items have been selected"; + static String m22(count) => "${count} seconds"; - static String m18(label) => "${label} must be a url"; + static String m23(count) => "${count} items have been selected"; - static String m19(count) => + static String m24(label) => "${label} must be a url"; + + static String m25(count) => "${Intl.plural(count, one: '1 year ago', other: '${count} years ago')}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -343,6 +355,7 @@ class MessageLookup extends MessageLookupByLibrary { "emptyTip": m4, "en": MessageLookupByLibrary.simpleMessage("English"), "entries": MessageLookupByLibrary.simpleMessage(" entries"), + "entriesCount": m5, "exclude": MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"), "excludeDesc": MessageLookupByLibrary.simpleMessage( "When the app is in the background, the app is hidden from the recent task", @@ -355,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { "When connected to an excluded SSID Wi-Fi, the app running state will be automatically switched.", ), "excludeType": MessageLookupByLibrary.simpleMessage("Exclude type"), - "existsTip": m5, + "existsTip": m6, "exit": MessageLookupByLibrary.simpleMessage("Exit"), "expand": MessageLookupByLibrary.simpleMessage("Standard"), "expectedStatus": MessageLookupByLibrary.simpleMessage("Expected status"), @@ -394,6 +407,18 @@ class MessageLookup extends MessageLookupByLibrary { ), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("FruitSalad"), "general": MessageLookupByLibrary.simpleMessage("General"), + "geoAutoUpdate": MessageLookupByLibrary.simpleMessage("Auto Update"), + "geoAutoUpdateInterval": MessageLookupByLibrary.simpleMessage( + "Auto Update Interval", + ), + "geoAutoUpdateIntervalTip": MessageLookupByLibrary.simpleMessage( + "Auto update interval must be greater than 0", + ), + "geoOptions": MessageLookupByLibrary.simpleMessage("Geo Options"), + "geoResources": MessageLookupByLibrary.simpleMessage("Geo Resources"), + "geoSkipped": m7, + "geoUpdated": m8, + "geoUpdating": m9, "geodataLoader": MessageLookupByLibrary.simpleMessage( "Geo Low Memory Mode", ), @@ -420,7 +445,9 @@ class MessageLookup extends MessageLookupByLibrary { "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage( "Use keyboard to control applications", ), - "hoursAgo": m6, + "hours": MessageLookupByLibrary.simpleMessage("hours"), + "hoursAgo": m10, + "hoursCount": m11, "icon": MessageLookupByLibrary.simpleMessage("Icon"), "iconRecords": MessageLookupByLibrary.simpleMessage("Icon records"), "iconStyle": MessageLookupByLibrary.simpleMessage("Icon style"), @@ -464,10 +491,10 @@ class MessageLookup extends MessageLookupByLibrary { "invalidBackupFile": MessageLookupByLibrary.simpleMessage( "Invalid backup file", ), - "invalidPolicy": m7, - "invalidProxy": m8, - "invalidProxyProvider": m9, - "invalidSubRule": m10, + "invalidPolicy": m12, + "invalidProxy": m13, + "invalidProxyProvider": m14, + "invalidSubRule": m15, "ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( "When turned on it will be able to receive IPv6 traffic", @@ -501,7 +528,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "According to system requirements, obtaining the Wi-Fi name requires you to grant location permission.", ), - "locationPermissionGuide": m11, + "locationPermissionGuide": m16, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "Location Permission Required", ), @@ -531,11 +558,11 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "Modify the default system exit event", ), - "minutesAgo": m12, + "minutesAgo": m17, "mixedPort": MessageLookupByLibrary.simpleMessage("Mixed Port"), "mode": MessageLookupByLibrary.simpleMessage("Mode"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("Monochrome"), - "monthsAgo": m13, + "monthsAgo": m18, "more": MessageLookupByLibrary.simpleMessage("More"), "name": MessageLookupByLibrary.simpleMessage("Name"), "nameserver": MessageLookupByLibrary.simpleMessage("Nameserver"), @@ -581,8 +608,8 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "No profile, Please add a profile", ), - "nullTip": m14, - "numberTip": m15, + "nullTip": m19, + "numberTip": m20, "onDemand": MessageLookupByLibrary.simpleMessage("On Demand"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "Configure the program running state for specific scenarios", @@ -631,7 +658,7 @@ class MessageLookup extends MessageLookupByLibrary { "portConflictTip": MessageLookupByLibrary.simpleMessage( "Please enter a different port", ), - "portTip": m16, + "portTip": m21, "preferH3Desc": MessageLookupByLibrary.simpleMessage( "Prioritize the use of DOH\'s http/3", ), @@ -888,6 +915,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "search": MessageLookupByLibrary.simpleMessage("Search"), "seconds": MessageLookupByLibrary.simpleMessage("Seconds"), + "secondsCount": m22, "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selectProxies": MessageLookupByLibrary.simpleMessage("Select proxies"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage( @@ -903,7 +931,7 @@ class MessageLookup extends MessageLookupByLibrary { "Please select sub rule", ), "selected": MessageLookupByLibrary.simpleMessage("Selected"), - "selectedCountTitle": m17, + "selectedCountTitle": m23, "settings": MessageLookupByLibrary.simpleMessage("Settings"), "show": MessageLookupByLibrary.simpleMessage("Show"), "shrink": MessageLookupByLibrary.simpleMessage("Shrink"), @@ -1003,9 +1031,10 @@ class MessageLookup extends MessageLookupByLibrary { "urlDesc": MessageLookupByLibrary.simpleMessage( "Obtain profile through URL", ), - "urlTip": m18, + "urlTip": m24, "useHosts": MessageLookupByLibrary.simpleMessage("Use hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("Use system hosts"), + "userAgent": MessageLookupByLibrary.simpleMessage("User-Agent"), "value": MessageLookupByLibrary.simpleMessage("Value"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("Vibrant"), "view": MessageLookupByLibrary.simpleMessage("View"), @@ -1022,7 +1051,7 @@ class MessageLookup extends MessageLookupByLibrary { "WebDAV configuration", ), "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"), - "yearsAgo": m19, + "yearsAgo": m25, "zh_CN": MessageLookupByLibrary.simpleMessage("Simplified Chinese"), }; } diff --git a/lib/l10n/intl/messages_ja.dart b/lib/l10n/intl/messages_ja.dart index 7a6df21d..ceac8a46 100644 --- a/lib/l10n/intl/messages_ja.dart +++ b/lib/l10n/intl/messages_ja.dart @@ -30,36 +30,48 @@ class MessageLookup extends MessageLookupByLibrary { static String m4(label) => "${label}は空欄にできません"; - static String m5(label) => "現在の${label}は既に存在しています"; + static String m5(count) => "${count} エントリ"; - static String m6(count) => "${count}時間前"; + static String m6(label) => "現在の${label}は既に存在しています"; - static String m7(target) => "${target} は無効なポリシーです"; + static String m7(name) => "${name} スキップ済み"; - static String m8(proxyName) => "${proxyName} は無効なプロキシです"; + static String m8(name) => "${name} 更新済み"; - static String m9(providerName) => "${providerName} は無効なプロキシプロバイダーです"; + static String m9(name) => "${name}を更新中..."; - static String m10(subRule) => "${subRule} は無効なSUB_RULEです"; + static String m10(count) => "${count}時間前"; - static String m11(appName) => + static String m11(count) => "${count} 時間"; + + static String m12(target) => "${target} は無効なポリシーです"; + + static String m13(proxyName) => "${proxyName} は無効なプロキシです"; + + static String m14(providerName) => "${providerName} は無効なプロキシプロバイダーです"; + + static String m15(subRule) => "${subRule} は無効なSUB_RULEです"; + + static String m16(appName) => "1. Open System Settings > Privacy & Security\n2. Choose Location Services\n3. Find and check ${appName} in the right list\n\nAfter completing the setup, return to the app and use it normally. Thank you for your cooperation."; - static String m12(count) => "${count}分前"; + static String m17(count) => "${count}分前"; - static String m13(count) => "${count}ヶ月前"; + static String m18(count) => "${count}ヶ月前"; - static String m14(label) => "まだ${label}はありません"; + static String m19(label) => "まだ${label}はありません"; - static String m15(label) => "${label}は数字でなければなりません"; + static String m20(label) => "${label}は数字でなければなりません"; - static String m16(label) => "${label} は 1024 から 49151 の間でなければなりません"; + static String m21(label) => "${label} は 1024 から 49151 の間でなければなりません"; - static String m17(count) => "${count} 項目が選択されています"; + static String m22(count) => "${count} 秒"; - static String m18(label) => "${label}はURLである必要があります"; + static String m23(count) => "${count} 項目が選択されています"; - static String m19(count) => "${count}年前"; + static String m24(label) => "${label}はURLである必要があります"; + + static String m25(count) => "${count}年前"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -265,6 +277,7 @@ class MessageLookup extends MessageLookupByLibrary { "emptyTip": m4, "en": MessageLookupByLibrary.simpleMessage("英語"), "entries": MessageLookupByLibrary.simpleMessage(" エントリ"), + "entriesCount": m5, "exclude": MessageLookupByLibrary.simpleMessage("最近のタスクから非表示"), "excludeDesc": MessageLookupByLibrary.simpleMessage( "アプリがバックグラウンド時に最近のタスクから非表示", @@ -275,7 +288,7 @@ class MessageLookup extends MessageLookupByLibrary { "When connected to an excluded SSID Wi-Fi, the app running state will be automatically switched.", ), "excludeType": MessageLookupByLibrary.simpleMessage("除外タイプ"), - "existsTip": m5, + "existsTip": m6, "exit": MessageLookupByLibrary.simpleMessage("終了"), "expand": MessageLookupByLibrary.simpleMessage("標準"), "expectedStatus": MessageLookupByLibrary.simpleMessage("期待されるステータス"), @@ -310,6 +323,16 @@ class MessageLookup extends MessageLookupByLibrary { ), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("フルーツサラダ"), "general": MessageLookupByLibrary.simpleMessage("一般"), + "geoAutoUpdate": MessageLookupByLibrary.simpleMessage("自動更新"), + "geoAutoUpdateInterval": MessageLookupByLibrary.simpleMessage("自動更新間隔"), + "geoAutoUpdateIntervalTip": MessageLookupByLibrary.simpleMessage( + "自動更新間隔は0より大きくなければなりません", + ), + "geoOptions": MessageLookupByLibrary.simpleMessage("Geoオプション"), + "geoResources": MessageLookupByLibrary.simpleMessage("Geoリソース"), + "geoSkipped": m7, + "geoUpdated": m8, + "geoUpdating": m9, "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低メモリモード"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "有効化するとGeo低メモリローダーを使用", @@ -328,7 +351,9 @@ class MessageLookup extends MessageLookupByLibrary { "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage( "キーボードでアプリを制御", ), - "hoursAgo": m6, + "hours": MessageLookupByLibrary.simpleMessage("時間"), + "hoursAgo": m10, + "hoursCount": m11, "icon": MessageLookupByLibrary.simpleMessage("アイコン"), "iconRecords": MessageLookupByLibrary.simpleMessage("アイコン履歴"), "iconStyle": MessageLookupByLibrary.simpleMessage("アイコンスタイル"), @@ -360,10 +385,10 @@ class MessageLookup extends MessageLookupByLibrary { "interval": MessageLookupByLibrary.simpleMessage("インターバル"), "intranetIP": MessageLookupByLibrary.simpleMessage("イントラネットIP"), "invalidBackupFile": MessageLookupByLibrary.simpleMessage("無効なバックアップファイル"), - "invalidPolicy": m7, - "invalidProxy": m8, - "invalidProxyProvider": m9, - "invalidSubRule": m10, + "invalidPolicy": m12, + "invalidProxy": m13, + "invalidProxyProvider": m14, + "invalidSubRule": m15, "ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("有効化するとIPv6トラフィックを受信可能"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("IPv6インバウンドを許可"), @@ -391,7 +416,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "According to system requirements, obtaining the Wi-Fi name requires you to grant location permission.", ), - "locationPermissionGuide": m11, + "locationPermissionGuide": m16, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "Location Permission Required", ), @@ -415,11 +440,11 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "システムの終了イベントを変更", ), - "minutesAgo": m12, + "minutesAgo": m17, "mixedPort": MessageLookupByLibrary.simpleMessage("混合ポート"), "mode": MessageLookupByLibrary.simpleMessage("モード"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("モノクローム"), - "monthsAgo": m13, + "monthsAgo": m18, "more": MessageLookupByLibrary.simpleMessage("詳細"), "name": MessageLookupByLibrary.simpleMessage("名前"), "nameserver": MessageLookupByLibrary.simpleMessage("ネームサーバー"), @@ -453,8 +478,8 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "プロファイルがありません。追加してください", ), - "nullTip": m14, - "numberTip": m15, + "nullTip": m19, + "numberTip": m20, "onDemand": MessageLookupByLibrary.simpleMessage("On Demand"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "Configure the program running state for specific scenarios", @@ -497,7 +522,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "port": MessageLookupByLibrary.simpleMessage("ポート"), "portConflictTip": MessageLookupByLibrary.simpleMessage("別のポートを入力してください"), - "portTip": m16, + "portTip": m21, "preferH3Desc": MessageLookupByLibrary.simpleMessage("DOHのHTTP/3を優先使用"), "prerequisites": MessageLookupByLibrary.simpleMessage("Prerequisites"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("キーボードを押してください"), @@ -716,6 +741,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "search": MessageLookupByLibrary.simpleMessage("検索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), + "secondsCount": m22, "selectAll": MessageLookupByLibrary.simpleMessage("すべて選択"), "selectProxies": MessageLookupByLibrary.simpleMessage("プロキシを選択"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage( @@ -727,7 +753,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "selectSubRule": MessageLookupByLibrary.simpleMessage("サブルールを選択してください"), "selected": MessageLookupByLibrary.simpleMessage("選択済み"), - "selectedCountTitle": m17, + "selectedCountTitle": m23, "settings": MessageLookupByLibrary.simpleMessage("設定"), "show": MessageLookupByLibrary.simpleMessage("表示"), "shrink": MessageLookupByLibrary.simpleMessage("縮小"), @@ -809,9 +835,10 @@ class MessageLookup extends MessageLookupByLibrary { "upload": MessageLookupByLibrary.simpleMessage("アップロード"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("URL経由でプロファイルを取得"), - "urlTip": m18, + "urlTip": m24, "useHosts": MessageLookupByLibrary.simpleMessage("ホストを使用"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("システムホストを使用"), + "userAgent": MessageLookupByLibrary.simpleMessage("ユーザーエージェント"), "value": MessageLookupByLibrary.simpleMessage("値"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("ビブラント"), "view": MessageLookupByLibrary.simpleMessage("表示"), @@ -824,7 +851,7 @@ class MessageLookup extends MessageLookupByLibrary { "vpnTip": MessageLookupByLibrary.simpleMessage("変更はVPN再起動後に有効"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV設定"), "whitelistMode": MessageLookupByLibrary.simpleMessage("ホワイトリストモード"), - "yearsAgo": m19, + "yearsAgo": m25, "zh_CN": MessageLookupByLibrary.simpleMessage("簡体字中国語"), }; } diff --git a/lib/l10n/intl/messages_ru.dart b/lib/l10n/intl/messages_ru.dart index 0295bcf5..adf4d6de 100644 --- a/lib/l10n/intl/messages_ru.dart +++ b/lib/l10n/intl/messages_ru.dart @@ -32,40 +32,52 @@ class MessageLookup extends MessageLookupByLibrary { static String m4(label) => "${label} не может быть пустым"; - static String m5(label) => "Текущий ${label} уже существует"; + static String m5(count) => "${count} записей"; - static String m6(count) => + static String m6(label) => "Текущий ${label} уже существует"; + + static String m7(name) => "${name} пропущено"; + + static String m8(name) => "${name} обновлено"; + + static String m9(name) => "Обновление ${name}..."; + + static String m10(count) => "${Intl.plural(count, one: '${count} час назад', few: '${count} часа назад', many: '${count} часов назад', other: '${count} часа назад')}"; - static String m7(target) => "${target} является недопустимой политикой"; + static String m11(count) => "${count} часов"; - static String m8(proxyName) => "${proxyName} является недопустимым прокси"; + static String m12(target) => "${target} является недопустимой политикой"; - static String m9(providerName) => + static String m13(proxyName) => "${proxyName} является недопустимым прокси"; + + static String m14(providerName) => "${providerName} является недопустимым провайдером прокси"; - static String m10(subRule) => "${subRule} является недопустимым подправилом"; + static String m15(subRule) => "${subRule} является недопустимым подправилом"; - static String m11(appName) => + static String m16(appName) => "1. Open System Settings > Privacy & Security\n2. Choose Location Services\n3. Find and check ${appName} in the right list\n\nAfter completing the setup, return to the app and use it normally. Thank you for your cooperation."; - static String m12(count) => + static String m17(count) => "${Intl.plural(count, one: '${count} минута назад', few: '${count} минуты назад', many: '${count} минут назад', other: '${count} минуты назад')}"; - static String m13(count) => + static String m18(count) => "${Intl.plural(count, one: '${count} месяц назад', few: '${count} месяца назад', many: '${count} месяцев назад', other: '${count} месяца назад')}"; - static String m14(label) => "${label} пока отсутствуют"; + static String m19(label) => "${label} пока отсутствуют"; - static String m15(label) => "${label} должно быть числом"; + static String m20(label) => "${label} должно быть числом"; - static String m16(label) => "${label} должен быть числом от 1024 до 49151"; + static String m21(label) => "${label} должен быть числом от 1024 до 49151"; - static String m17(count) => "Выбрано ${count} элементов"; + static String m22(count) => "${count} секунд"; - static String m18(label) => "${label} должен быть URL"; + static String m23(count) => "Выбрано ${count} элементов"; - static String m19(count) => + static String m24(label) => "${label} должен быть URL"; + + static String m25(count) => "${Intl.plural(count, one: '${count} год назад', few: '${count} года назад', many: '${count} лет назад', other: '${count} года назад')}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -352,6 +364,7 @@ class MessageLookup extends MessageLookupByLibrary { "emptyTip": m4, "en": MessageLookupByLibrary.simpleMessage("Английский"), "entries": MessageLookupByLibrary.simpleMessage(" записей"), + "entriesCount": m5, "exclude": MessageLookupByLibrary.simpleMessage( "Скрыть из последних задач", ), @@ -366,7 +379,7 @@ class MessageLookup extends MessageLookupByLibrary { "When connected to an excluded SSID Wi-Fi, the app running state will be automatically switched.", ), "excludeType": MessageLookupByLibrary.simpleMessage("Тип исключения"), - "existsTip": m5, + "existsTip": m6, "exit": MessageLookupByLibrary.simpleMessage("Выход"), "expand": MessageLookupByLibrary.simpleMessage("Стандартный"), "expectedStatus": MessageLookupByLibrary.simpleMessage("Ожидаемый статус"), @@ -409,6 +422,18 @@ class MessageLookup extends MessageLookupByLibrary { ), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("Фруктовый микс"), "general": MessageLookupByLibrary.simpleMessage("Общие"), + "geoAutoUpdate": MessageLookupByLibrary.simpleMessage("Автообновление"), + "geoAutoUpdateInterval": MessageLookupByLibrary.simpleMessage( + "Интервал автообновления", + ), + "geoAutoUpdateIntervalTip": MessageLookupByLibrary.simpleMessage( + "Интервал автообновления должен быть больше 0", + ), + "geoOptions": MessageLookupByLibrary.simpleMessage("Настройки Geo"), + "geoResources": MessageLookupByLibrary.simpleMessage("Ресурсы Geo"), + "geoSkipped": m7, + "geoUpdated": m8, + "geoUpdating": m9, "geodataLoader": MessageLookupByLibrary.simpleMessage( "Режим низкого потребления памяти для геоданных", ), @@ -437,7 +462,9 @@ class MessageLookup extends MessageLookupByLibrary { "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage( "Использование клавиатуры для управления приложением", ), - "hoursAgo": m6, + "hours": MessageLookupByLibrary.simpleMessage("часов"), + "hoursAgo": m10, + "hoursCount": m11, "icon": MessageLookupByLibrary.simpleMessage("Иконка"), "iconRecords": MessageLookupByLibrary.simpleMessage("История иконок"), "iconStyle": MessageLookupByLibrary.simpleMessage("Стиль иконки"), @@ -483,10 +510,10 @@ class MessageLookup extends MessageLookupByLibrary { "invalidBackupFile": MessageLookupByLibrary.simpleMessage( "Неверный файл резервной копии", ), - "invalidPolicy": m7, - "invalidProxy": m8, - "invalidProxyProvider": m9, - "invalidSubRule": m10, + "invalidPolicy": m12, + "invalidProxy": m13, + "invalidProxyProvider": m14, + "invalidSubRule": m15, "ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( "При включении будет возможно получать IPv6 трафик", @@ -520,7 +547,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "According to system requirements, obtaining the Wi-Fi name requires you to grant location permission.", ), - "locationPermissionGuide": m11, + "locationPermissionGuide": m16, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "Location Permission Required", ), @@ -558,11 +585,11 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "Изменить стандартное событие выхода из системы", ), - "minutesAgo": m12, + "minutesAgo": m17, "mixedPort": MessageLookupByLibrary.simpleMessage("Смешанный порт"), "mode": MessageLookupByLibrary.simpleMessage("Режим"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("Монохром"), - "monthsAgo": m13, + "monthsAgo": m18, "more": MessageLookupByLibrary.simpleMessage("Еще"), "name": MessageLookupByLibrary.simpleMessage("Имя"), "nameserver": MessageLookupByLibrary.simpleMessage("Сервер имен"), @@ -608,8 +635,8 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "Нет профиля, пожалуйста, добавьте профиль", ), - "nullTip": m14, - "numberTip": m15, + "nullTip": m19, + "numberTip": m20, "onDemand": MessageLookupByLibrary.simpleMessage("On Demand"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "Configure the program running state for specific scenarios", @@ -666,7 +693,7 @@ class MessageLookup extends MessageLookupByLibrary { "portConflictTip": MessageLookupByLibrary.simpleMessage( "Введите другой порт", ), - "portTip": m16, + "portTip": m21, "preferH3Desc": MessageLookupByLibrary.simpleMessage( "Приоритетное использование HTTP/3 для DOH", ), @@ -935,6 +962,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "search": MessageLookupByLibrary.simpleMessage("Поиск"), "seconds": MessageLookupByLibrary.simpleMessage("Секунд"), + "secondsCount": m22, "selectAll": MessageLookupByLibrary.simpleMessage("Выбрать все"), "selectProxies": MessageLookupByLibrary.simpleMessage("Выбрать прокси"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage( @@ -950,7 +978,7 @@ class MessageLookup extends MessageLookupByLibrary { "Пожалуйста, выберите подправило", ), "selected": MessageLookupByLibrary.simpleMessage("Выбрано"), - "selectedCountTitle": m17, + "selectedCountTitle": m23, "settings": MessageLookupByLibrary.simpleMessage("Настройки"), "show": MessageLookupByLibrary.simpleMessage("Показать"), "shrink": MessageLookupByLibrary.simpleMessage("Сжать"), @@ -1064,11 +1092,12 @@ class MessageLookup extends MessageLookupByLibrary { "urlDesc": MessageLookupByLibrary.simpleMessage( "Получить профиль через URL", ), - "urlTip": m18, + "urlTip": m24, "useHosts": MessageLookupByLibrary.simpleMessage("Использовать hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage( "Использовать системные hosts", ), + "userAgent": MessageLookupByLibrary.simpleMessage("User-Agent"), "value": MessageLookupByLibrary.simpleMessage("Значение"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("Яркие"), "view": MessageLookupByLibrary.simpleMessage("Просмотр"), @@ -1087,7 +1116,7 @@ class MessageLookup extends MessageLookupByLibrary { "whitelistMode": MessageLookupByLibrary.simpleMessage( "Режим белого списка", ), - "yearsAgo": m19, + "yearsAgo": m25, "zh_CN": MessageLookupByLibrary.simpleMessage("Упрощенный китайский"), }; } diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 3797ff58..e1982612 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -30,36 +30,48 @@ class MessageLookup extends MessageLookupByLibrary { static String m4(label) => "${label}不能为空"; - static String m5(label) => "${label}当前已存在"; + static String m5(count) => "${count} 个条目"; - static String m6(count) => "${count} 小时前"; + static String m6(label) => "${label}当前已存在"; - static String m7(target) => "${target} 是一个无效的策略"; + static String m7(name) => "${name} 已跳过"; - static String m8(proxyName) => "${proxyName} 是一个无效的代理"; + static String m8(name) => "${name} 已更新"; - static String m9(providerName) => "${providerName} 是一个无效的代理集"; + static String m9(name) => "正在更新 ${name}..."; - static String m10(subRule) => "${subRule} 是一个无效的SUB_RULE"; + static String m10(count) => "${count} 小时前"; - static String m11(appName) => + static String m11(count) => "${count} 小时"; + + static String m12(target) => "${target} 是一个无效的策略"; + + static String m13(proxyName) => "${proxyName} 是一个无效的代理"; + + static String m14(providerName) => "${providerName} 是一个无效的代理集"; + + static String m15(subRule) => "${subRule} 是一个无效的SUB_RULE"; + + static String m16(appName) => "1. 打开 系统设置 > 隐私与安全性\n2. 选择 定位服务\n3. 在右侧列表中找到并勾选 ${appName}\n\n完成设置后,返回应用即可正常使用。感谢您的配合。"; - static String m12(count) => "${count} 分钟前"; + static String m17(count) => "${count} 分钟前"; - static String m13(count) => "${count} 个月前"; + static String m18(count) => "${count} 个月前"; - static String m14(label) => "暂无${label}"; + static String m19(label) => "暂无${label}"; - static String m15(label) => "${label}必须为数字"; + static String m20(label) => "${label}必须为数字"; - static String m16(label) => "${label} 必须在 1024 到 49151 之间"; + static String m21(label) => "${label} 必须在 1024 到 49151 之间"; - static String m17(count) => "已选择 ${count} 项"; + static String m22(count) => "${count} 秒"; - static String m18(label) => "${label}必须为URL"; + static String m23(count) => "已选择 ${count} 项"; - static String m19(count) => "${count} 年前"; + static String m24(label) => "${label}必须为URL"; + + static String m25(count) => "${count} 年前"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -237,6 +249,7 @@ class MessageLookup extends MessageLookupByLibrary { "emptyTip": m4, "en": MessageLookupByLibrary.simpleMessage("英语"), "entries": MessageLookupByLibrary.simpleMessage("个条目"), + "entriesCount": m5, "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), "excludeProxyFilter": MessageLookupByLibrary.simpleMessage("排除节点过滤器"), @@ -245,7 +258,7 @@ class MessageLookup extends MessageLookupByLibrary { "连接到被排除SSID的WIFI时,将会自动切换应用运行状态", ), "excludeType": MessageLookupByLibrary.simpleMessage("排除类型"), - "existsTip": m5, + "existsTip": m6, "exit": MessageLookupByLibrary.simpleMessage("退出"), "expand": MessageLookupByLibrary.simpleMessage("标准"), "expectedStatus": MessageLookupByLibrary.simpleMessage("预期状态"), @@ -274,6 +287,16 @@ class MessageLookup extends MessageLookupByLibrary { "forceRestartCoreTip": MessageLookupByLibrary.simpleMessage("您确定要强制重启核心吗?"), "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("果缤纷"), "general": MessageLookupByLibrary.simpleMessage("常规"), + "geoAutoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"), + "geoAutoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔"), + "geoAutoUpdateIntervalTip": MessageLookupByLibrary.simpleMessage( + "自动更新间隔必须大于0", + ), + "geoOptions": MessageLookupByLibrary.simpleMessage("Geo 选项"), + "geoResources": MessageLookupByLibrary.simpleMessage("Geo 资源"), + "geoSkipped": m7, + "geoUpdated": m8, + "geoUpdating": m9, "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"), "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"), @@ -288,7 +311,9 @@ class MessageLookup extends MessageLookupByLibrary { "hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"), - "hoursAgo": m6, + "hours": MessageLookupByLibrary.simpleMessage("小时"), + "hoursAgo": m10, + "hoursCount": m11, "icon": MessageLookupByLibrary.simpleMessage("图片"), "iconRecords": MessageLookupByLibrary.simpleMessage("图标记录"), "iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"), @@ -316,10 +341,10 @@ class MessageLookup extends MessageLookupByLibrary { "interval": MessageLookupByLibrary.simpleMessage("间隔"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "invalidBackupFile": MessageLookupByLibrary.simpleMessage("无效备份文件"), - "invalidPolicy": m7, - "invalidProxy": m8, - "invalidProxyProvider": m9, - "invalidSubRule": m10, + "invalidPolicy": m12, + "invalidProxy": m13, + "invalidProxyProvider": m14, + "invalidSubRule": m15, "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"), @@ -343,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "根据系统要求,获取Wi-Fi名称需要您授予位置权限。", ), - "locationPermissionGuide": m11, + "locationPermissionGuide": m16, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "需要位置权限", ), @@ -365,11 +390,11 @@ class MessageLookup extends MessageLookupByLibrary { "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), - "minutesAgo": m12, + "minutesAgo": m17, "mixedPort": MessageLookupByLibrary.simpleMessage("混合端口"), "mode": MessageLookupByLibrary.simpleMessage("模式"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("单色"), - "monthsAgo": m13, + "monthsAgo": m18, "more": MessageLookupByLibrary.simpleMessage("更多"), "name": MessageLookupByLibrary.simpleMessage("名称"), "nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"), @@ -395,8 +420,8 @@ class MessageLookup extends MessageLookupByLibrary { "none": MessageLookupByLibrary.simpleMessage("无"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"), "nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), - "nullTip": m14, - "numberTip": m15, + "nullTip": m19, + "numberTip": m20, "onDemand": MessageLookupByLibrary.simpleMessage("按需运行"), "onDemandDesc": MessageLookupByLibrary.simpleMessage("配置程序特定场景运行状态"), "onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"), @@ -431,7 +456,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "port": MessageLookupByLibrary.simpleMessage("端口"), "portConflictTip": MessageLookupByLibrary.simpleMessage("请输入不同的端口"), - "portTip": m16, + "portTip": m21, "preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"), "prerequisites": MessageLookupByLibrary.simpleMessage("前置条件"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"), @@ -614,6 +639,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "search": MessageLookupByLibrary.simpleMessage("搜索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), + "secondsCount": m22, "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selectProxies": MessageLookupByLibrary.simpleMessage("选择代理"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage("选择代理集"), @@ -621,7 +647,7 @@ class MessageLookup extends MessageLookupByLibrary { "selectSplitStrategy": MessageLookupByLibrary.simpleMessage("请选择分流策略"), "selectSubRule": MessageLookupByLibrary.simpleMessage("请选择子规则"), "selected": MessageLookupByLibrary.simpleMessage("已选择"), - "selectedCountTitle": m17, + "selectedCountTitle": m23, "settings": MessageLookupByLibrary.simpleMessage("设置"), "show": MessageLookupByLibrary.simpleMessage("显示"), "shrink": MessageLookupByLibrary.simpleMessage("紧凑"), @@ -697,9 +723,10 @@ class MessageLookup extends MessageLookupByLibrary { "upload": MessageLookupByLibrary.simpleMessage("上传"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"), - "urlTip": m18, + "urlTip": m24, "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), + "userAgent": MessageLookupByLibrary.simpleMessage("用户代理"), "value": MessageLookupByLibrary.simpleMessage("值"), "vibrantScheme": MessageLookupByLibrary.simpleMessage("活力"), "view": MessageLookupByLibrary.simpleMessage("查看"), @@ -712,7 +739,7 @@ class MessageLookup extends MessageLookupByLibrary { "vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), - "yearsAgo": m19, + "yearsAgo": m25, "zh_CN": MessageLookupByLibrary.simpleMessage("中文简体"), }; } diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 52825a80..3b96b136 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1039,6 +1039,11 @@ class AppLocalizations { ); } + /// `User-Agent` + String get userAgent { + return Intl.message('User-Agent', name: 'userAgent', desc: '', args: []); + } + /// `Unified delay` String get unifiedDelay { return Intl.message( @@ -4378,6 +4383,116 @@ class AppLocalizations { String get suspended { return Intl.message('Suspended...', name: 'suspended', desc: '', args: []); } + + /// `Geo Options` + String get geoOptions { + return Intl.message('Geo Options', name: 'geoOptions', desc: '', args: []); + } + + /// `Auto Update` + String get geoAutoUpdate { + return Intl.message( + 'Auto Update', + name: 'geoAutoUpdate', + desc: '', + args: [], + ); + } + + /// `Auto Update Interval` + String get geoAutoUpdateInterval { + return Intl.message( + 'Auto Update Interval', + name: 'geoAutoUpdateInterval', + desc: '', + args: [], + ); + } + + /// `Auto update interval must be greater than 0` + String get geoAutoUpdateIntervalTip { + return Intl.message( + 'Auto update interval must be greater than 0', + name: 'geoAutoUpdateIntervalTip', + desc: '', + args: [], + ); + } + + /// `hours` + String get hours { + return Intl.message('hours', name: 'hours', desc: '', args: []); + } + + /// `{count} hours` + String hoursCount(Object count) { + return Intl.message( + '$count hours', + name: 'hoursCount', + desc: '', + args: [count], + ); + } + + /// `Geo Resources` + String get geoResources { + return Intl.message( + 'Geo Resources', + name: 'geoResources', + desc: '', + args: [], + ); + } + + /// `Updating {name}...` + String geoUpdating(Object name) { + return Intl.message( + 'Updating $name...', + name: 'geoUpdating', + desc: '', + args: [name], + ); + } + + /// `{name} skipped` + String geoSkipped(Object name) { + return Intl.message( + '$name skipped', + name: 'geoSkipped', + desc: '', + args: [name], + ); + } + + /// `{name} updated` + String geoUpdated(Object name) { + return Intl.message( + '$name updated', + name: 'geoUpdated', + desc: '', + args: [name], + ); + } + + /// `{count} seconds` + String secondsCount(Object count) { + return Intl.message( + '$count seconds', + name: 'secondsCount', + desc: '', + args: [count], + ); + } + + /// `{count} entries` + String entriesCount(Object count) { + return Intl.message( + '$count entries', + name: 'entriesCount', + desc: '', + args: [count], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index f5d4f1e5..ebe8da09 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,8 +11,8 @@ import 'application.dart'; import 'common/common.dart'; Future main() async { + WidgetsFlutterBinding.ensureInitialized(); try { - WidgetsFlutterBinding.ensureInitialized(); if (system.isDesktop) { await RustLib.init(); } @@ -26,7 +26,7 @@ Future main() async { ), ); } catch (e, s) { - return runApp( + runApp( MaterialApp( home: InitErrorScreen(error: e, stack: s), ), diff --git a/lib/manager/core_manager.dart b/lib/manager/core_manager.dart index 2ccf7e48..0ae0927a 100644 --- a/lib/manager/core_manager.dart +++ b/lib/manager/core_manager.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/core/core.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -72,7 +74,7 @@ class _CoreContainerState extends ConsumerState @override void onLog(Log log) { - // ref.read(logsProvider.notifier).add(log); + ref.read(logsProvider.notifier).add(log); if (log.logLevel == LogLevel.error) { globalState.showNotifier(log.payload); } @@ -109,4 +111,23 @@ class _CoreContainerState extends ConsumerState await coreController.shutdown(false); super.onCrash(message); } + + @override + void onGeoUpdate(String geoType, bool updating, bool skipped, String? error) { + final geoResource = GeoResource.fromJson(geoType.toLowerCase()); + final key = geoResource.updatingKey; + final l10n = currentAppLocalizations; + if (updating) { + globalState.showNotifier(l10n.geoUpdating(geoResource.name)); + } else if (skipped) { + globalState.showNotifier(l10n.geoSkipped(geoResource.name)); + } else { + globalState.showNotifier(l10n.geoUpdated(geoResource.name)); + } + ref.read(isUpdatingProvider(key).notifier).value = updating; + if (!updating && error != null && error.isNotEmpty) { + globalState.showNotifier(error); + } + super.onGeoUpdate(geoType, updating, skipped, error); + } } diff --git a/lib/manager/hotkey_manager.dart b/lib/manager/hotkey_manager.dart index 2b5e7453..f70f0e53 100644 --- a/lib/manager/hotkey_manager.dart +++ b/lib/manager/hotkey_manager.dart @@ -73,7 +73,7 @@ class _HotKeyManagerState extends ConsumerState { await Future.wait(hotkeyActionHandles); } - Shortcuts _buildShortcuts(Widget child) { + Shortcuts _buildCloseShortcuts(Widget child) { return Shortcuts( shortcuts: { utils.controlSingleActivator(LogicalKeyboardKey.keyW): @@ -84,7 +84,7 @@ class _HotKeyManagerState extends ConsumerState { CloseWindowIntent: CallbackAction( onInvoke: (_) => globalState.container .read(systemActionProvider.notifier) - .handleBackOrExit(), + .handleClose(false), ), DoNothingIntent: CallbackAction( onInvoke: (_) => null, @@ -97,6 +97,6 @@ class _HotKeyManagerState extends ConsumerState { @override Widget build(BuildContext context) { - return _buildShortcuts(widget.child); + return _buildCloseShortcuts(widget.child); } } diff --git a/lib/manager/theme_manager.dart b/lib/manager/theme_manager.dart index 11554208..1b6388ef 100644 --- a/lib/manager/theme_manager.dart +++ b/lib/manager/theme_manager.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/theme.dart'; import 'package:fl_clash/providers/action.dart'; -import 'package:fl_clash/providers/app.dart'; import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -21,41 +20,25 @@ class ThemeManager extends ConsumerWidget { if (!system.isAndroid) { return child; } - return AnnotatedRegion( - sized: false, - value: SystemUiMode.edgeToEdge, - child: Consumer( - builder: (context, ref, _) { - final brightness = ref.watch(currentBrightnessProvider); - final iconBrightness = brightness == Brightness.light - ? Brightness.dark - : Brightness.light; - WidgetsBinding.instance.addPostFrameCallback((_) { - ref - .read(systemUiOverlayStyleStateProvider.notifier) - .update( - (state) => state.copyWith( - statusBarColor: Colors.transparent, - statusBarIconBrightness: iconBrightness, - systemNavigationBarIconBrightness: iconBrightness, - systemNavigationBarColor: context.colorScheme.surface, - systemNavigationBarDividerColor: Colors.transparent, - ), - ); - }); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: iconBrightness, - systemNavigationBarIconBrightness: iconBrightness, - systemNavigationBarColor: context.colorScheme.surface, - systemNavigationBarDividerColor: Colors.transparent, - ), - sized: false, - child: child, - ); - }, - ), + return Consumer( + builder: (context, ref, _) { + final brightness = ref.watch(currentBrightnessProvider); + final iconBrightness = brightness == Brightness.light + ? Brightness.dark + : Brightness.light; + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: iconBrightness, + systemNavigationBarIconBrightness: iconBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, + ), + sized: false, + child: child, + ); + }, ); } diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index 17716e29..ca81cd14 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -44,7 +44,7 @@ class _WindowContainerState extends ConsumerState @override void onWindowClose() async { - await ref.read(systemActionProvider.notifier).handleBackOrExit(); + await ref.read(systemActionProvider.notifier).handleClose(); super.onWindowClose(); } @@ -178,7 +178,10 @@ class _WindowHeaderState extends State { windowExtManager.setWindowCornerPreference(round: false); } } - isMaximizedNotifier.value = await windowManager.isMaximized(); + final res = await windowManager.isMaximized(); + if (mounted) { + isMaximizedNotifier.value = res; + } } Future _updatePin() async { @@ -226,7 +229,7 @@ class _WindowHeaderState extends State { onPressed: () { globalState.container .read(systemActionProvider.notifier) - .handleBackOrExit(); + .handleClose(); }, icon: const Icon(Icons.close), ), diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 552fee87..19fe746e 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -3,13 +3,23 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'generated/clash_config.freezed.dart'; + part 'generated/clash_config.g.dart'; const defaultClashConfig = PatchClashConfig(); const defaultTun = Tun(); const defaultDns = Dns(); -const defaultGeoXUrl = GeoXUrl(); +const defaultGeoXUrl = { + GeoResource.MMDB: + 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb', + GeoResource.ASN: + 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb', + GeoResource.GEOIP: + 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat', + GeoResource.GEOSITE: + 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat', +}; const defaultMixedPort = 7890; const defaultKeepAliveInterval = 30; @@ -309,42 +319,6 @@ abstract class Dns with _$Dns { } } -@freezed -abstract class GeoXUrl with _$GeoXUrl { - const factory GeoXUrl({ - @Default( - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb', - ) - String mmdb, - @Default( - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb', - ) - String asn, - @Default( - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat', - ) - String geoip, - @Default( - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat', - ) - String geosite, - }) = _GeoXUrl; - - factory GeoXUrl.fromJson(Map json) => - _$GeoXUrlFromJson(json); - - factory GeoXUrl.safeFormJson(Map? json) { - if (json == null) { - return defaultGeoXUrl; - } - try { - return GeoXUrl.fromJson(json); - } catch (_) { - return defaultGeoXUrl; - } - } -} - @freezed abstract class Rule with _$Rule { const factory Rule({ @@ -542,6 +516,24 @@ abstract class ClashConfig with _$ClashConfig { _$ClashConfigFromJson(json); } +extension GeoResourceUrlMapExt on Map { + Map get raw => + map((key, value) => MapEntry(key.value, value)); +} + +Map _geoXUrlFromJson(Map? json) { + if (json == null) { + return defaultGeoXUrl; + } + return json.map( + (key, value) => MapEntry(GeoResource.fromJson(key), value as String), + ); +} + +Map _geoXUrlToJson(Map value) { + return value.raw; +} + @freezed abstract class PatchClashConfig with _$PatchClashConfig { const factory PatchClashConfig({ @@ -568,8 +560,12 @@ abstract class PatchClashConfig with _$PatchClashConfig { @Default(defaultTun) @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @Default(defaultDns) @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @Default(defaultGeoXUrl) - @JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) - GeoXUrl geoXUrl, + @JsonKey( + name: 'geox-url', + fromJson: _geoXUrlFromJson, + toJson: _geoXUrlToJson, + ) + Map geoXUrl, @Default(GeodataLoader.memconservative) @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @@ -578,6 +574,8 @@ abstract class PatchClashConfig with _$PatchClashConfig { @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @Default({}) Map hosts, + @Default(false) @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, + @Default(24) @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, }) = _PatchClashConfig; factory PatchClashConfig.fromJson(Map json) => diff --git a/lib/models/common.dart b/lib/models/common.dart index b2d89a9b..aa88a7ff 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -9,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'clash_config.dart'; part 'generated/common.freezed.dart'; + part 'generated/common.g.dart'; @freezed @@ -271,7 +272,7 @@ extension TrafficExt on Traffic { } String get trayTitle { - return '${up.shortTraffic.show}/s \n ${down.shortTraffic.show}/s'; + return '${up.shortTraffic.show}/s\n${down.shortTraffic.show}/s'; } num get speed => up + down; diff --git a/lib/models/config.dart b/lib/models/config.dart index d9fc3918..a71ee4e6 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -84,6 +84,7 @@ abstract class AppSettingProps with _$AppSettingProps { @Default(false) bool developerMode, @Default(RestoreStrategy.compatible) RestoreStrategy restoreStrategy, @Default(true) bool showTrayTitle, + @Default('') String customUserAgent, }) = _AppSettingProps; factory AppSettingProps.fromJson(Map json) => diff --git a/lib/models/core.dart b/lib/models/core.dart index 77bc28b6..7cd12576 100644 --- a/lib/models/core.dart +++ b/lib/models/core.dart @@ -31,6 +31,8 @@ abstract class UpdateParams with _$UpdateParams { @JsonKey(name: 'external-controller') required ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') required bool unifiedDelay, + @Default(false) @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, + @Default(24) @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, }) = _UpdateParams; factory UpdateParams.fromJson(Map json) => diff --git a/lib/models/generated/clash_config.freezed.dart b/lib/models/generated/clash_config.freezed.dart index 3a1143d1..b33a1efc 100644 --- a/lib/models/generated/clash_config.freezed.dart +++ b/lib/models/generated/clash_config.freezed.dart @@ -3000,278 +3000,6 @@ $FallbackFilterCopyWith<$Res> get fallbackFilter { } -/// @nodoc -mixin _$GeoXUrl { - - String get mmdb; String get asn; String get geoip; String get geosite; -/// Create a copy of GeoXUrl -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$GeoXUrlCopyWith get copyWith => _$GeoXUrlCopyWithImpl(this as GeoXUrl, _$identity); - - /// Serializes this GeoXUrl to a JSON map. - Map toJson(); - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is GeoXUrl&&(identical(other.mmdb, mmdb) || other.mmdb == mmdb)&&(identical(other.asn, asn) || other.asn == asn)&&(identical(other.geoip, geoip) || other.geoip == geoip)&&(identical(other.geosite, geosite) || other.geosite == geosite)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,mmdb,asn,geoip,geosite); - -@override -String toString() { - return 'GeoXUrl(mmdb: $mmdb, asn: $asn, geoip: $geoip, geosite: $geosite)'; -} - - -} - -/// @nodoc -abstract mixin class $GeoXUrlCopyWith<$Res> { - factory $GeoXUrlCopyWith(GeoXUrl value, $Res Function(GeoXUrl) _then) = _$GeoXUrlCopyWithImpl; -@useResult -$Res call({ - String mmdb, String asn, String geoip, String geosite -}); - - - - -} -/// @nodoc -class _$GeoXUrlCopyWithImpl<$Res> - implements $GeoXUrlCopyWith<$Res> { - _$GeoXUrlCopyWithImpl(this._self, this._then); - - final GeoXUrl _self; - final $Res Function(GeoXUrl) _then; - -/// Create a copy of GeoXUrl -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? mmdb = null,Object? asn = null,Object? geoip = null,Object? geosite = null,}) { - return _then(_self.copyWith( -mmdb: null == mmdb ? _self.mmdb : mmdb // ignore: cast_nullable_to_non_nullable -as String,asn: null == asn ? _self.asn : asn // ignore: cast_nullable_to_non_nullable -as String,geoip: null == geoip ? _self.geoip : geoip // ignore: cast_nullable_to_non_nullable -as String,geosite: null == geosite ? _self.geosite : geosite // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -} - - -/// Adds pattern-matching-related methods to [GeoXUrl]. -extension GeoXUrlPatterns on GeoXUrl { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _GeoXUrl value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _GeoXUrl() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _GeoXUrl value) $default,){ -final _that = this; -switch (_that) { -case _GeoXUrl(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _GeoXUrl value)? $default,){ -final _that = this; -switch (_that) { -case _GeoXUrl() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( String mmdb, String asn, String geoip, String geosite)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _GeoXUrl() when $default != null: -return $default(_that.mmdb,_that.asn,_that.geoip,_that.geosite);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( String mmdb, String asn, String geoip, String geosite) $default,) {final _that = this; -switch (_that) { -case _GeoXUrl(): -return $default(_that.mmdb,_that.asn,_that.geoip,_that.geosite);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String mmdb, String asn, String geoip, String geosite)? $default,) {final _that = this; -switch (_that) { -case _GeoXUrl() when $default != null: -return $default(_that.mmdb,_that.asn,_that.geoip,_that.geosite);case _: - return null; - -} -} - -} - -/// @nodoc -@JsonSerializable() - -class _GeoXUrl implements GeoXUrl { - const _GeoXUrl({this.mmdb = 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb', this.asn = 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb', this.geoip = 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat', this.geosite = 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'}); - factory _GeoXUrl.fromJson(Map json) => _$GeoXUrlFromJson(json); - -@override@JsonKey() final String mmdb; -@override@JsonKey() final String asn; -@override@JsonKey() final String geoip; -@override@JsonKey() final String geosite; - -/// Create a copy of GeoXUrl -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$GeoXUrlCopyWith<_GeoXUrl> get copyWith => __$GeoXUrlCopyWithImpl<_GeoXUrl>(this, _$identity); - -@override -Map toJson() { - return _$GeoXUrlToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _GeoXUrl&&(identical(other.mmdb, mmdb) || other.mmdb == mmdb)&&(identical(other.asn, asn) || other.asn == asn)&&(identical(other.geoip, geoip) || other.geoip == geoip)&&(identical(other.geosite, geosite) || other.geosite == geosite)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,mmdb,asn,geoip,geosite); - -@override -String toString() { - return 'GeoXUrl(mmdb: $mmdb, asn: $asn, geoip: $geoip, geosite: $geosite)'; -} - - -} - -/// @nodoc -abstract mixin class _$GeoXUrlCopyWith<$Res> implements $GeoXUrlCopyWith<$Res> { - factory _$GeoXUrlCopyWith(_GeoXUrl value, $Res Function(_GeoXUrl) _then) = __$GeoXUrlCopyWithImpl; -@override @useResult -$Res call({ - String mmdb, String asn, String geoip, String geosite -}); - - - - -} -/// @nodoc -class __$GeoXUrlCopyWithImpl<$Res> - implements _$GeoXUrlCopyWith<$Res> { - __$GeoXUrlCopyWithImpl(this._self, this._then); - - final _GeoXUrl _self; - final $Res Function(_GeoXUrl) _then; - -/// Create a copy of GeoXUrl -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? mmdb = null,Object? asn = null,Object? geoip = null,Object? geosite = null,}) { - return _then(_GeoXUrl( -mmdb: null == mmdb ? _self.mmdb : mmdb // ignore: cast_nullable_to_non_nullable -as String,asn: null == asn ? _self.asn : asn // ignore: cast_nullable_to_non_nullable -as String,geoip: null == geoip ? _self.geoip : geoip // ignore: cast_nullable_to_non_nullable -as String,geosite: null == geosite ? _self.geosite : geosite // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - - /// @nodoc mixin _$Rule { @@ -3885,7 +3613,7 @@ as Map, /// @nodoc mixin _$PatchClashConfig { -@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'socks-port') int get socksPort;@JsonKey(name: 'port') int get port;@JsonKey(name: 'redir-port') int get redirPort;@JsonKey(name: 'tproxy-port') int get tproxyPort; Mode get mode;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode get findProcessMode;@JsonKey(name: 'keep-alive-interval') int get keepAliveInterval;@JsonKey(name: 'unified-delay') bool get unifiedDelay;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(fromJson: Tun.safeFormJson) Tun get tun;@JsonKey(fromJson: Dns.safeDnsFromJson) Dns get dns;@JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl get geoXUrl;@JsonKey(name: 'geodata-loader') GeodataLoader get geodataLoader;@JsonKey(name: 'global-ua') String? get globalUa;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController; Map get hosts; +@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'socks-port') int get socksPort;@JsonKey(name: 'port') int get port;@JsonKey(name: 'redir-port') int get redirPort;@JsonKey(name: 'tproxy-port') int get tproxyPort; Mode get mode;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode get findProcessMode;@JsonKey(name: 'keep-alive-interval') int get keepAliveInterval;@JsonKey(name: 'unified-delay') bool get unifiedDelay;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(fromJson: Tun.safeFormJson) Tun get tun;@JsonKey(fromJson: Dns.safeDnsFromJson) Dns get dns;@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map get geoXUrl;@JsonKey(name: 'geodata-loader') GeodataLoader get geodataLoader;@JsonKey(name: 'global-ua') String? get globalUa;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController; Map get hosts;@JsonKey(name: 'geo-auto-update') bool get geoAutoUpdate;@JsonKey(name: 'geo-update-interval') int get geoUpdateInterval; /// Create a copy of PatchClashConfig /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -3898,16 +3626,16 @@ $PatchClashConfigCopyWith get copyWith => _$PatchClashConfigCo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PatchClashConfig&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.socksPort, socksPort) || other.socksPort == socksPort)&&(identical(other.port, port) || other.port == port)&&(identical(other.redirPort, redirPort) || other.redirPort == redirPort)&&(identical(other.tproxyPort, tproxyPort) || other.tproxyPort == tproxyPort)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.keepAliveInterval, keepAliveInterval) || other.keepAliveInterval == keepAliveInterval)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.dns, dns) || other.dns == dns)&&(identical(other.geoXUrl, geoXUrl) || other.geoXUrl == geoXUrl)&&(identical(other.geodataLoader, geodataLoader) || other.geodataLoader == geodataLoader)&&(identical(other.globalUa, globalUa) || other.globalUa == globalUa)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&const DeepCollectionEquality().equals(other.hosts, hosts)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PatchClashConfig&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.socksPort, socksPort) || other.socksPort == socksPort)&&(identical(other.port, port) || other.port == port)&&(identical(other.redirPort, redirPort) || other.redirPort == redirPort)&&(identical(other.tproxyPort, tproxyPort) || other.tproxyPort == tproxyPort)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.keepAliveInterval, keepAliveInterval) || other.keepAliveInterval == keepAliveInterval)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.dns, dns) || other.dns == dns)&&const DeepCollectionEquality().equals(other.geoXUrl, geoXUrl)&&(identical(other.geodataLoader, geodataLoader) || other.geodataLoader == geodataLoader)&&(identical(other.globalUa, globalUa) || other.globalUa == globalUa)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&const DeepCollectionEquality().equals(other.hosts, hosts)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,mixedPort,socksPort,port,redirPort,tproxyPort,mode,allowLan,logLevel,ipv6,findProcessMode,keepAliveInterval,unifiedDelay,tcpConcurrent,tun,dns,geoXUrl,geodataLoader,globalUa,externalController,const DeepCollectionEquality().hash(hosts)]); +int get hashCode => Object.hashAll([runtimeType,mixedPort,socksPort,port,redirPort,tproxyPort,mode,allowLan,logLevel,ipv6,findProcessMode,keepAliveInterval,unifiedDelay,tcpConcurrent,tun,dns,const DeepCollectionEquality().hash(geoXUrl),geodataLoader,globalUa,externalController,const DeepCollectionEquality().hash(hosts),geoAutoUpdate,geoUpdateInterval]); @override String toString() { - return 'PatchClashConfig(mixedPort: $mixedPort, socksPort: $socksPort, port: $port, redirPort: $redirPort, tproxyPort: $tproxyPort, mode: $mode, allowLan: $allowLan, logLevel: $logLevel, ipv6: $ipv6, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, geodataLoader: $geodataLoader, globalUa: $globalUa, externalController: $externalController, hosts: $hosts)'; + return 'PatchClashConfig(mixedPort: $mixedPort, socksPort: $socksPort, port: $port, redirPort: $redirPort, tproxyPort: $tproxyPort, mode: $mode, allowLan: $allowLan, logLevel: $logLevel, ipv6: $ipv6, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, geodataLoader: $geodataLoader, globalUa: $globalUa, externalController: $externalController, hosts: $hosts, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval)'; } @@ -3918,11 +3646,11 @@ abstract mixin class $PatchClashConfigCopyWith<$Res> { factory $PatchClashConfigCopyWith(PatchClashConfig value, $Res Function(PatchClashConfig) _then) = _$PatchClashConfigCopyWithImpl; @useResult $Res call({ -@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts +@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval }); -$TunCopyWith<$Res> get tun;$DnsCopyWith<$Res> get dns;$GeoXUrlCopyWith<$Res> get geoXUrl; +$TunCopyWith<$Res> get tun;$DnsCopyWith<$Res> get dns; } /// @nodoc @@ -3935,7 +3663,7 @@ class _$PatchClashConfigCopyWithImpl<$Res> /// Create a copy of PatchClashConfig /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? hosts = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? hosts = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) { return _then(_self.copyWith( mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable as int,socksPort: null == socksPort ? _self.socksPort : socksPort // ignore: cast_nullable_to_non_nullable @@ -3953,11 +3681,13 @@ as bool,tcpConcurrent: null == tcpConcurrent ? _self.tcpConcurrent : tcpConcurre as bool,tun: null == tun ? _self.tun : tun // ignore: cast_nullable_to_non_nullable as Tun,dns: null == dns ? _self.dns : dns // ignore: cast_nullable_to_non_nullable as Dns,geoXUrl: null == geoXUrl ? _self.geoXUrl : geoXUrl // ignore: cast_nullable_to_non_nullable -as GeoXUrl,geodataLoader: null == geodataLoader ? _self.geodataLoader : geodataLoader // ignore: cast_nullable_to_non_nullable +as Map,geodataLoader: null == geodataLoader ? _self.geodataLoader : geodataLoader // ignore: cast_nullable_to_non_nullable as GeodataLoader,globalUa: freezed == globalUa ? _self.globalUa : globalUa // ignore: cast_nullable_to_non_nullable as String?,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable as ExternalControllerStatus,hosts: null == hosts ? _self.hosts : hosts // ignore: cast_nullable_to_non_nullable -as Map, +as Map,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable +as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable +as int, )); } /// Create a copy of PatchClashConfig @@ -3978,15 +3708,6 @@ $DnsCopyWith<$Res> get dns { return $DnsCopyWith<$Res>(_self.dns, (value) { return _then(_self.copyWith(dns: value)); }); -}/// Create a copy of PatchClashConfig -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$GeoXUrlCopyWith<$Res> get geoXUrl { - - return $GeoXUrlCopyWith<$Res>(_self.geoXUrl, (value) { - return _then(_self.copyWith(geoXUrl: value)); - }); } } @@ -4069,10 +3790,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _PatchClashConfig() when $default != null: -return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts);case _: +return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _: return orElse(); } @@ -4090,10 +3811,10 @@ return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that /// } /// ``` -@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval) $default,) {final _that = this; switch (_that) { case _PatchClashConfig(): -return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts);case _: +return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _: throw StateError('Unexpected subclass'); } @@ -4110,10 +3831,10 @@ return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,) {final _that = this; switch (_that) { case _PatchClashConfig() when $default != null: -return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts);case _: +return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _: return null; } @@ -4125,7 +3846,7 @@ return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that @JsonSerializable() class _PatchClashConfig implements PatchClashConfig { - const _PatchClashConfig({@JsonKey(name: 'mixed-port') this.mixedPort = defaultMixedPort, @JsonKey(name: 'socks-port') this.socksPort = 0, @JsonKey(name: 'port') this.port = 0, @JsonKey(name: 'redir-port') this.redirPort = 0, @JsonKey(name: 'tproxy-port') this.tproxyPort = 0, this.mode = Mode.rule, @JsonKey(name: 'allow-lan') this.allowLan = false, @JsonKey(name: 'log-level') this.logLevel = LogLevel.error, this.ipv6 = false, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) this.findProcessMode = FindProcessMode.always, @JsonKey(name: 'keep-alive-interval') this.keepAliveInterval = defaultKeepAliveInterval, @JsonKey(name: 'unified-delay') this.unifiedDelay = true, @JsonKey(name: 'tcp-concurrent') this.tcpConcurrent = true, @JsonKey(fromJson: Tun.safeFormJson) this.tun = defaultTun, @JsonKey(fromJson: Dns.safeDnsFromJson) this.dns = defaultDns, @JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) this.geoXUrl = defaultGeoXUrl, @JsonKey(name: 'geodata-loader') this.geodataLoader = GeodataLoader.memconservative, @JsonKey(name: 'global-ua') this.globalUa, @JsonKey(name: 'external-controller') this.externalController = ExternalControllerStatus.close, final Map hosts = const {}}): _hosts = hosts; + const _PatchClashConfig({@JsonKey(name: 'mixed-port') this.mixedPort = defaultMixedPort, @JsonKey(name: 'socks-port') this.socksPort = 0, @JsonKey(name: 'port') this.port = 0, @JsonKey(name: 'redir-port') this.redirPort = 0, @JsonKey(name: 'tproxy-port') this.tproxyPort = 0, this.mode = Mode.rule, @JsonKey(name: 'allow-lan') this.allowLan = false, @JsonKey(name: 'log-level') this.logLevel = LogLevel.error, this.ipv6 = false, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) this.findProcessMode = FindProcessMode.always, @JsonKey(name: 'keep-alive-interval') this.keepAliveInterval = defaultKeepAliveInterval, @JsonKey(name: 'unified-delay') this.unifiedDelay = true, @JsonKey(name: 'tcp-concurrent') this.tcpConcurrent = true, @JsonKey(fromJson: Tun.safeFormJson) this.tun = defaultTun, @JsonKey(fromJson: Dns.safeDnsFromJson) this.dns = defaultDns, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) final Map geoXUrl = defaultGeoXUrl, @JsonKey(name: 'geodata-loader') this.geodataLoader = GeodataLoader.memconservative, @JsonKey(name: 'global-ua') this.globalUa, @JsonKey(name: 'external-controller') this.externalController = ExternalControllerStatus.close, final Map hosts = const {}, @JsonKey(name: 'geo-auto-update') this.geoAutoUpdate = false, @JsonKey(name: 'geo-update-interval') this.geoUpdateInterval = 24}): _geoXUrl = geoXUrl,_hosts = hosts; factory _PatchClashConfig.fromJson(Map json) => _$PatchClashConfigFromJson(json); @override@JsonKey(name: 'mixed-port') final int mixedPort; @@ -4143,7 +3864,13 @@ class _PatchClashConfig implements PatchClashConfig { @override@JsonKey(name: 'tcp-concurrent') final bool tcpConcurrent; @override@JsonKey(fromJson: Tun.safeFormJson) final Tun tun; @override@JsonKey(fromJson: Dns.safeDnsFromJson) final Dns dns; -@override@JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) final GeoXUrl geoXUrl; + final Map _geoXUrl; +@override@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map get geoXUrl { + if (_geoXUrl is EqualUnmodifiableMapView) return _geoXUrl; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_geoXUrl); +} + @override@JsonKey(name: 'geodata-loader') final GeodataLoader geodataLoader; @override@JsonKey(name: 'global-ua') final String? globalUa; @override@JsonKey(name: 'external-controller') final ExternalControllerStatus externalController; @@ -4154,6 +3881,8 @@ class _PatchClashConfig implements PatchClashConfig { return EqualUnmodifiableMapView(_hosts); } +@override@JsonKey(name: 'geo-auto-update') final bool geoAutoUpdate; +@override@JsonKey(name: 'geo-update-interval') final int geoUpdateInterval; /// Create a copy of PatchClashConfig /// with the given fields replaced by the non-null parameter values. @@ -4168,16 +3897,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PatchClashConfig&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.socksPort, socksPort) || other.socksPort == socksPort)&&(identical(other.port, port) || other.port == port)&&(identical(other.redirPort, redirPort) || other.redirPort == redirPort)&&(identical(other.tproxyPort, tproxyPort) || other.tproxyPort == tproxyPort)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.keepAliveInterval, keepAliveInterval) || other.keepAliveInterval == keepAliveInterval)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.dns, dns) || other.dns == dns)&&(identical(other.geoXUrl, geoXUrl) || other.geoXUrl == geoXUrl)&&(identical(other.geodataLoader, geodataLoader) || other.geodataLoader == geodataLoader)&&(identical(other.globalUa, globalUa) || other.globalUa == globalUa)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&const DeepCollectionEquality().equals(other._hosts, _hosts)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PatchClashConfig&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.socksPort, socksPort) || other.socksPort == socksPort)&&(identical(other.port, port) || other.port == port)&&(identical(other.redirPort, redirPort) || other.redirPort == redirPort)&&(identical(other.tproxyPort, tproxyPort) || other.tproxyPort == tproxyPort)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.keepAliveInterval, keepAliveInterval) || other.keepAliveInterval == keepAliveInterval)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.dns, dns) || other.dns == dns)&&const DeepCollectionEquality().equals(other._geoXUrl, _geoXUrl)&&(identical(other.geodataLoader, geodataLoader) || other.geodataLoader == geodataLoader)&&(identical(other.globalUa, globalUa) || other.globalUa == globalUa)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&const DeepCollectionEquality().equals(other._hosts, _hosts)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,mixedPort,socksPort,port,redirPort,tproxyPort,mode,allowLan,logLevel,ipv6,findProcessMode,keepAliveInterval,unifiedDelay,tcpConcurrent,tun,dns,geoXUrl,geodataLoader,globalUa,externalController,const DeepCollectionEquality().hash(_hosts)]); +int get hashCode => Object.hashAll([runtimeType,mixedPort,socksPort,port,redirPort,tproxyPort,mode,allowLan,logLevel,ipv6,findProcessMode,keepAliveInterval,unifiedDelay,tcpConcurrent,tun,dns,const DeepCollectionEquality().hash(_geoXUrl),geodataLoader,globalUa,externalController,const DeepCollectionEquality().hash(_hosts),geoAutoUpdate,geoUpdateInterval]); @override String toString() { - return 'PatchClashConfig(mixedPort: $mixedPort, socksPort: $socksPort, port: $port, redirPort: $redirPort, tproxyPort: $tproxyPort, mode: $mode, allowLan: $allowLan, logLevel: $logLevel, ipv6: $ipv6, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, geodataLoader: $geodataLoader, globalUa: $globalUa, externalController: $externalController, hosts: $hosts)'; + return 'PatchClashConfig(mixedPort: $mixedPort, socksPort: $socksPort, port: $port, redirPort: $redirPort, tproxyPort: $tproxyPort, mode: $mode, allowLan: $allowLan, logLevel: $logLevel, ipv6: $ipv6, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, geodataLoader: $geodataLoader, globalUa: $globalUa, externalController: $externalController, hosts: $hosts, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval)'; } @@ -4188,11 +3917,11 @@ abstract mixin class _$PatchClashConfigCopyWith<$Res> implements $PatchClashConf factory _$PatchClashConfigCopyWith(_PatchClashConfig value, $Res Function(_PatchClashConfig) _then) = __$PatchClashConfigCopyWithImpl; @override @useResult $Res call({ -@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: 'geox-url', fromJson: GeoXUrl.safeFormJson) GeoXUrl geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts +@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map hosts,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval }); -@override $TunCopyWith<$Res> get tun;@override $DnsCopyWith<$Res> get dns;@override $GeoXUrlCopyWith<$Res> get geoXUrl; +@override $TunCopyWith<$Res> get tun;@override $DnsCopyWith<$Res> get dns; } /// @nodoc @@ -4205,7 +3934,7 @@ class __$PatchClashConfigCopyWithImpl<$Res> /// Create a copy of PatchClashConfig /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? hosts = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? hosts = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) { return _then(_PatchClashConfig( mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable as int,socksPort: null == socksPort ? _self.socksPort : socksPort // ignore: cast_nullable_to_non_nullable @@ -4222,12 +3951,14 @@ as int,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // as bool,tcpConcurrent: null == tcpConcurrent ? _self.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable as bool,tun: null == tun ? _self.tun : tun // ignore: cast_nullable_to_non_nullable as Tun,dns: null == dns ? _self.dns : dns // ignore: cast_nullable_to_non_nullable -as Dns,geoXUrl: null == geoXUrl ? _self.geoXUrl : geoXUrl // ignore: cast_nullable_to_non_nullable -as GeoXUrl,geodataLoader: null == geodataLoader ? _self.geodataLoader : geodataLoader // ignore: cast_nullable_to_non_nullable +as Dns,geoXUrl: null == geoXUrl ? _self._geoXUrl : geoXUrl // ignore: cast_nullable_to_non_nullable +as Map,geodataLoader: null == geodataLoader ? _self.geodataLoader : geodataLoader // ignore: cast_nullable_to_non_nullable as GeodataLoader,globalUa: freezed == globalUa ? _self.globalUa : globalUa // ignore: cast_nullable_to_non_nullable as String?,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable as ExternalControllerStatus,hosts: null == hosts ? _self._hosts : hosts // ignore: cast_nullable_to_non_nullable -as Map, +as Map,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable +as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable +as int, )); } @@ -4249,15 +3980,6 @@ $DnsCopyWith<$Res> get dns { return $DnsCopyWith<$Res>(_self.dns, (value) { return _then(_self.copyWith(dns: value)); }); -}/// Create a copy of PatchClashConfig -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$GeoXUrlCopyWith<$Res> get geoXUrl { - - return $GeoXUrlCopyWith<$Res>(_self.geoXUrl, (value) { - return _then(_self.copyWith(geoXUrl: value)); - }); } } diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index ed956a31..b70d7a5a 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -295,28 +295,6 @@ const _$DnsModeEnumMap = { DnsMode.hosts: 'hosts', }; -_GeoXUrl _$GeoXUrlFromJson(Map json) => _GeoXUrl( - mmdb: - json['mmdb'] as String? ?? - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb', - asn: - json['asn'] as String? ?? - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb', - geoip: - json['geoip'] as String? ?? - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat', - geosite: - json['geosite'] as String? ?? - 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat', -); - -Map _$GeoXUrlToJson(_GeoXUrl instance) => { - 'mmdb': instance.mmdb, - 'asn': instance.asn, - 'geoip': instance.geoip, - 'geosite': instance.geosite, -}; - _Rule _$RuleFromJson(Map json) => _Rule( id: (json['id'] as num?)?.toInt() ?? -1, ruleAction: @@ -451,7 +429,7 @@ _PatchClashConfig _$PatchClashConfigFromJson(Map json) => : Dns.safeDnsFromJson(json['dns'] as Map), geoXUrl: json['geox-url'] == null ? defaultGeoXUrl - : GeoXUrl.safeFormJson(json['geox-url'] as Map?), + : _geoXUrlFromJson(json['geox-url'] as Map?), geodataLoader: $enumDecodeNullable(_$GeodataLoaderEnumMap, json['geodata-loader']) ?? GeodataLoader.memconservative, @@ -467,6 +445,8 @@ _PatchClashConfig _$PatchClashConfigFromJson(Map json) => (k, e) => MapEntry(k, e as String), ) ?? const {}, + geoAutoUpdate: json['geo-auto-update'] as bool? ?? false, + geoUpdateInterval: (json['geo-update-interval'] as num?)?.toInt() ?? 24, ); Map _$PatchClashConfigToJson(_PatchClashConfig instance) => @@ -486,12 +466,14 @@ Map _$PatchClashConfigToJson(_PatchClashConfig instance) => 'tcp-concurrent': instance.tcpConcurrent, 'tun': instance.tun, 'dns': instance.dns, - 'geox-url': instance.geoXUrl, + 'geox-url': _geoXUrlToJson(instance.geoXUrl), 'geodata-loader': _$GeodataLoaderEnumMap[instance.geodataLoader]!, 'global-ua': instance.globalUa, 'external-controller': _$ExternalControllerStatusEnumMap[instance.externalController]!, 'hosts': instance.hosts, + 'geo-auto-update': instance.geoAutoUpdate, + 'geo-update-interval': instance.geoUpdateInterval, }; const _$ModeEnumMap = { diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 273c294e..e776a211 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$AppSettingProps { - String? get locale;@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List get dashboardWidgets; bool get onlyStatisticsProxy; bool get autoLaunch; bool get silentLaunch; bool get autoRun; bool get openLogs; bool get closeConnections; String get testUrl; bool get isAnimateToPage; bool get autoCheckUpdate; bool get showLabel; bool get disclaimerAccepted; bool get crashlyticsTip; bool get crashlytics; bool get minimizeOnExit; bool get hidden; bool get developerMode; RestoreStrategy get restoreStrategy; bool get showTrayTitle; + String? get locale;@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List get dashboardWidgets; bool get onlyStatisticsProxy; bool get autoLaunch; bool get silentLaunch; bool get autoRun; bool get openLogs; bool get closeConnections; String get testUrl; bool get isAnimateToPage; bool get autoCheckUpdate; bool get showLabel; bool get disclaimerAccepted; bool get crashlyticsTip; bool get crashlytics; bool get minimizeOnExit; bool get hidden; bool get developerMode; RestoreStrategy get restoreStrategy; bool get showTrayTitle; String get customUserAgent; /// Create a copy of AppSettingProps /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $AppSettingPropsCopyWith get copyWith => _$AppSettingPropsCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other.dashboardWidgets, dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.crashlyticsTip, crashlyticsTip) || other.crashlyticsTip == crashlyticsTip)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.restoreStrategy, restoreStrategy) || other.restoreStrategy == restoreStrategy)&&(identical(other.showTrayTitle, showTrayTitle) || other.showTrayTitle == showTrayTitle)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other.dashboardWidgets, dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.crashlyticsTip, crashlyticsTip) || other.crashlyticsTip == crashlyticsTip)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.restoreStrategy, restoreStrategy) || other.restoreStrategy == restoreStrategy)&&(identical(other.showTrayTitle, showTrayTitle) || other.showTrayTitle == showTrayTitle)&&(identical(other.customUserAgent, customUserAgent) || other.customUserAgent == customUserAgent)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,locale,const DeepCollectionEquality().hash(dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,crashlyticsTip,crashlytics,minimizeOnExit,hidden,developerMode,restoreStrategy,showTrayTitle]); +int get hashCode => Object.hashAll([runtimeType,locale,const DeepCollectionEquality().hash(dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,crashlyticsTip,crashlytics,minimizeOnExit,hidden,developerMode,restoreStrategy,showTrayTitle,customUserAgent]); @override String toString() { - return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, crashlyticsTip: $crashlyticsTip, crashlytics: $crashlytics, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, restoreStrategy: $restoreStrategy, showTrayTitle: $showTrayTitle)'; + return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, crashlyticsTip: $crashlyticsTip, crashlytics: $crashlytics, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, restoreStrategy: $restoreStrategy, showTrayTitle: $showTrayTitle, customUserAgent: $customUserAgent)'; } @@ -48,7 +48,7 @@ abstract mixin class $AppSettingPropsCopyWith<$Res> { factory $AppSettingPropsCopyWith(AppSettingProps value, $Res Function(AppSettingProps) _then) = _$AppSettingPropsCopyWithImpl; @useResult $Res call({ - String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle + String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle, String customUserAgent }); @@ -65,7 +65,7 @@ class _$AppSettingPropsCopyWithImpl<$Res> /// Create a copy of AppSettingProps /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? crashlyticsTip = null,Object? crashlytics = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? restoreStrategy = null,Object? showTrayTitle = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? crashlyticsTip = null,Object? crashlytics = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? restoreStrategy = null,Object? showTrayTitle = null,Object? customUserAgent = null,}) { return _then(_self.copyWith( locale: freezed == locale ? _self.locale : locale // ignore: cast_nullable_to_non_nullable as String?,dashboardWidgets: null == dashboardWidgets ? _self.dashboardWidgets : dashboardWidgets // ignore: cast_nullable_to_non_nullable @@ -87,7 +87,8 @@ as bool,hidden: null == hidden ? _self.hidden : hidden // ignore: cast_nullable_ as bool,developerMode: null == developerMode ? _self.developerMode : developerMode // ignore: cast_nullable_to_non_nullable as bool,restoreStrategy: null == restoreStrategy ? _self.restoreStrategy : restoreStrategy // ignore: cast_nullable_to_non_nullable as RestoreStrategy,showTrayTitle: null == showTrayTitle ? _self.showTrayTitle : showTrayTitle // ignore: cast_nullable_to_non_nullable -as bool, +as bool,customUserAgent: null == customUserAgent ? _self.customUserAgent : customUserAgent // ignore: cast_nullable_to_non_nullable +as String, )); } @@ -172,10 +173,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle, String customUserAgent)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _AppSettingProps() when $default != null: -return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.restoreStrategy,_that.showTrayTitle);case _: +return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.restoreStrategy,_that.showTrayTitle,_that.customUserAgent);case _: return orElse(); } @@ -193,10 +194,10 @@ return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_t /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle, String customUserAgent) $default,) {final _that = this; switch (_that) { case _AppSettingProps(): -return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.restoreStrategy,_that.showTrayTitle);case _: +return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.restoreStrategy,_that.showTrayTitle,_that.customUserAgent);case _: throw StateError('Unexpected subclass'); } @@ -213,10 +214,10 @@ return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_t /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle, String customUserAgent)? $default,) {final _that = this; switch (_that) { case _AppSettingProps() when $default != null: -return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.restoreStrategy,_that.showTrayTitle);case _: +return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.restoreStrategy,_that.showTrayTitle,_that.customUserAgent);case _: return null; } @@ -228,7 +229,7 @@ return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_t @JsonSerializable() class _AppSettingProps implements AppSettingProps { - const _AppSettingProps({this.locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) final List dashboardWidgets = defaultDashboardWidgets, this.onlyStatisticsProxy = false, this.autoLaunch = false, this.silentLaunch = false, this.autoRun = false, this.openLogs = false, this.closeConnections = true, this.testUrl = defaultTestUrl, this.isAnimateToPage = true, this.autoCheckUpdate = true, this.showLabel = false, this.disclaimerAccepted = false, this.crashlyticsTip = false, this.crashlytics = false, this.minimizeOnExit = true, this.hidden = false, this.developerMode = false, this.restoreStrategy = RestoreStrategy.compatible, this.showTrayTitle = true}): _dashboardWidgets = dashboardWidgets; + const _AppSettingProps({this.locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) final List dashboardWidgets = defaultDashboardWidgets, this.onlyStatisticsProxy = false, this.autoLaunch = false, this.silentLaunch = false, this.autoRun = false, this.openLogs = false, this.closeConnections = true, this.testUrl = defaultTestUrl, this.isAnimateToPage = true, this.autoCheckUpdate = true, this.showLabel = false, this.disclaimerAccepted = false, this.crashlyticsTip = false, this.crashlytics = false, this.minimizeOnExit = true, this.hidden = false, this.developerMode = false, this.restoreStrategy = RestoreStrategy.compatible, this.showTrayTitle = true, this.customUserAgent = ''}): _dashboardWidgets = dashboardWidgets; factory _AppSettingProps.fromJson(Map json) => _$AppSettingPropsFromJson(json); @override final String? locale; @@ -257,6 +258,7 @@ class _AppSettingProps implements AppSettingProps { @override@JsonKey() final bool developerMode; @override@JsonKey() final RestoreStrategy restoreStrategy; @override@JsonKey() final bool showTrayTitle; +@override@JsonKey() final String customUserAgent; /// Create a copy of AppSettingProps /// with the given fields replaced by the non-null parameter values. @@ -271,16 +273,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other._dashboardWidgets, _dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.crashlyticsTip, crashlyticsTip) || other.crashlyticsTip == crashlyticsTip)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.restoreStrategy, restoreStrategy) || other.restoreStrategy == restoreStrategy)&&(identical(other.showTrayTitle, showTrayTitle) || other.showTrayTitle == showTrayTitle)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other._dashboardWidgets, _dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.crashlyticsTip, crashlyticsTip) || other.crashlyticsTip == crashlyticsTip)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.restoreStrategy, restoreStrategy) || other.restoreStrategy == restoreStrategy)&&(identical(other.showTrayTitle, showTrayTitle) || other.showTrayTitle == showTrayTitle)&&(identical(other.customUserAgent, customUserAgent) || other.customUserAgent == customUserAgent)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,locale,const DeepCollectionEquality().hash(_dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,crashlyticsTip,crashlytics,minimizeOnExit,hidden,developerMode,restoreStrategy,showTrayTitle]); +int get hashCode => Object.hashAll([runtimeType,locale,const DeepCollectionEquality().hash(_dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,crashlyticsTip,crashlytics,minimizeOnExit,hidden,developerMode,restoreStrategy,showTrayTitle,customUserAgent]); @override String toString() { - return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, crashlyticsTip: $crashlyticsTip, crashlytics: $crashlytics, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, restoreStrategy: $restoreStrategy, showTrayTitle: $showTrayTitle)'; + return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, crashlyticsTip: $crashlyticsTip, crashlytics: $crashlytics, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, restoreStrategy: $restoreStrategy, showTrayTitle: $showTrayTitle, customUserAgent: $customUserAgent)'; } @@ -291,7 +293,7 @@ abstract mixin class _$AppSettingPropsCopyWith<$Res> implements $AppSettingProps factory _$AppSettingPropsCopyWith(_AppSettingProps value, $Res Function(_AppSettingProps) _then) = __$AppSettingPropsCopyWithImpl; @override @useResult $Res call({ - String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle + String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RestoreStrategy restoreStrategy, bool showTrayTitle, String customUserAgent }); @@ -308,7 +310,7 @@ class __$AppSettingPropsCopyWithImpl<$Res> /// Create a copy of AppSettingProps /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? crashlyticsTip = null,Object? crashlytics = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? restoreStrategy = null,Object? showTrayTitle = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? crashlyticsTip = null,Object? crashlytics = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? restoreStrategy = null,Object? showTrayTitle = null,Object? customUserAgent = null,}) { return _then(_AppSettingProps( locale: freezed == locale ? _self.locale : locale // ignore: cast_nullable_to_non_nullable as String?,dashboardWidgets: null == dashboardWidgets ? _self._dashboardWidgets : dashboardWidgets // ignore: cast_nullable_to_non_nullable @@ -330,7 +332,8 @@ as bool,hidden: null == hidden ? _self.hidden : hidden // ignore: cast_nullable_ as bool,developerMode: null == developerMode ? _self.developerMode : developerMode // ignore: cast_nullable_to_non_nullable as bool,restoreStrategy: null == restoreStrategy ? _self.restoreStrategy : restoreStrategy // ignore: cast_nullable_to_non_nullable as RestoreStrategy,showTrayTitle: null == showTrayTitle ? _self.showTrayTitle : showTrayTitle // ignore: cast_nullable_to_non_nullable -as bool, +as bool,customUserAgent: null == customUserAgent ? _self.customUserAgent : customUserAgent // ignore: cast_nullable_to_non_nullable +as String, )); } diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 3f54d5ae..76d3d604 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -35,6 +35,7 @@ _AppSettingProps _$AppSettingPropsFromJson(Map json) => ) ?? RestoreStrategy.compatible, showTrayTitle: json['showTrayTitle'] as bool? ?? true, + customUserAgent: json['customUserAgent'] as String? ?? '', ); Map _$AppSettingPropsToJson(_AppSettingProps instance) => @@ -61,6 +62,7 @@ Map _$AppSettingPropsToJson(_AppSettingProps instance) => 'developerMode': instance.developerMode, 'restoreStrategy': _$RestoreStrategyEnumMap[instance.restoreStrategy]!, 'showTrayTitle': instance.showTrayTitle, + 'customUserAgent': instance.customUserAgent, }; const _$RestoreStrategyEnumMap = { diff --git a/lib/models/generated/core.freezed.dart b/lib/models/generated/core.freezed.dart index b29669e2..bc15672f 100644 --- a/lib/models/generated/core.freezed.dart +++ b/lib/models/generated/core.freezed.dart @@ -287,7 +287,7 @@ as String, /// @nodoc mixin _$UpdateParams { - Tun get tun;@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'find-process-mode') FindProcessMode get findProcessMode; Mode get mode;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController;@JsonKey(name: 'unified-delay') bool get unifiedDelay; + Tun get tun;@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'find-process-mode') FindProcessMode get findProcessMode; Mode get mode;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController;@JsonKey(name: 'unified-delay') bool get unifiedDelay;@JsonKey(name: 'geo-auto-update') bool get geoAutoUpdate;@JsonKey(name: 'geo-update-interval') int get geoUpdateInterval; /// Create a copy of UpdateParams /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -300,16 +300,16 @@ $UpdateParamsCopyWith get copyWith => _$UpdateParamsCopyWithImpl Object.hash(runtimeType,tun,mixedPort,allowLan,findProcessMode,mode,logLevel,ipv6,tcpConcurrent,externalController,unifiedDelay); +int get hashCode => Object.hash(runtimeType,tun,mixedPort,allowLan,findProcessMode,mode,logLevel,ipv6,tcpConcurrent,externalController,unifiedDelay,geoAutoUpdate,geoUpdateInterval); @override String toString() { - return 'UpdateParams(tun: $tun, mixedPort: $mixedPort, allowLan: $allowLan, findProcessMode: $findProcessMode, mode: $mode, logLevel: $logLevel, ipv6: $ipv6, tcpConcurrent: $tcpConcurrent, externalController: $externalController, unifiedDelay: $unifiedDelay)'; + return 'UpdateParams(tun: $tun, mixedPort: $mixedPort, allowLan: $allowLan, findProcessMode: $findProcessMode, mode: $mode, logLevel: $logLevel, ipv6: $ipv6, tcpConcurrent: $tcpConcurrent, externalController: $externalController, unifiedDelay: $unifiedDelay, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval)'; } @@ -320,7 +320,7 @@ abstract mixin class $UpdateParamsCopyWith<$Res> { factory $UpdateParamsCopyWith(UpdateParams value, $Res Function(UpdateParams) _then) = _$UpdateParamsCopyWithImpl; @useResult $Res call({ - Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController,@JsonKey(name: 'unified-delay') bool unifiedDelay + Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval }); @@ -337,7 +337,7 @@ class _$UpdateParamsCopyWithImpl<$Res> /// Create a copy of UpdateParams /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? unifiedDelay = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? unifiedDelay = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) { return _then(_self.copyWith( tun: null == tun ? _self.tun : tun // ignore: cast_nullable_to_non_nullable as Tun,mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable @@ -349,7 +349,9 @@ as LogLevel,ipv6: null == ipv6 ? _self.ipv6 : ipv6 // ignore: cast_nullable_to_n as bool,tcpConcurrent: null == tcpConcurrent ? _self.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable as bool,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable as ExternalControllerStatus,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // ignore: cast_nullable_to_non_nullable -as bool, +as bool,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable +as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable +as int, )); } /// Create a copy of UpdateParams @@ -443,10 +445,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _UpdateParams() when $default != null: -return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay);case _: +return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay,_that.geoAutoUpdate,_that.geoUpdateInterval);case _: return orElse(); } @@ -464,10 +466,10 @@ return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_ /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval) $default,) {final _that = this; switch (_that) { case _UpdateParams(): -return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay);case _: +return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay,_that.geoAutoUpdate,_that.geoUpdateInterval);case _: throw StateError('Unexpected subclass'); } @@ -484,10 +486,10 @@ return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_ /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,) {final _that = this; switch (_that) { case _UpdateParams() when $default != null: -return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay);case _: +return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay,_that.geoAutoUpdate,_that.geoUpdateInterval);case _: return null; } @@ -499,7 +501,7 @@ return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_ @JsonSerializable() class _UpdateParams implements UpdateParams { - const _UpdateParams({required this.tun, @JsonKey(name: 'mixed-port') required this.mixedPort, @JsonKey(name: 'allow-lan') required this.allowLan, @JsonKey(name: 'find-process-mode') required this.findProcessMode, required this.mode, @JsonKey(name: 'log-level') required this.logLevel, required this.ipv6, @JsonKey(name: 'tcp-concurrent') required this.tcpConcurrent, @JsonKey(name: 'external-controller') required this.externalController, @JsonKey(name: 'unified-delay') required this.unifiedDelay}); + const _UpdateParams({required this.tun, @JsonKey(name: 'mixed-port') required this.mixedPort, @JsonKey(name: 'allow-lan') required this.allowLan, @JsonKey(name: 'find-process-mode') required this.findProcessMode, required this.mode, @JsonKey(name: 'log-level') required this.logLevel, required this.ipv6, @JsonKey(name: 'tcp-concurrent') required this.tcpConcurrent, @JsonKey(name: 'external-controller') required this.externalController, @JsonKey(name: 'unified-delay') required this.unifiedDelay, @JsonKey(name: 'geo-auto-update') this.geoAutoUpdate = false, @JsonKey(name: 'geo-update-interval') this.geoUpdateInterval = 24}); factory _UpdateParams.fromJson(Map json) => _$UpdateParamsFromJson(json); @override final Tun tun; @@ -512,6 +514,8 @@ class _UpdateParams implements UpdateParams { @override@JsonKey(name: 'tcp-concurrent') final bool tcpConcurrent; @override@JsonKey(name: 'external-controller') final ExternalControllerStatus externalController; @override@JsonKey(name: 'unified-delay') final bool unifiedDelay; +@override@JsonKey(name: 'geo-auto-update') final bool geoAutoUpdate; +@override@JsonKey(name: 'geo-update-interval') final int geoUpdateInterval; /// Create a copy of UpdateParams /// with the given fields replaced by the non-null parameter values. @@ -526,16 +530,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateParams&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateParams&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,tun,mixedPort,allowLan,findProcessMode,mode,logLevel,ipv6,tcpConcurrent,externalController,unifiedDelay); +int get hashCode => Object.hash(runtimeType,tun,mixedPort,allowLan,findProcessMode,mode,logLevel,ipv6,tcpConcurrent,externalController,unifiedDelay,geoAutoUpdate,geoUpdateInterval); @override String toString() { - return 'UpdateParams(tun: $tun, mixedPort: $mixedPort, allowLan: $allowLan, findProcessMode: $findProcessMode, mode: $mode, logLevel: $logLevel, ipv6: $ipv6, tcpConcurrent: $tcpConcurrent, externalController: $externalController, unifiedDelay: $unifiedDelay)'; + return 'UpdateParams(tun: $tun, mixedPort: $mixedPort, allowLan: $allowLan, findProcessMode: $findProcessMode, mode: $mode, logLevel: $logLevel, ipv6: $ipv6, tcpConcurrent: $tcpConcurrent, externalController: $externalController, unifiedDelay: $unifiedDelay, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval)'; } @@ -546,7 +550,7 @@ abstract mixin class _$UpdateParamsCopyWith<$Res> implements $UpdateParamsCopyWi factory _$UpdateParamsCopyWith(_UpdateParams value, $Res Function(_UpdateParams) _then) = __$UpdateParamsCopyWithImpl; @override @useResult $Res call({ - Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController,@JsonKey(name: 'unified-delay') bool unifiedDelay + Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval }); @@ -563,7 +567,7 @@ class __$UpdateParamsCopyWithImpl<$Res> /// Create a copy of UpdateParams /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? unifiedDelay = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? unifiedDelay = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) { return _then(_UpdateParams( tun: null == tun ? _self.tun : tun // ignore: cast_nullable_to_non_nullable as Tun,mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable @@ -575,7 +579,9 @@ as LogLevel,ipv6: null == ipv6 ? _self.ipv6 : ipv6 // ignore: cast_nullable_to_n as bool,tcpConcurrent: null == tcpConcurrent ? _self.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable as bool,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable as ExternalControllerStatus,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // ignore: cast_nullable_to_non_nullable -as bool, +as bool,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable +as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable +as int, )); } diff --git a/lib/models/generated/core.g.dart b/lib/models/generated/core.g.dart index 1a022350..7e785dfa 100644 --- a/lib/models/generated/core.g.dart +++ b/lib/models/generated/core.g.dart @@ -35,6 +35,8 @@ _UpdateParams _$UpdateParamsFromJson(Map json) => json['external-controller'], ), unifiedDelay: json['unified-delay'] as bool, + geoAutoUpdate: json['geo-auto-update'] as bool? ?? false, + geoUpdateInterval: (json['geo-update-interval'] as num?)?.toInt() ?? 24, ); Map _$UpdateParamsToJson(_UpdateParams instance) => @@ -50,6 +52,8 @@ Map _$UpdateParamsToJson(_UpdateParams instance) => 'external-controller': _$ExternalControllerStatusEnumMap[instance.externalController]!, 'unified-delay': instance.unifiedDelay, + 'geo-auto-update': instance.geoAutoUpdate, + 'geo-update-interval': instance.geoUpdateInterval, }; const _$FindProcessModeEnumMap = { @@ -164,6 +168,7 @@ const _$CoreEventTypeEnumMap = { CoreEventType.request: 'request', CoreEventType.loaded: 'loaded', CoreEventType.crash: 'crash', + CoreEventType.geoUpdate: 'geoUpdate', }; _InvokeMessage _$InvokeMessageFromJson(Map json) => diff --git a/lib/pages/editor.dart b/lib/pages/editor.dart index 0ac93970..9d25aeeb 100644 --- a/lib/pages/editor.dart +++ b/lib/pages/editor.dart @@ -139,7 +139,7 @@ class _EditorPageState extends ConsumerState { if (file == null) { return; } - final res = utf8.decode(file.bytes?.toList() ?? []); + final res = utf8.decode(await file.readBytes()); _controller.text = res; } @@ -150,6 +150,7 @@ class _EditorPageState extends ConsumerState { title: appLocalizations.import, value: '', labelText: appLocalizations.url, + inputFormatters: TextInputLimits.limit(TextInputLimits.url), validator: (value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.value); @@ -442,7 +443,11 @@ class FindPanel extends StatelessWidget implements PreferredSizeWidget { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [bar, const SizedBox(height: 12), _buildFindInput(context, value)], + children: [ + bar, + const SizedBox(height: 12), + _buildFindInput(context, value), + ], ); } return bar; @@ -499,6 +504,7 @@ class FindPanel extends StatelessWidget implements PreferredSizeWidget { child: TextField( maxLines: 1, focusNode: focusNode, + inputFormatters: TextInputLimits.limit(TextInputLimits.search), style: context.textTheme.bodyMedium, decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/lib/pages/home.dart b/lib/pages/home.dart index a891477e..c5742a87 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -6,7 +6,6 @@ import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -30,9 +29,6 @@ class HomePage extends StatelessWidget { child: Consumer( builder: (context, ref, child) { final state = ref.watch(navigationStateProvider); - final systemUiOverlayStyle = ref.read( - systemUiOverlayStyleStateProvider, - ); final isMobile = state.viewMode == ViewMode.mobile; final navigationItems = state.navigationItems; final currentIndex = state.currentIndex; @@ -54,34 +50,28 @@ class HomePage extends StatelessWidget { ), ); if (isMobile) { - return AnnotatedRegion( - value: systemUiOverlayStyle.copyWith( - systemNavigationBarColor: - context.colorScheme.surfaceContainer, - ), - child: Column( - children: [ - Flexible( - flex: 1, - child: MediaQuery.removePadding( - removeTop: false, - removeBottom: true, - removeLeft: true, - removeRight: true, - context: context, - child: child!, - ), - ), - MediaQuery.removePadding( - removeTop: true, - removeBottom: false, + return Column( + children: [ + Flexible( + flex: 1, + child: MediaQuery.removePadding( + removeTop: false, + removeBottom: true, removeLeft: true, removeRight: true, context: context, - child: bottomNavigationBar, + child: child!, ), - ], - ), + ), + MediaQuery.removePadding( + removeTop: true, + removeBottom: false, + removeLeft: true, + removeRight: true, + context: context, + child: bottomNavigationBar, + ), + ], ); } else { return child!; @@ -287,7 +277,7 @@ class HomeBackScopeContainer extends ConsumerWidget { } else { await globalState.container .read(systemActionProvider.notifier) - .handleBackOrExit(); + .handleClose(); } return false; }, diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart index 20072deb..ef362525 100644 --- a/lib/pages/scan.dart +++ b/lib/pages/scan.dart @@ -61,7 +61,10 @@ class _ScanPageState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - final double sideLength = min(400, MediaQuery.of(context).size.width * 0.67); + final double sideLength = min( + 400, + MediaQuery.of(context).size.width * 0.67, + ); final scanWindow = Rect.fromCenter( center: MediaQuery.sizeOf(context).center(Offset.zero), width: sideLength, diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index b70ed091..612b7cc4 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -53,9 +53,7 @@ class App { } Future requestNotificationsPermission() async { - return methodChannel.invokeMethod( - 'requestNotificationsPermission', - ); + return methodChannel.invokeMethod('requestNotificationsPermission'); } Future openFile(String path) async { @@ -74,9 +72,7 @@ class App { } Future tip(String? message) async { - return methodChannel.invokeMethod('tip', { - 'message': '$message', - }); + return methodChannel.invokeMethod('tip', {'message': '$message'}); } Future initShortcuts() async { diff --git a/lib/providers/action.dart b/lib/providers/action.dart index 7baf8214..c444723c 100644 --- a/lib/providers/action.dart +++ b/lib/providers/action.dart @@ -224,6 +224,7 @@ class SetupAction extends _$SetupAction { final message = await coreController.updateConfig( updateParams.copyWith.tun(enable: realTunEnable), ); + ref.read(checkIpNumProvider.notifier).add(); if (message.isNotEmpty) throw message; }); }); @@ -254,7 +255,6 @@ class SetupAction extends _$SetupAction { .read(proxiesActionProvider.notifier) .updateCurrentGroupName(GroupName.GLOBAL.name); } - ref.read(checkIpNumProvider.notifier).add(); } void autoApplyProfile() { @@ -593,9 +593,11 @@ class SystemAction extends _$SystemAction { } } - Future handleBackOrExit() async { - if (ref.read(backBlockProvider)) return; - if (ref.read(appSettingProvider).minimizeOnExit) { + Future handleClose([bool exit = true]) async { + if (!system.isDesktop) { + if (ref.read(backBlockProvider)) return; + } + if (ref.read(appSettingProvider).minimizeOnExit || !exit) { if (system.isDesktop) { await preferences.saveConfig(ref.read(configProvider)); } @@ -910,14 +912,14 @@ class ProfilesAction extends _$ProfilesAction { Future addProfileFormFile() async { final platformFile = await globalState.safeRun(picker.pickerFile); - final bytes = platformFile?.bytes; - if (bytes == null) return; + if (platformFile == null) return; + final bytes = await platformFile.readBytes(); globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); ref.read(currentPageLabelProvider.notifier).toProfiles(); final profile = await globalState.loadingRun( tag: LoadingTag.profiles, () async { - return Profile.normal(label: platformFile?.name).saveFile(bytes); + return Profile.normal(label: platformFile.name).saveFile(bytes); }, title: currentAppLocalizations.addProfile, ); @@ -973,3 +975,22 @@ class ProfilesAction extends _$ProfilesAction { await coreController.deleteFile(providersDirPath); } } + +@Riverpod(keepAlive: true) +class GeoResourceAction extends _$GeoResourceAction { + @override + void build() {} + + Future updateGeoResource(GeoResource geoResource) async { + await coreController.updateGeoData(geoResource.name); + } + + void updateGeoResourceUrl(GeoResource geoResource, String newUrl) { + if (!newUrl.isUrl) { + throw 'Invalid url'; + } + ref.read(patchClashConfigProvider.notifier).update((state) { + return state.copyWith(geoXUrl: {...state.geoXUrl, geoResource: newUrl}); + }); + } +} diff --git a/lib/providers/database.dart b/lib/providers/database.dart index 4045613f..1d0d5124 100644 --- a/lib/providers/database.dart +++ b/lib/providers/database.dart @@ -243,14 +243,11 @@ class GlobalRules extends _$GlobalRules with AsyncNotifierMixin { void order(int oldIndex, int newIndex) { final previous = List.from(value); - int insertIndex = newIndex; - if (oldIndex < newIndex) insertIndex -= 1; - final nextItems = List.from(previous); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(insertIndex, item); + final item = previous[oldIndex]; + final nextItems = previous.copyAndReorder(oldIndex, newIndex); value = nextItems; - final preOrder = nextItems.safeGet(insertIndex - 1)?.order; - final nextOrder = nextItems.safeGet(insertIndex + 1)?.order; + final preOrder = nextItems.safeGet(newIndex - 1)?.order; + final nextOrder = nextItems.safeGet(newIndex + 1)?.order; final newOrder = indexing.generateKeyBetween(preOrder, nextOrder)!; unawaited( withRollback( @@ -308,14 +305,11 @@ class ProfileAddedRules extends _$ProfileAddedRules with AsyncNotifierMixin { void order(int oldIndex, int newIndex) { final previous = List.from(value); - int insertIndex = newIndex; - if (oldIndex < newIndex) insertIndex -= 1; - final nextItems = List.from(previous); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(insertIndex, item); + final item = previous[oldIndex]; + final nextItems = previous.copyAndReorder(oldIndex, newIndex); value = nextItems; - final preOrder = nextItems.safeGet(insertIndex - 1)?.order; - final nextOrder = nextItems.safeGet(insertIndex + 1)?.order; + final preOrder = nextItems.safeGet(newIndex - 1)?.order; + final nextOrder = nextItems.safeGet(newIndex + 1)?.order; final newOrder = indexing.generateKeyBetween(preOrder, nextOrder)!; unawaited( withRollback( @@ -377,14 +371,11 @@ class ProfileCustomRules extends _$ProfileCustomRules with AsyncNotifierMixin { void order(int oldIndex, int newIndex) { final previous = List.from(value); - int insertIndex = newIndex; - if (oldIndex < newIndex) insertIndex -= 1; - final nextItems = List.from(previous); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(insertIndex, item); + final item = previous[oldIndex]; + final nextItems = previous.copyAndReorder(oldIndex, newIndex); value = nextItems; - final preOrder = nextItems.safeGet(insertIndex - 1)?.order; - final nextOrder = nextItems.safeGet(insertIndex + 1)?.order; + final preOrder = nextItems.safeGet(newIndex - 1)?.order; + final nextOrder = nextItems.safeGet(newIndex + 1)?.order; final newOrder = indexing.generateKeyBetween(preOrder, nextOrder)!; unawaited( withRollback( @@ -480,14 +471,11 @@ class ProxyGroups extends _$ProxyGroups with AsyncNotifierMixin { void order(int oldIndex, int newIndex) { final previous = List.from(value); - int insertIndex = newIndex; - if (oldIndex < newIndex) insertIndex -= 1; - final nextItems = List.from(previous); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(insertIndex, item); + final item = previous[oldIndex]; + final nextItems = previous.copyAndReorder(oldIndex, newIndex); value = nextItems; - final preOrder = nextItems.safeGet(insertIndex - 1)?.order; - final nextOrder = nextItems.safeGet(insertIndex + 1)?.order; + final preOrder = nextItems.safeGet(newIndex - 1)?.order; + final nextOrder = nextItems.safeGet(newIndex + 1)?.order; final newOrder = indexing.generateKeyBetween(preOrder, nextOrder)!; unawaited( withRollback( diff --git a/lib/providers/generated/action.g.dart b/lib/providers/generated/action.g.dart index 401232f5..eac384e1 100644 --- a/lib/providers/generated/action.g.dart +++ b/lib/providers/generated/action.g.dart @@ -91,7 +91,7 @@ final class SetupActionProvider extends $NotifierProvider { } } -String _$setupActionHash() => r'5e7aff49668e1013e2024c452b08950ccf2472fe'; +String _$setupActionHash() => r'c6c7b3b5d90f5070dca9ed02e51dcc06f812a294'; abstract class _$SetupAction extends $Notifier { void build(); @@ -244,7 +244,7 @@ final class SystemActionProvider extends $NotifierProvider { } } -String _$systemActionHash() => r'579650623bcc9263528fc0ac1f91b3d83bdc45e3'; +String _$systemActionHash() => r'04f09af2702925aff276423a9c224fd8d3aca061'; abstract class _$SystemAction extends $Notifier { void build(); @@ -450,7 +450,7 @@ final class ProfilesActionProvider } } -String _$profilesActionHash() => r'5c4a52394a97ff40cb9bc7bf30d7a412e36e877a'; +String _$profilesActionHash() => r'b2457dc5b18d51204d17995949a52cffce41df38'; abstract class _$ProfilesAction extends $Notifier { void build(); @@ -469,3 +469,55 @@ abstract class _$ProfilesAction extends $Notifier { element.handleCreate(ref, build); } } + +@ProviderFor(GeoResourceAction) +final geoResourceActionProvider = GeoResourceActionProvider._(); + +final class GeoResourceActionProvider + extends $NotifierProvider { + GeoResourceActionProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'geoResourceActionProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$geoResourceActionHash(); + + @$internal + @override + GeoResourceAction create() => GeoResourceAction(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$geoResourceActionHash() => r'980385b1cc4e685e0e2732471083c63d29b59c10'; + +abstract class _$GeoResourceAction extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + void, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/providers/generated/app.g.dart b/lib/providers/generated/app.g.dart index dccb4736..213ea094 100644 --- a/lib/providers/generated/app.g.dart +++ b/lib/providers/generated/app.g.dart @@ -1791,7 +1791,7 @@ final class NetworkDetectionProvider } } -String _$networkDetectionHash() => r'e2892c87c76992bec307699de7d838152aa272d5'; +String _$networkDetectionHash() => r'1cab20d67ec54321b4dbba9d971cd80e98542e23'; abstract class _$NetworkDetection extends $Notifier { NetworkDetectionState build(); diff --git a/lib/providers/generated/database.g.dart b/lib/providers/generated/database.g.dart index c2301f6e..a1eea995 100644 --- a/lib/providers/generated/database.g.dart +++ b/lib/providers/generated/database.g.dart @@ -292,7 +292,7 @@ final class ProfilesProvider } } -String _$profilesHash() => r'a37c94a2b4f8c9aabb25e1c90b22a760507611f8'; +String _$profilesHash() => r'a977548501ae750bc4fcc0f59dc0a4994ced7c91'; abstract class _$Profiles extends $Notifier> { List build(); @@ -336,7 +336,7 @@ final class ScriptsProvider Scripts create() => Scripts(); } -String _$scriptsHash() => r'c5c3c1a4529be6e13a9516ca0b6f98a2e0e127b4'; +String _$scriptsHash() => r'363611e5787ec107459446f305e35dada3e07cad'; abstract class _$Scripts extends $StreamNotifier> { Stream> build(); @@ -449,7 +449,7 @@ final class GlobalRulesProvider GlobalRules create() => GlobalRules(); } -String _$globalRulesHash() => r'fcded4e1dc862ac0ddd3223df9b723072d6f2fd5'; +String _$globalRulesHash() => r'209223171050c7d26de48537f8ff4f14d6fe5b1d'; abstract class _$GlobalRules extends $StreamNotifier> { Stream> build(); @@ -510,7 +510,7 @@ final class ProfileAddedRulesProvider } } -String _$profileAddedRulesHash() => r'276c520db54aec72ade2544260f26a43cbb71960'; +String _$profileAddedRulesHash() => r'eae5aef723d5fa858c35a530bca21aa0f80f00ad'; final class ProfileAddedRulesFamily extends $Family with @@ -600,7 +600,7 @@ final class ProfileCustomRulesProvider } String _$profileCustomRulesHash() => - r'3f4871fe309c1525ae65699eb1eb5fbcb1474acd'; + r'89a8547b746e4c4ff0b1f5ac2dd0b573d1e5892e'; final class ProfileCustomRulesFamily extends $Family with @@ -689,7 +689,7 @@ final class ProxyGroupsProvider } } -String _$proxyGroupsHash() => r'ea6abec14f31cf0fd8e74b5ba81c1625b3a705c3'; +String _$proxyGroupsHash() => r'78aa494f41c48600612d1376e1f4a931e8a21aef'; final class ProxyGroupsFamily extends $Family with @@ -781,7 +781,7 @@ final class ProfileDisabledRuleIdsProvider } String _$profileDisabledRuleIdsHash() => - r'595f81356a549fb4438ac5796df8b26a0c3e5e5d'; + r'8fdd7dc5c5ff51e7d9474c0351887073e3f8d468'; final class ProfileDisabledRuleIdsFamily extends $Family with diff --git a/lib/providers/generated/state.g.dart b/lib/providers/generated/state.g.dart index 1773d6d2..79c7088d 100644 --- a/lib/providers/generated/state.g.dart +++ b/lib/providers/generated/state.g.dart @@ -187,7 +187,7 @@ final class UpdateParamsProvider } } -String _$updateParamsHash() => r'012df72ab0e769a51c573f4692031506d7b1f1b4'; +String _$updateParamsHash() => r'6f471ce2a4114291cc7dc725723911764c8c3cd9'; @ProviderFor(proxyState) final proxyStateProvider = ProxyStateProvider._(); @@ -228,7 +228,7 @@ final class ProxyStateProvider } } -String _$proxyStateHash() => r'3df11daa70bd06de32da43e9b3e09a74389264b2'; +String _$proxyStateHash() => r'b4a316e7f67927d6903af702d2b4e542c11c11c8'; @ProviderFor(trayState) final trayStateProvider = TrayStateProvider._(); diff --git a/lib/providers/state.dart b/lib/providers/state.dart index 22452a11..2f814899 100644 --- a/lib/providers/state.dart +++ b/lib/providers/state.dart @@ -94,6 +94,8 @@ UpdateParams updateParams(Ref ref) { externalController: state.externalController, unifiedDelay: state.unifiedDelay, mixedPort: state.mixedPort, + geoAutoUpdate: state.geoAutoUpdate, + geoUpdateInterval: state.geoUpdateInterval, ), ), ); diff --git a/lib/state.dart b/lib/state.dart index d66f31e4..1e93f6df 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -112,6 +112,9 @@ class GlobalState { WidgetsBinding.instance.platformDispatcher.locale, ); await window?.init(version, config.windowProps); + if (system.isAndroid) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } return container; } @@ -302,10 +305,12 @@ class GlobalState { Future _initApp() async { FlutterError.onError = (details) { - commonPrint.log( - 'exception: ${details.exception} stack: ${details.stack}', - logLevel: LogLevel.warning, - ); + Future.microtask(() { + commonPrint.log( + 'exception: ${details.exception} stack: ${details.stack}', + logLevel: LogLevel.warning, + ); + }); }; container.read(systemActionProvider.notifier).updateTray(); container.read(profilesActionProvider.notifier).autoUpdateProfiles(); diff --git a/lib/views/application_setting.dart b/lib/views/application_setting.dart index d3b2e68a..ea835a22 100644 --- a/lib/views/application_setting.dart +++ b/lib/views/application_setting.dart @@ -275,7 +275,10 @@ class ApplicationSettingView extends StatelessWidget { Widget build(BuildContext context) { final List items = [ const MinimizeItem(), - if (system.isDesktop) ...[const AutoLaunchItem(), const SilentLaunchItem()], + if (system.isDesktop) ...[ + const AutoLaunchItem(), + const SilentLaunchItem(), + ], const AutoRunItem(), if (system.isAndroid) ...[const HiddenItem()], const AnimateTabItem(), diff --git a/lib/views/backup_and_restore.dart b/lib/views/backup_and_restore.dart index b83877e2..c2183a17 100644 --- a/lib/views/backup_and_restore.dart +++ b/lib/views/backup_and_restore.dart @@ -138,7 +138,7 @@ class _BackupAndRestoreState extends ConsumerState Future _restoreOnLocal(RestoreOption option) async { final appLocalizations = context.appLocalizations; - final file = await picker.pickerFile(withData: false); + final file = await picker.pickerFile(); final path = file?.path; if (path == null) return; await File(path).safeCopy(await appPath.backupFilePath); @@ -283,6 +283,7 @@ class _BackupAndRestoreState extends ConsumerState title: appLocalizations.file, value: dav.fileName, resetValue: defaultDavFileName, + maxLength: TextInputLimits.fileName, onChanged: (value) { _handleChange(value, ref); }, @@ -450,6 +451,7 @@ class _WebDAVFormDialogState extends ConsumerState { children: [ TextFormField( controller: _uriController, + inputFormatters: TextInputLimits.limit(TextInputLimits.uri), maxLines: 5, minLines: 1, decoration: InputDecoration( @@ -467,6 +469,7 @@ class _WebDAVFormDialogState extends ConsumerState { ), TextFormField( controller: _userController, + inputFormatters: TextInputLimits.limit(TextInputLimits.userName), decoration: InputDecoration( prefixIcon: const Icon(Icons.account_circle), border: const OutlineInputBorder(), @@ -484,6 +487,9 @@ class _WebDAVFormDialogState extends ConsumerState { builder: (_, obscure, _) { return TextFormField( controller: _passwordController, + inputFormatters: TextInputLimits.limit( + TextInputLimits.password, + ), obscureText: obscure, decoration: InputDecoration( prefixIcon: const Icon(Icons.password), diff --git a/lib/views/config/dns.dart b/lib/views/config/dns.dart index 275a07fc..7c3a1513 100644 --- a/lib/views/config/dns.dart +++ b/lib/views/config/dns.dart @@ -64,6 +64,7 @@ class ListenItem extends ConsumerWidget { delegate: InputDelegate( title: appLocalizations.listen, value: listen, + maxLength: TextInputLimits.dnsListen, validator: (value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.listen); @@ -198,6 +199,7 @@ class FakeIpRangeItem extends ConsumerWidget { delegate: InputDelegate( title: appLocalizations.fakeipRange, value: fakeIpRange, + maxLength: TextInputLimits.cidr, validator: (value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.fakeipRange); @@ -233,6 +235,7 @@ class FakeIpFilterItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.fakeipFilter, items: fakeIpFilter, + itemMaxLength: TextInputLimits.domain, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -264,6 +267,7 @@ class DefaultNameserverItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.defaultNameserver, items: defaultNameserver, + itemMaxLength: TextInputLimits.dnsServer, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -296,6 +300,7 @@ class NameserverItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.nameserver, items: nameserver, + itemMaxLength: TextInputLimits.dnsServer, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -373,6 +378,8 @@ class NameserverPolicyItem extends ConsumerWidget { widget: MapInputPage( title: appLocalizations.nameserverPolicy, map: nameserverPolicy, + keyMaxLength: TextInputLimits.domain, + valueMaxLength: TextInputLimits.dnsServer, titleBuilder: (item) => Text(item.key), subtitleBuilder: (item) => Text(item.value), ), @@ -405,6 +412,7 @@ class ProxyServerNameserverItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.proxyNameserver, items: proxyServerNameserver, + itemMaxLength: TextInputLimits.dnsServer, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -437,6 +445,7 @@ class FallbackItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.fallback, items: fallback, + itemMaxLength: TextInputLimits.dnsServer, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -494,6 +503,7 @@ class GeoipCodeItem extends ConsumerWidget { delegate: InputDelegate( title: appLocalizations.geoipCode, value: geoipCode, + maxLength: TextInputLimits.geoIpCode, validator: (value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.geoipCode); @@ -532,6 +542,7 @@ class GeositeItem extends ConsumerWidget { widget: ListInputPage( title: 'Geosite', items: geosite, + itemMaxLength: TextInputLimits.geoSite, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -566,6 +577,7 @@ class IpcidrItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.ipcidr, items: ipcidr, + itemMaxLength: TextInputLimits.cidr, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -599,6 +611,7 @@ class DomainItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.domain, items: domain, + itemMaxLength: TextInputLimits.domain, titleBuilder: (item) => Text(item), ), onChanged: (items) { diff --git a/lib/views/config/general.dart b/lib/views/config/general.dart index 2fd3cd3b..784d9869 100644 --- a/lib/views/config/general.dart +++ b/lib/views/config/general.dart @@ -7,6 +7,10 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +const _defaultUaValue = ''; +const _customUaValue = '__custom_ua__'; +const _presetUas = ['clash-verge/v2.4.2', 'ClashforWindows/0.19.23']; + class LogLevelItem extends ConsumerWidget { const LogLevelItem({super.key}); @@ -41,26 +45,221 @@ class LogLevelItem extends ConsumerWidget { class UaItem extends ConsumerWidget { const UaItem({super.key}); + Future _handleShowUaDialog(WidgetRef ref) async { + final result = await globalState.showCommonDialog<_UaDialogResult>( + child: _UaDialog( + value: ref.read(patchClashConfigProvider).globalUa, + customValue: ref.read(appSettingProvider).customUserAgent, + ), + ); + if (result == null) { + return; + } + final userAgent = result.value.trim(); + if (result.isCustom) { + ref + .read(appSettingProvider.notifier) + .update((state) => state.copyWith(customUserAgent: userAgent)); + } + ref + .read(patchClashConfigProvider.notifier) + .update( + (state) => + state.copyWith(globalUa: userAgent.isEmpty ? null : userAgent), + ); + } + @override Widget build(BuildContext context, ref) { final appLocalizations = context.appLocalizations; final globalUa = ref.watch( patchClashConfigProvider.select((state) => state.globalUa), ); - return ListItem.options( + return ListItem( leading: const Icon(Icons.computer_outlined), - title: const Text('UA'), + title: Text(appLocalizations.userAgent), subtitle: Text(globalUa ?? appLocalizations.defaultText), - delegate: OptionsDelegate( - title: 'UA', - options: [null, 'clash-verge/v2.4.2', 'ClashforWindows/0.19.23'], - value: globalUa, - onChanged: (value) { - ref - .read(patchClashConfigProvider.notifier) - .update((state) => state.copyWith(globalUa: value)); - }, - textBuilder: (ua) => ua ?? appLocalizations.defaultText, + onTap: () => _handleShowUaDialog(ref), + ); + } +} + +class _UaDialogResult { + final String value; + final bool isCustom; + + const _UaDialogResult({required this.value, required this.isCustom}); +} + +class _UaDialog extends StatefulWidget { + final String? value; + final String customValue; + + const _UaDialog({this.value, required this.customValue}); + + @override + State<_UaDialog> createState() => _UaDialogState(); +} + +class _UaDialogState extends State<_UaDialog> { + final _formKey = GlobalKey(); + + late final TextEditingController _customController; + late String _groupValue; + + @override + void initState() { + super.initState(); + final value = widget.value ?? _defaultUaValue; + _groupValue = _presetUas.contains(value) || value.isEmpty + ? value + : _customUaValue; + _customController = TextEditingController( + text: _groupValue == _customUaValue ? value : widget.customValue, + ); + } + + void _handleChanged(String? value) { + if (value == null) { + return; + } + if (value == _customUaValue) { + setState(() { + _groupValue = value; + }); + return; + } + Navigator.of(context).pop(_UaDialogResult(value: value, isCustom: false)); + } + + void _handleSubmit() { + if (_groupValue == _customUaValue && + _formKey.currentState?.validate() == false) { + return; + } + Navigator.of(context).pop( + _UaDialogResult( + value: _groupValue == _customUaValue + ? _customController.text + : _groupValue, + isCustom: _groupValue == _customUaValue, + ), + ); + } + + @override + void dispose() { + _customController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + return CommonDialog( + title: appLocalizations.userAgent, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(appLocalizations.cancel), + ), + TextButton( + onPressed: _handleSubmit, + child: Text(appLocalizations.submit), + ), + ], + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: RadioGroup( + groupValue: _groupValue, + onChanged: _handleChanged, + child: Wrap( + runSpacing: 8, + children: [ + ListItem.radio( + delegate: RadioDelegate( + value: _defaultUaValue, + onTab: () { + Navigator.of(context).pop( + const _UaDialogResult( + value: _defaultUaValue, + isCustom: false, + ), + ); + }, + ), + title: Text(appLocalizations.defaultText), + ), + for (final ua in _presetUas) + ListItem.radio( + delegate: RadioDelegate( + value: ua, + onTab: () { + Navigator.of( + context, + ).pop(_UaDialogResult(value: ua, isCustom: false)); + }, + ), + title: Text(ua), + ), + ListItem.radio( + delegate: RadioDelegate( + value: _customUaValue, + onTab: () { + setState(() { + _groupValue = _customUaValue; + }); + }, + ), + title: Builder( + builder: (context) { + final titleStyle = DefaultTextStyle.of(context).style; + return TextFormField( + enabled: _groupValue == _customUaValue, + style: titleStyle, + maxLength: TextInputLimits.userAgent, + inputFormatters: TextInputLimits.limit( + TextInputLimits.userAgent, + ), + decoration: InputDecoration( + isCollapsed: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + counterText: '', + hintStyle: titleStyle, + hintText: appLocalizations.custom, + ), + keyboardType: TextInputType.url, + maxLines: 1, + controller: _customController, + onChanged: (value) { + setState(() { + _groupValue = _customUaValue; + }); + }, + onFieldSubmitted: (_) { + _handleSubmit(); + }, + validator: (value) { + if (_groupValue == _customUaValue && + (value == null || value.trim().isEmpty)) { + return appLocalizations.emptyTip( + appLocalizations.userAgent, + ); + } + return null; + }, + ); + }, + ), + ), + ], + ), + ), ), ); } @@ -78,12 +277,13 @@ class KeepAliveIntervalItem extends ConsumerWidget { return ListItem.input( leading: const Icon(Icons.timer_outlined), title: Text(appLocalizations.keepAliveIntervalDesc), - subtitle: Text('$keepAliveInterval ${appLocalizations.seconds}'), + subtitle: Text(appLocalizations.secondsCount(keepAliveInterval)), delegate: InputDelegate( title: appLocalizations.keepAliveIntervalDesc, suffixText: appLocalizations.seconds, resetValue: '$defaultKeepAliveInterval', value: '$keepAliveInterval', + maxLength: TextInputLimits.interval, validator: (String? value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.interval); @@ -125,6 +325,7 @@ class TestUrlItem extends ConsumerWidget { resetValue: defaultTestUrl, title: appLocalizations.testUrl, value: testUrl, + maxLength: TextInputLimits.url, validator: (String? value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.testUrl); @@ -219,6 +420,8 @@ class HostsItem extends ConsumerWidget { widget: MapInputPage( title: 'Hosts', map: hosts, + keyMaxLength: TextInputLimits.domain, + valueMaxLength: TextInputLimits.hostValue, titleBuilder: (item) => Text(item.key), subtitleBuilder: (item) => Text(item.value), ), @@ -610,9 +813,12 @@ class _PortDialogState extends ConsumerState<_PortDialog> { spacing: 24, children: [ TextFormField( - keyboardType: TextInputType.url, + keyboardType: TextInputType.number, maxLines: 1, minLines: 1, + inputFormatters: TextInputLimits.digitsOnly( + TextInputLimits.port, + ), controller: _mixedPortController, onFieldSubmitted: (_) { _handleUpdate(); @@ -652,9 +858,12 @@ class _PortDialogState extends ConsumerState<_PortDialog> { ), if (_isMore) ...[ TextFormField( - keyboardType: TextInputType.url, + keyboardType: TextInputType.number, maxLines: 1, minLines: 1, + inputFormatters: TextInputLimits.digitsOnly( + TextInputLimits.port, + ), controller: _portController, onFieldSubmitted: (_) { _handleUpdate(); @@ -692,9 +901,12 @@ class _PortDialogState extends ConsumerState<_PortDialog> { }, ), TextFormField( - keyboardType: TextInputType.url, + keyboardType: TextInputType.number, maxLines: 1, minLines: 1, + inputFormatters: TextInputLimits.digitsOnly( + TextInputLimits.port, + ), controller: _socksPortController, onFieldSubmitted: (_) { _handleUpdate(); @@ -736,9 +948,12 @@ class _PortDialogState extends ConsumerState<_PortDialog> { }, ), TextFormField( - keyboardType: TextInputType.url, + keyboardType: TextInputType.number, maxLines: 1, minLines: 1, + inputFormatters: TextInputLimits.digitsOnly( + TextInputLimits.port, + ), controller: _redirPortController, onFieldSubmitted: (_) { _handleUpdate(); @@ -780,9 +995,12 @@ class _PortDialogState extends ConsumerState<_PortDialog> { }, ), TextFormField( - keyboardType: TextInputType.url, + keyboardType: TextInputType.number, maxLines: 1, minLines: 1, + inputFormatters: TextInputLimits.digitsOnly( + TextInputLimits.port, + ), controller: _tProxyPortController, onFieldSubmitted: (_) { _handleUpdate(); diff --git a/lib/views/config/network.dart b/lib/views/config/network.dart index dc97311e..b9001088 100644 --- a/lib/views/config/network.dart +++ b/lib/views/config/network.dart @@ -221,6 +221,7 @@ class BypassDomainItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.bypassDomain, items: bypassDomain, + itemMaxLength: TextInputLimits.domain, titleBuilder: (item) => Text(item), ), onChanged: (items) { @@ -314,6 +315,7 @@ class RouteAddressItem extends ConsumerWidget { widget: ListInputPage( title: appLocalizations.routeAddress, items: routeAddress, + itemMaxLength: TextInputLimits.cidr, titleBuilder: (item) => Text(item), ), onChanged: (items) { diff --git a/lib/views/config/on_demand.dart b/lib/views/config/on_demand.dart index 495c51ea..f89e2770 100644 --- a/lib/views/config/on_demand.dart +++ b/lib/views/config/on_demand.dart @@ -106,13 +106,7 @@ class _OnDemandViewState extends ConsumerState void _handleReorder(int oldIndex, newIndex) { globalState.container.read(excludeSSIDsProvider.notifier).update((value) { - final nextItems = List.from(value); - if (oldIndex < newIndex) { - newIndex -= 1; - } - final item = nextItems.removeAt(oldIndex); - nextItems.insert(newIndex, item); - return nextItems; + return value.copyAndReorder(oldIndex, newIndex); }); } @@ -350,7 +344,7 @@ class _OnDemandViewState extends ConsumerState ); }, itemCount: excludeSSIDs.length, - onReorder: _handleReorder, + onReorderItem: _handleReorder, ), ), ], diff --git a/lib/views/config/rules.dart b/lib/views/config/rules.dart index 046428d6..389d664e 100644 --- a/lib/views/config/rules.dart +++ b/lib/views/config/rules.dart @@ -109,7 +109,10 @@ class _AddedRulesViewState extends ConsumerState { illustration: const RuleEmptyIllustration(), ) : ReorderableList( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), itemBuilder: (context, index) { final rule = rules[index]; final position = ItemPosition.get(index, rules.length); @@ -135,7 +138,7 @@ class _AddedRulesViewState extends ConsumerState { }, itemExtent: ruleItemHeight, itemCount: rules.length, - onReorder: ref.read(globalRulesProvider.notifier).order, + onReorderItem: ref.read(globalRulesProvider.notifier).order, ), ), ); diff --git a/lib/views/config/scripts.dart b/lib/views/config/scripts.dart index cb8ed2fc..529e20c6 100644 --- a/lib/views/config/scripts.dart +++ b/lib/views/config/scripts.dart @@ -97,6 +97,7 @@ class _ScriptsViewState extends ConsumerState { title: appLocalizations.save, value: '', hintText: appLocalizations.pleaseEnterScriptName, + inputFormatters: TextInputLimits.limit(TextInputLimits.name), validator: (value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip(appLocalizations.name); diff --git a/lib/views/dashboard/dashboard.dart b/lib/views/dashboard/dashboard.dart index ac559d34..6a793132 100644 --- a/lib/views/dashboard/dashboard.dart +++ b/lib/views/dashboard/dashboard.dart @@ -87,7 +87,10 @@ class _DashboardViewState extends ConsumerState { }, ), onPressed: _handleConnection, - icon: const Icon(Icons.check, fontWeight: FontWeight.w900), + icon: const Icon( + Icons.check, + fontWeight: FontWeight.w900, + ), ) : FilledButton.icon( key: ValueKey(coreStatus), diff --git a/lib/views/dashboard/widgets/widgets.dart b/lib/views/dashboard/widgets/widgets.dart index fdb22e57..16560006 100644 --- a/lib/views/dashboard/widgets/widgets.dart +++ b/lib/views/dashboard/widgets/widgets.dart @@ -4,4 +4,4 @@ export 'network_speed.dart'; export 'outbound_mode.dart'; export 'quick_options.dart'; export 'traffic_usage.dart'; -export 'memory_info.dart'; \ No newline at end of file +export 'memory_info.dart'; diff --git a/lib/views/logs.dart b/lib/views/logs.dart index f5fe4b05..a701b6c0 100644 --- a/lib/views/logs.dart +++ b/lib/views/logs.dart @@ -72,9 +72,7 @@ class _LogsViewState extends ConsumerState { Future _handleExport() async { final appLocalizations = context.appLocalizations; final res = await globalState.safeRun(() async { - return globalState.container - .read(logsProvider.notifier) - .exportLogs(); + return globalState.container.read(logsProvider.notifier).exportLogs(); }, title: appLocalizations.exportLogs); if (res != true) return; globalState.showMessage( diff --git a/lib/views/profiles/add.dart b/lib/views/profiles/add.dart index bf61e543..a96eaaca 100644 --- a/lib/views/profiles/add.dart +++ b/lib/views/profiles/add.dart @@ -45,6 +45,7 @@ class AddProfileView extends StatelessWidget { title: appLocalizations.importFromURL, labelText: appLocalizations.url, value: '', + inputFormatters: TextInputLimits.limit(TextInputLimits.url), validator: (value) { if (value == null || value.isEmpty) { return appLocalizations.emptyTip('').trim(); @@ -131,6 +132,7 @@ class _URLFormDialogState extends State { keyboardType: TextInputType.url, minLines: 1, maxLines: 5, + inputFormatters: TextInputLimits.limit(TextInputLimits.url), onSubmitted: (_) { _handleAddProfileFormURL(); }, diff --git a/lib/views/profiles/edit.dart b/lib/views/profiles/edit.dart index b07ad214..af64e09b 100644 --- a/lib/views/profiles/edit.dart +++ b/lib/views/profiles/edit.dart @@ -176,8 +176,8 @@ class _EditProfileViewState extends State { Future _uploadProfileFile() async { final platformFile = await globalState.safeRun(picker.pickerFile); - if (platformFile?.bytes == null) return; - _fileData = platformFile?.bytes; + if (platformFile == null) return; + _fileData = await platformFile.readBytes(); if (!mounted) { return; } @@ -220,6 +220,7 @@ class _EditProfileViewState extends State { title: TextFormField( textInputAction: TextInputAction.next, controller: _labelController, + inputFormatters: TextInputLimits.limit(TextInputLimits.name), decoration: InputDecoration( border: const OutlineInputBorder(), labelText: appLocalizations.name, @@ -238,6 +239,7 @@ class _EditProfileViewState extends State { textInputAction: TextInputAction.next, keyboardType: TextInputType.url, controller: _urlController, + inputFormatters: TextInputLimits.limit(TextInputLimits.url), maxLines: 5, minLines: 1, decoration: InputDecoration( @@ -267,6 +269,9 @@ class _EditProfileViewState extends State { title: TextFormField( textInputAction: TextInputAction.next, controller: _autoUpdateDurationController, + inputFormatters: TextInputLimits.digitsOnly( + TextInputLimits.interval, + ), decoration: InputDecoration( border: const OutlineInputBorder(), labelText: appLocalizations.autoUpdateInterval, diff --git a/lib/views/profiles/overwrite/custom/groups.dart b/lib/views/profiles/overwrite/custom/groups.dart index 012f531e..82661c30 100644 --- a/lib/views/profiles/overwrite/custom/groups.dart +++ b/lib/views/profiles/overwrite/custom/groups.dart @@ -9,7 +9,6 @@ import 'package:fl_clash/state.dart'; import 'package:fl_clash/views/profiles/overwrite/custom/proxy_providers.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:smooth_sheets/smooth_sheets.dart'; @@ -167,7 +166,7 @@ class _CustomProxyGroupsViewState extends ConsumerState { globalState.measure.bodyLargeHeight + globalState.measure.bodyMediumHeight + 16, - onReorder: (oldIndex, newIndex) { + onReorderItem: (oldIndex, newIndex) { _handleReorder(oldIndex, newIndex); }, ), @@ -390,7 +389,7 @@ class _AddOrEditProxyGroupNestedSheetState child: PagedSheetRouteTheme( data: const PagedSheetRouteThemeData( transitionsBuilder: fadeAndSlideTransition, - transitionDuration: Duration(milliseconds: 400), + transitionDuration: Duration(milliseconds: 300), ), child: PagedSheet( decoration: MaterialSheetDecoration( @@ -535,6 +534,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { trailing: TextFormField( textAlign: TextAlign.end, initialValue: filter, + inputFormatters: TextInputLimits.limit(TextInputLimits.filter), onChanged: (value) { ref .read(proxyGroupProvider.notifier) @@ -554,7 +554,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { title: Text(appLocalizations.maxFailedTimes), trailing: TextFormField( keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + inputFormatters: TextInputLimits.digitsOnly(TextInputLimits.number), textAlign: TextAlign.end, initialValue: maxFailedTimes?.toString(), onChanged: (value) { @@ -578,6 +578,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { title: Text(appLocalizations.testUrl), trailing: TextFormField( keyboardType: TextInputType.url, + inputFormatters: TextInputLimits.limit(TextInputLimits.url), textAlign: TextAlign.end, initialValue: url, onChanged: (value) { @@ -599,7 +600,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { title: Text(appLocalizations.testInterval), trailing: TextFormField( keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + inputFormatters: TextInputLimits.digitsOnly(TextInputLimits.interval), textAlign: TextAlign.end, initialValue: interval?.toString(), onChanged: (value) { @@ -622,6 +623,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { trailing: TextFormField( textAlign: TextAlign.end, initialValue: excludeFilter, + inputFormatters: TextInputLimits.limit(TextInputLimits.filter), onChanged: (value) { ref .read(proxyGroupProvider.notifier) @@ -642,6 +644,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { trailing: TextFormField( textAlign: TextAlign.end, initialValue: type, + inputFormatters: TextInputLimits.limit(TextInputLimits.name), onChanged: (value) { ref .read(proxyGroupProvider.notifier) @@ -662,6 +665,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { trailing: TextFormField( textAlign: TextAlign.end, initialValue: expectedStatus, + inputFormatters: TextInputLimits.limit(TextInputLimits.status), onChanged: (value) { ref .read(proxyGroupProvider.notifier) @@ -744,6 +748,7 @@ class _EditProxyGroupViewState extends ConsumerState<_EditProxyGroupView> { trailing: TextFormField( initialValue: name, keyboardType: TextInputType.name, + inputFormatters: TextInputLimits.limit(TextInputLimits.groupName), onChanged: (value) { ref .read(proxyGroupProvider.notifier) diff --git a/lib/views/profiles/overwrite/custom/icon.dart b/lib/views/profiles/overwrite/custom/icon.dart index 34cf41fd..a3e0a0f4 100644 --- a/lib/views/profiles/overwrite/custom/icon.dart +++ b/lib/views/profiles/overwrite/custom/icon.dart @@ -246,6 +246,9 @@ class _IconEditViewState extends ConsumerState title: TextField( controller: _srcController, keyboardType: TextInputType.url, + inputFormatters: TextInputLimits.limit( + TextInputLimits.iconUrl, + ), onChanged: (_) { _handleInputChange(); }, diff --git a/lib/views/profiles/overwrite/custom/proxies.dart b/lib/views/profiles/overwrite/custom/proxies.dart index 8c30a844..8c3dab48 100644 --- a/lib/views/profiles/overwrite/custom/proxies.dart +++ b/lib/views/profiles/overwrite/custom/proxies.dart @@ -138,13 +138,11 @@ class _EditProxiesViewState extends ConsumerState } void _handleReorder(int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } ref.read(proxyGroupProvider.notifier).update((state) { - final nextItems = List.from(state.proxies ?? []); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(newIndex, item); + final nextItems = (state.proxies ?? []).copyAndReorder( + oldIndex, + newIndex, + ); return state.copyWith(proxies: nextItems); }); } @@ -292,7 +290,7 @@ class _EditProxiesViewState extends ConsumerState animation, ); }, - onReorder: (int oldIndex, int newIndex) { + onReorderItem: (int oldIndex, int newIndex) { _handleReorder(oldIndex, newIndex); }, ) diff --git a/lib/views/profiles/overwrite/custom/proxy_providers.dart b/lib/views/profiles/overwrite/custom/proxy_providers.dart index cde02f90..412b96bd 100644 --- a/lib/views/profiles/overwrite/custom/proxy_providers.dart +++ b/lib/views/profiles/overwrite/custom/proxy_providers.dart @@ -138,13 +138,8 @@ class _EditProxyProvidersViewState extends ConsumerState } void _handleReorder(int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } ref.read(proxyGroupProvider.notifier).update((state) { - final nextItems = List.from(state.use ?? []); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(newIndex, item); + final nextItems = (state.use ?? []).copyAndReorder(oldIndex, newIndex); return state.copyWith(use: nextItems); }); } @@ -283,7 +278,7 @@ class _EditProxyProvidersViewState extends ConsumerState animation, ); }, - onReorder: (int oldIndex, int newIndex) { + onReorderItem: (int oldIndex, int newIndex) { _handleReorder(oldIndex, newIndex); }, ) diff --git a/lib/views/profiles/overwrite/custom/rules.dart b/lib/views/profiles/overwrite/custom/rules.dart index 1cb5bf15..d9051fbf 100644 --- a/lib/views/profiles/overwrite/custom/rules.dart +++ b/lib/views/profiles/overwrite/custom/rules.dart @@ -245,7 +245,7 @@ class _CustomRulesViewState extends ConsumerState animation, ); }, - onReorder: _handleReorder, + onReorderItem: _handleReorder, ), ), ); @@ -360,7 +360,7 @@ class _AddOrEditRuleNestedSheetState child: PagedSheetRouteTheme( data: const PagedSheetRouteThemeData( transitionsBuilder: fadeAndSlideTransition, - transitionDuration: Duration(milliseconds: 400), + transitionDuration: Duration(milliseconds: 300), ), child: PagedSheet( decoration: MaterialSheetDecoration( @@ -468,6 +468,7 @@ class _AddOrEditRuleViewState extends ConsumerState<_AddOrEditRuleView> { trailing: TextFormField( initialValue: content, keyboardType: TextInputType.name, + inputFormatters: TextInputLimits.limit(TextInputLimits.rule), onChanged: (value) { ref .read(ruleProvider.notifier) diff --git a/lib/views/profiles/overwrite/custom/widgets.dart b/lib/views/profiles/overwrite/custom/widgets.dart index 37ae57e9..a6aba820 100644 --- a/lib/views/profiles/overwrite/custom/widgets.dart +++ b/lib/views/profiles/overwrite/custom/widgets.dart @@ -30,10 +30,10 @@ Widget fadeAndSlideTransition( return FadeTransition( opacity: CurveTween(curve: Curves.easeInExpo).animate(animation), child: FadeTransition( - opacity: Tween(begin: 1.0, end: 0.0) + opacity: Tween(begin: 1.0, end: 0.1) .chain(CurveTween(curve: Curves.easeOutExpo)) .animate(secondaryAnimation), - child: const CommonPageTransitionsBuilder().buildTransitions( + child: const FadeForwardsPageTransitionsBuilder().buildTransitions( ModalRoute.of(context) as PageRoute, context, animation, diff --git a/lib/views/profiles/overwrite/standard.dart b/lib/views/profiles/overwrite/standard.dart index f90ff3b3..91549a6f 100644 --- a/lib/views/profiles/overwrite/standard.dart +++ b/lib/views/profiles/overwrite/standard.dart @@ -166,7 +166,7 @@ class _StandardContentState extends ConsumerState { ); }, itemExtent: ruleItemHeight, - onReorder: ref + onReorderItem: ref .read(profileAddedRulesProvider(_profileId).notifier) .order, ); diff --git a/lib/views/profiles/preview.dart b/lib/views/profiles/preview.dart index f159ec41..5c76a145 100644 --- a/lib/views/profiles/preview.dart +++ b/lib/views/profiles/preview.dart @@ -45,7 +45,11 @@ class _PreviewProfileViewState extends State { builder: (_, content, _) { final title = widget.profile.realLabel; - return EditorPage(key: const Key('content'), title: title, content: content); + return EditorPage( + key: const Key('content'), + title: title, + content: content, + ); }, ); } diff --git a/lib/views/profiles/profiles.dart b/lib/views/profiles/profiles.dart index e2b20ee6..369c417b 100644 --- a/lib/views/profiles/profiles.dart +++ b/lib/views/profiles/profiles.dart @@ -510,13 +510,9 @@ class _ReorderableProfilesSheetState extends State { proxyDecorator: (child, index, animation) { return commonProxyDecorator(_buildItem(index), index, animation); }, - onReorder: (oldIndex, newIndex) { + onReorderItem: (oldIndex, newIndex) { setState(() { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final profile = profiles.removeAt(oldIndex); - profiles.insert(newIndex, profile); + profiles = profiles.copyAndReorder(oldIndex, newIndex); }); }, itemBuilder: (_, index) { diff --git a/lib/views/proxies/providers.dart b/lib/views/proxies/providers.dart index 5d023768..aeb1897d 100644 --- a/lib/views/proxies/providers.dart +++ b/lib/views/proxies/providers.dart @@ -88,8 +88,8 @@ class ProviderItem extends StatelessWidget { final ref = globalState.container; await globalState.safeRun(() async { final platformFile = await picker.pickerFile(); - final bytes = platformFile?.bytes; - if (bytes == null || provider.path == null) return; + if (platformFile == null || provider.path == null) return; + final bytes = await platformFile.readBytes(); await File(provider.path!).safeWriteAsBytes(bytes); final providerName = provider.name; final message = await coreController.sideLoadExternalProvider( @@ -110,7 +110,7 @@ class ProviderItem extends StatelessWidget { final count = provider.count; return switch (count == 0) { true => baseInfo, - false => '$baseInfo · $count${context.appLocalizations.entries}', + false => '$baseInfo · ${context.appLocalizations.entriesCount(count)}', }; } diff --git a/lib/views/resources.dart b/lib/views/resources.dart index 445608f8..45ac6a13 100644 --- a/lib/views/resources.dart +++ b/lib/views/resources.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/core/core.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/providers/action.dart'; +import 'package:fl_clash/providers/app.dart'; import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; @@ -10,83 +12,133 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' hide context; -@immutable -class GeoItem { - final String label; - final String key; - final String fileName; - - const GeoItem({ - required this.label, - required this.key, - required this.fileName, - }); -} - class ResourcesView extends StatelessWidget { const ResourcesView({super.key}); @override Widget build(BuildContext context) { - const geoItems = [ - GeoItem(label: 'GEOIP', fileName: GEOIP, key: 'geoip'), - GeoItem(label: 'GEOSITE', fileName: GEOSITE, key: 'geosite'), - GeoItem(label: 'MMDB', fileName: MMDB, key: 'mmdb'), - GeoItem(label: 'ASN', fileName: ASN, key: 'asn'), - ]; - + const geoResources = GeoResource.values; + final appLocalizations = context.appLocalizations; return CommonScaffold( title: context.appLocalizations.resources, - body: ListView.separated( - itemBuilder: (_, index) { - final geoItem = geoItems[index]; - return GeoDataListItem(geoItem: geoItem); + body: Consumer( + builder: (_, ref, _) { + final vm2 = ref.watch( + patchClashConfigProvider.select( + (state) => VM2(state.geoAutoUpdate, state.geoUpdateInterval), + ), + ); + return generateListView([ + ...generateSection( + title: appLocalizations.geoOptions, + items: [ + ListItem.switchItem( + title: Text(appLocalizations.geoAutoUpdate), + delegate: SwitchDelegate( + value: vm2.a, + onChanged: (value) { + ref + .read(patchClashConfigProvider.notifier) + .update( + (state) => state.copyWith(geoAutoUpdate: value), + ); + }, + ), + ), + ListItem.input( + title: Text(appLocalizations.geoAutoUpdateInterval), + trailing: Text( + appLocalizations.hoursCount(vm2.b), + style: context.textTheme.bodyMedium?.toSoftBold, + ), + delegate: InputDelegate( + suffixText: appLocalizations.hours, + title: appLocalizations.geoAutoUpdateInterval, + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return appLocalizations.emptyTip( + appLocalizations.geoAutoUpdateInterval, + ); + } + final interval = int.tryParse(value); + if (interval == null) { + return appLocalizations.numberTip( + appLocalizations.geoAutoUpdateInterval, + ); + } + if (interval <= 0) { + return appLocalizations.geoAutoUpdateIntervalTip; + } + return null; + }, + value: vm2.b.toString(), + onChanged: (value) { + final intValue = int.tryParse(value ?? '') ?? 0; + if (intValue <= 0) { + return; + } + ref + .read(patchClashConfigProvider.notifier) + .update( + (state) => + state.copyWith(geoUpdateInterval: intValue), + ); + }, + ), + ), + ], + ), + ...generateSection( + title: appLocalizations.geoResources, + items: [ + for (final geoResource in geoResources) + _GeoResourceListItem(geoResource), + ], + ), + ]); }, - separatorBuilder: (BuildContext context, int index) { - return const Divider(height: 0); - }, - itemCount: geoItems.length, ), ); } } -class GeoDataListItem extends StatefulWidget { - final GeoItem geoItem; +class _GeoResourceListItem extends ConsumerStatefulWidget { + final GeoResource type; - const GeoDataListItem({super.key, required this.geoItem}); + const _GeoResourceListItem(this.type); @override - State createState() => _GeoDataListItemState(); + ConsumerState<_GeoResourceListItem> createState() => + _GeoResourceListItemState(); } -class _GeoDataListItemState extends State { - final isUpdating = ValueNotifier(false); +class _GeoResourceListItemState extends ConsumerState<_GeoResourceListItem> { + String get fileName { + return switch (widget.type) { + GeoResource.MMDB => MMDB, + GeoResource.ASN => ASN, + GeoResource.GEOIP => GEOIP, + GeoResource.GEOSITE => GEOSITE, + }; + } - GeoItem get geoItem => widget.geoItem; - - Future _updateUrl(String url, WidgetRef ref) async { - final defaultMap = defaultGeoXUrl.toJson(); + Future _updateUrl(String url) async { final newUrl = await globalState.showCommonDialog( child: UpdateGeoUrlFormDialog( - title: geoItem.label, + title: widget.type.name, url: url, - defaultValue: defaultMap[geoItem.key], + defaultValue: defaultGeoXUrl[widget.type], ), ); if (newUrl != null && newUrl != url && mounted) { try { - if (!newUrl.isUrl) { - throw 'Invalid url'; - } - ref.read(patchClashConfigProvider.notifier).update((state) { - final map = state.geoXUrl.toJson(); - map[geoItem.key] = newUrl; - return state.copyWith(geoXUrl: GeoXUrl.fromJson(map)); - }); + ref + .read(geoResourceActionProvider.notifier) + .updateGeoResourceUrl(widget.type, newUrl); } catch (e) { globalState.showMessage( - title: geoItem.label, + title: widget.type.name, message: TextSpan(text: e.toString()), ); } @@ -101,60 +153,65 @@ class _GeoDataListItemState extends State { return FileInfo(size: size, lastModified: lastModified); } - Widget _buildSubtitle() { - return Consumer( - builder: (context, ref, _) { - final appLocalizations = context.appLocalizations; - final url = ref.watch( - patchClashConfigProvider.select( - (state) => state.geoXUrl.toJson()[geoItem.key], - ), - ); - if (url == null) { - return const SizedBox(); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 6), - FutureBuilder( - future: _getGeoFileLastModified(geoItem.fileName), - builder: (_, snapshot) { - final height = globalState.measure.bodyMediumHeight; - return SizedBox( - height: height, - child: snapshot.data == null - ? SizedBox(width: height, height: height) - : Text( - snapshot.data!.getDesc(context), - style: context.textTheme.bodyMedium, - ), - ); - }, - ), - const SizedBox(height: 4), - Text(url, style: context.textTheme.bodyMedium?.toLight), - const SizedBox(height: 12), - Wrap( - runSpacing: 6, - spacing: 12, - runAlignment: WrapAlignment.center, + Future _handleUpdateGeoDataItem() async { + await globalState.safeRun(() async { + await ref + .read(geoResourceActionProvider.notifier) + .updateGeoResource(widget.type); + }, silence: false); + } + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + final isUpdating = ref.watch(isUpdatingProvider(widget.type.updatingKey)); + final url = ref.watch( + patchClashConfigProvider.select((state) => state.geoXUrl[widget.type]), + ); + return ListItem( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + title: Text(widget.type.name), + subtitle: url == null + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CommonChip( - avatar: const Icon(Icons.edit), - label: appLocalizations.edit, - onPressed: () { - _updateUrl(url, ref); + const SizedBox(height: 6), + FutureBuilder( + future: _getGeoFileLastModified(fileName), + builder: (_, snapshot) { + final height = globalState.measure.bodyMediumHeight; + return SizedBox( + height: height, + child: snapshot.data == null + ? SizedBox(width: height, height: height) + : Text( + snapshot.data!.getDesc(context), + style: context.textTheme.bodyMedium, + ), + ); }, ), - Row( - mainAxisSize: MainAxisSize.min, + const SizedBox(height: 4), + Text(url, style: context.textTheme.bodyMedium?.toLight), + const SizedBox(height: 12), + Wrap( + runSpacing: 6, + spacing: 12, + runAlignment: WrapAlignment.center, children: [ - SizedBox( - child: ValueListenableBuilder( - valueListenable: isUpdating, - builder: (_, isUpdating, _) { - return isUpdating + CommonChip( + avatar: const Icon(Icons.edit), + label: appLocalizations.edit, + onPressed: () { + _updateUrl(url); + }, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + child: isUpdating ? const SizedBox( height: 30, width: 30, @@ -169,62 +226,20 @@ class _GeoDataListItemState extends State { onPressed: () { _handleUpdateGeoDataItem(); }, - ); - }, - ), + ), + ), + ], ), ], ), + const SizedBox(height: 6), ], ), - const SizedBox(height: 6), - ], - ); - }, - ); - } - - Future _handleUpdateGeoDataItem() async { - await globalState.safeRun(() async { - await updateGeoDateItem(); - }, silence: false); - if (mounted) { - setState(() {}); - } - } - - Future updateGeoDateItem() async { - isUpdating.value = true; - try { - final message = await coreController.updateGeoData( - UpdateGeoDataParams(geoName: geoItem.fileName, geoType: geoItem.label), - ); - if (message.isNotEmpty) throw message; - } catch (e) { - isUpdating.value = false; - rethrow; - } - isUpdating.value = false; - return; - } - - @override - void dispose() { - super.dispose(); - isUpdating.dispose(); - } - - @override - Widget build(BuildContext context) { - return ListItem( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - title: Text(geoItem.label), - subtitle: _buildSubtitle(), ); } } -class UpdateGeoUrlFormDialog extends StatefulWidget { +class UpdateGeoUrlFormDialog extends StatelessWidget { final String title; final String url; final String? defaultValue; @@ -236,68 +251,24 @@ class UpdateGeoUrlFormDialog extends StatefulWidget { this.defaultValue, }); - @override - State createState() => _UpdateGeoUrlFormDialogState(); -} - -class _UpdateGeoUrlFormDialogState extends State { - late final TextEditingController _urlController; - - @override - void initState() { - super.initState(); - _urlController = TextEditingController(text: widget.url); - } - - Future _handleReset() async { - if (widget.defaultValue == null) { - return; - } - Navigator.of(context).pop(widget.defaultValue); - } - - Future _handleUpdate() async { - final url = _urlController.value.text; - if (url.isEmpty) return; - Navigator.of(context).pop(url); - } - - @override - void dispose() { - _urlController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final appLocalizations = context.appLocalizations; - return CommonDialog( - title: widget.title, - actions: [ - if (widget.defaultValue != null && - _urlController.value.text != widget.defaultValue) ...[ - TextButton( - onPressed: _handleReset, - child: Text(appLocalizations.reset), - ), - const SizedBox(width: 4), - ], - TextButton( - onPressed: _handleUpdate, - child: Text(appLocalizations.submit), - ), - ], - child: Wrap( - runSpacing: 16, - children: [ - TextField( - maxLines: 5, - minLines: 1, - controller: _urlController, - decoration: const InputDecoration(border: OutlineInputBorder()), - ), - ], - ), + return InputDialog( + autovalidateMode: AutovalidateMode.onUserInteraction, + title: title, + value: url, + resetValue: defaultValue, + inputFormatters: TextInputLimits.limit(TextInputLimits.url), + validator: (value) { + if (value == null || value.isEmpty) { + return appLocalizations.emptyTip('').trim(); + } + if (!value.isUrl) { + return appLocalizations.urlTip('').trim(); + } + return null; + }, ); } } diff --git a/lib/views/theme.dart b/lib/views/theme.dart index e07ca1cf..5b4d845e 100644 --- a/lib/views/theme.dart +++ b/lib/views/theme.dart @@ -1,7 +1,6 @@ // ignore_for_file: deprecated_member_use import 'dart:math'; -import 'dart:ui' as ui; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -12,6 +11,7 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:material_color_utilities/hct/hct.dart'; class ThemeModeItem { final ThemeMode themeMode; @@ -500,7 +500,7 @@ class _TextScaleFactorItem extends ConsumerWidget { child: ActivateBox( active: textScale.enable, child: SliderTheme( - data: _SliderDefaultsM3(context), + data: SliderDefaultsM3(context), child: Slider( padding: EdgeInsets.zero, min: minTextScale, @@ -540,7 +540,7 @@ class _PaletteDialog extends StatefulWidget { } class _PaletteDialogState extends State<_PaletteDialog> { - final _controller = ValueNotifier(Colors.transparent); + final _controller = ValueNotifier(Color(Hct.from(0, 0, 60).toInt())); @override Widget build(BuildContext context) { @@ -562,136 +562,11 @@ class _PaletteDialogState extends State<_PaletteDialog> { ), ], child: Column( + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), - SizedBox( - width: 250, - height: 250, - child: Palette(controller: _controller), - ), - const SizedBox(height: 24), - ValueListenableBuilder( - valueListenable: _controller, - builder: (_, color, _) { - return PrimaryColorBox( - primaryColor: color, - child: FilledButton( - onPressed: () {}, - child: Text(_controller.value.hex), - ), - ); - }, - ), + SizedBox(width: 300, child: Palette(controller: _controller)), ], ), ); } } - -class _SliderDefaultsM3 extends SliderThemeData { - _SliderDefaultsM3(this.context) : super(trackHeight: 16.0); - - final BuildContext context; - late final ColorScheme _colors = Theme.of(context).colorScheme; - - @override - Color? get activeTrackColor => _colors.primary; - - @override - Color? get inactiveTrackColor => _colors.secondaryContainer; - - @override - Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54); - - @override - Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38); - - @override - Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12); - - @override - Color? get disabledSecondaryActiveTrackColor => - _colors.onSurface.withOpacity(0.38); - - @override - Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0); - - @override - Color? get inactiveTickMarkColor => - _colors.onSecondaryContainer.withOpacity(1.0); - - @override - Color? get disabledActiveTickMarkColor => _colors.onInverseSurface; - - @override - Color? get disabledInactiveTickMarkColor => _colors.onSurface; - - @override - Color? get thumbColor => _colors.primary; - - @override - Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38); - - @override - Color? get overlayColor => - WidgetStateColor.resolveWith((Set states) { - if (states.contains(WidgetState.dragged)) { - return _colors.primary.withOpacity(0.1); - } - if (states.contains(WidgetState.hovered)) { - return _colors.primary.withOpacity(0.08); - } - if (states.contains(WidgetState.focused)) { - return _colors.primary.withOpacity(0.1); - } - - return Colors.transparent; - }); - - @override - TextStyle? get valueIndicatorTextStyle => Theme.of( - context, - ).textTheme.labelLarge!.copyWith(color: _colors.onInverseSurface); - - @override - Color? get valueIndicatorColor => _colors.inverseSurface; - - @override - SliderComponentShape? get valueIndicatorShape => - const RoundedRectSliderValueIndicatorShape(); - - @override - SliderComponentShape? get thumbShape => const HandleThumbShape(); - - @override - SliderTrackShape? get trackShape => const GappedSliderTrackShape(); - - @override - SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); - - @override - SliderTickMarkShape? get tickMarkShape => - const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2); - - @override - WidgetStateProperty? get thumbSize { - return WidgetStateProperty.resolveWith((Set states) { - if (states.contains(WidgetState.disabled)) { - return const Size(4.0, 44.0); - } - if (states.contains(WidgetState.hovered)) { - return const Size(4.0, 44.0); - } - if (states.contains(WidgetState.focused)) { - return const Size(2.0, 44.0); - } - if (states.contains(WidgetState.pressed)) { - return const Size(2.0, 44.0); - } - return const Size(4.0, 44.0); - }); - } - - @override - double? get trackGap => 6.0; -} diff --git a/lib/views/tools.dart b/lib/views/tools.dart index 063c6c1b..950bdc36 100644 --- a/lib/views/tools.dart +++ b/lib/views/tools.dart @@ -36,7 +36,11 @@ class _ToolViewState extends ConsumerState { subtitle: navigationItem.description != null ? Text(Intl.message(navigationItem.description!)) : null, - delegate: OpenDelegate(widget: navigationItem.builder(context)), + delegate: OpenDelegate( + widget: navigationItem.builder(context), + maxWidth: 400, + forceFull: false, + ), ); } diff --git a/lib/widgets/activate_box.dart b/lib/widgets/activate_box.dart index bcb335c2..14b0cef5 100644 --- a/lib/widgets/activate_box.dart +++ b/lib/widgets/activate_box.dart @@ -4,17 +4,10 @@ class ActivateBox extends StatelessWidget { final Widget child; final bool active; - const ActivateBox({ - super.key, - required this.child, - this.active = false, - }); + const ActivateBox({super.key, required this.child, this.active = false}); @override Widget build(BuildContext context) { - return IgnorePointer( - ignoring: !active, - child: child, - ); + return IgnorePointer(ignoring: !active, child: child); } } diff --git a/lib/widgets/animate_grid.dart b/lib/widgets/animate_grid.dart index 7ce7255d..eb0234ab 100644 --- a/lib/widgets/animate_grid.dart +++ b/lib/widgets/animate_grid.dart @@ -26,69 +26,60 @@ class AnimateGrid extends StatelessWidget { int _rows(int columns, int count) => (count / columns).ceil(); - Offset _getOffset( - int index, - int count, - double itemWidth, - double itemHeight, - ) { + Offset _getOffset(int index, int count, double itemWidth, double itemHeight) { final xIndex = index % columns; final yIndex = (index / columns).floor(); return Offset( - xIndex * itemWidth + xIndex * gap, yIndex * itemHeight + yIndex * gap); + xIndex * itemWidth + xIndex * gap, + yIndex * itemHeight + yIndex * gap, + ); } @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (_, constraints) { - assert(constraints.hasBoundedHeight == false); - final gapWidth = (columns - 1) * gap; - final width = constraints.maxWidth; - final itemWidth = (width - gapWidth) / columns; - final count = items.length; - final rows = _rows(columns, count); - final gapHeight = (rows - 1) * gap; - final height = rows * itemHeight + gapHeight; - return SizedBox( - width: width, - height: height, - child: Stack( - children: [ - for (var i = 0; i <= count - 1; i++) - Builder( - key: keyBuilder(items[i]), - builder: (context) { - final item = items[i]; - final offset = _getOffset( - i, - count, - itemWidth, - itemHeight, - ); - return TweenAnimationBuilder( - tween: Tween(end: offset), - duration: duration, - curve: curve, - builder: (_, offset, child) { - return Transform.translate( - offset: offset, - child: child, - ); - }, - child: SizedBox( - height: itemHeight, - width: itemWidth, - child: builder( - context, - item, + return LayoutBuilder( + builder: (_, constraints) { + assert(constraints.hasBoundedHeight == false); + final gapWidth = (columns - 1) * gap; + final width = constraints.maxWidth; + final itemWidth = (width - gapWidth) / columns; + final count = items.length; + final rows = _rows(columns, count); + final gapHeight = (rows - 1) * gap; + final height = rows * itemHeight + gapHeight; + return SizedBox( + width: width, + height: height, + child: Stack( + children: [ + for (var i = 0; i <= count - 1; i++) + Builder( + key: keyBuilder(items[i]), + builder: (context) { + final item = items[i]; + final offset = _getOffset(i, count, itemWidth, itemHeight); + return TweenAnimationBuilder( + tween: Tween(end: offset), + duration: duration, + curve: curve, + builder: (_, offset, child) { + return Transform.translate( + offset: offset, + child: child, + ); + }, + child: SizedBox( + height: itemHeight, + width: itemWidth, + child: builder(context, item), ), - ), - ); - }, - ), - ], - ), - ); - }); + ); + }, + ), + ], + ), + ); + }, + ); } } diff --git a/lib/widgets/bar_chart.dart b/lib/widgets/bar_chart.dart index 6c363f7d..af76a49d 100644 --- a/lib/widgets/bar_chart.dart +++ b/lib/widgets/bar_chart.dart @@ -9,10 +9,7 @@ class BarChartData { final double value; final String label; - const BarChartData({ - required this.value, - required this.label, - }); + const BarChartData({required this.value, required this.label}); } class BarChart extends StatefulWidget { @@ -62,21 +59,23 @@ class _BarChartState extends State @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (_, container) { - return AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return CustomPaint( - painter: BarChartPainter( - _oldData, - widget.data, - _animationController.value, - ), - size: Size(container.maxWidth, container.maxHeight), - ); - }, - ); - }); + return LayoutBuilder( + builder: (_, container) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return CustomPaint( + painter: BarChartPainter( + _oldData, + widget.data, + _animationController.value, + ), + size: Size(container.maxWidth, container.maxHeight), + ); + }, + ); + }, + ); } } @@ -92,15 +91,18 @@ class BarChartPainter extends CustomPainter { const maxBarWidth = 30; final barWidth = (size.width - spacing * (dataList.length - 1)) / dataList.length; - final maxValue = - dataList.fold(0.0, (max, item) => max > item.value ? max : item.value); + final maxValue = dataList.fold( + 0.0, + (max, item) => max > item.value ? max : item.value, + ); final rects = {}; for (int i = 0; i < dataList.length; i++) { final data = dataList[i]; final double barHeight = (data.value / maxValue) * size.height; - final adjustLeft = - barWidth > maxBarWidth ? (barWidth - maxBarWidth) / 2 : 0; + final adjustLeft = barWidth > maxBarWidth + ? (barWidth - maxBarWidth) / 2 + : 0; final double left = i * (barWidth + spacing) + adjustLeft; final double top = size.height - barHeight; rects[data.label] = Rect.fromLTWH( @@ -125,7 +127,8 @@ class BarChartPainter extends CustomPainter { for (int i = 0; i < newRectEntries.length; i++) { final newRectEntry = newRectEntries[i]; final newRect = newRectEntry.value; - final oldRect = oldRectMap[newRectEntry.key] ?? + final oldRect = + oldRectMap[newRectEntry.key] ?? newRect.translate(newRect.left * (progress - 1), 0); final interpolatedRect = Rect.fromLTRB( diff --git a/lib/widgets/chip.dart b/lib/widgets/chip.dart index a147e264..d273a6bd 100644 --- a/lib/widgets/chip.dart +++ b/lib/widgets/chip.dart @@ -22,10 +22,7 @@ class CommonChip extends StatelessWidget { if (type == ChipType.delete) { return Chip( avatar: avatar, - labelPadding: const EdgeInsets.symmetric( - vertical: 0, - horizontal: 4, - ), + labelPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 4), clipBehavior: Clip.antiAlias, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onDeleted: onPressed ?? () {}, @@ -37,10 +34,7 @@ class CommonChip extends StatelessWidget { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, avatar: avatar, clipBehavior: Clip.antiAlias, - labelPadding: const EdgeInsets.symmetric( - vertical: 0, - horizontal: 4, - ), + labelPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 4), onPressed: onPressed ?? () {}, labelStyle: labelStyle, label: Text(label), diff --git a/lib/widgets/container.dart b/lib/widgets/container.dart index b01d5872..bb9a5f7b 100644 --- a/lib/widgets/container.dart +++ b/lib/widgets/container.dart @@ -61,13 +61,17 @@ class CommonSafeArea extends StatelessWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties - .add(FlagProperty('left', value: left, ifTrue: 'avoid left padding')); - properties - .add(FlagProperty('top', value: top, ifTrue: 'avoid top padding')); properties.add( - FlagProperty('right', value: right, ifTrue: 'avoid right padding')); + FlagProperty('left', value: left, ifTrue: 'avoid left padding'), + ); properties.add( - FlagProperty('bottom', value: bottom, ifTrue: 'avoid bottom padding')); + FlagProperty('top', value: top, ifTrue: 'avoid top padding'), + ); + properties.add( + FlagProperty('right', value: right, ifTrue: 'avoid right padding'), + ); + properties.add( + FlagProperty('bottom', value: bottom, ifTrue: 'avoid bottom padding'), + ); } } diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index b63187b8..3d48bd74 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -32,18 +32,11 @@ class CommonDialog extends ConsumerWidget { backgroundColor: backgroundColor, content: Container( constraints: BoxConstraints( - maxHeight: min( - size.height - 40, - 500, - ), + maxHeight: min(size.height - 40, 500), maxWidth: 300, ), width: size.width - 40, - child: !overrideScroll - ? SingleChildScrollView( - child: child, - ) - : child, + child: !overrideScroll ? SingleChildScrollView(child: child) : child, ), ); } @@ -52,10 +45,7 @@ class CommonDialog extends ConsumerWidget { class CommonModal extends ConsumerWidget { final Widget? child; - const CommonModal({ - super.key, - this.child, - }); + const CommonModal({super.key, this.child}); @override Widget build(BuildContext context, ref) { @@ -64,9 +54,7 @@ class CommonModal extends ConsumerWidget { child: Container( width: size.width * 0.85, height: size.height * 0.85, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)), clipBehavior: Clip.antiAlias, child: child, ), diff --git a/lib/widgets/disabled_mask.dart b/lib/widgets/disabled_mask.dart index 3b78251c..92f242dd 100644 --- a/lib/widgets/disabled_mask.dart +++ b/lib/widgets/disabled_mask.dart @@ -4,11 +4,7 @@ class DisabledMask extends StatefulWidget { final Widget child; final bool status; - const DisabledMask({ - super.key, - required this.child, - this.status = true, - }); + const DisabledMask({super.key, required this.child, this.status = true}); @override State createState() => _DisabledMaskState(); @@ -19,10 +15,7 @@ class _DisabledMaskState extends State { @override Widget build(BuildContext context) { - final child = Container( - key: childKey, - child: widget.child, - ); + final child = Container(key: childKey, child: widget.child); if (!widget.status) { return child; } diff --git a/lib/widgets/dismissible.dart b/lib/widgets/dismissible.dart index 71fef4a6..ab4e149a 100644 --- a/lib/widgets/dismissible.dart +++ b/lib/widgets/dismissible.dart @@ -127,7 +127,7 @@ class _ExternalDismissibleState extends State } return SizeTransition( - axisAlignment: 0.5, + alignment: AlignmentGeometry.center, sizeFactor: _resizeAnimation, axis: Axis.vertical, child: content, diff --git a/lib/widgets/donut_chart.dart b/lib/widgets/donut_chart.dart index 17e4b8c0..9a11a1fb 100644 --- a/lib/widgets/donut_chart.dart +++ b/lib/widgets/donut_chart.dart @@ -8,10 +8,8 @@ class DonutChartData { final double _value; final Color color; - const DonutChartData({ - required double value, - required this.color, - }) : _value = value + 1; + const DonutChartData({required double value, required this.color}) + : _value = value + 1; double get value => _value; @@ -151,10 +149,9 @@ class DonutChartPainter extends CustomPainter { final interpolatedValue = _expTransform(interpolatedLogValue); - result.add(DonutChartData( - value: interpolatedValue, - color: newData[i].color, - )); + result.add( + DonutChartData(value: interpolatedValue, color: newData[i].color), + ); } _cachedInterpolatedData = result; diff --git a/lib/widgets/float_layout.dart b/lib/widgets/float_layout.dart index a2d8a387..3a5863ff 100644 --- a/lib/widgets/float_layout.dart +++ b/lib/widgets/float_layout.dart @@ -16,15 +16,11 @@ class FloatLayout extends StatelessWidget { return Stack( fit: StackFit.loose, children: [ - Center( - child: child, - ), + Center(child: child), Positioned( bottom: 0, right: 0, - child: Container( - child: floatingWidget, - ), + child: Container(child: floatingWidget), ), ], ); @@ -34,10 +30,7 @@ class FloatLayout extends StatelessWidget { class FloatWrapper extends StatelessWidget { final Widget child; - const FloatWrapper({ - super.key, - required this.child, - }); + const FloatWrapper({super.key, required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/input.dart b/lib/widgets/input.dart index 8160df2e..39fc876e 100644 --- a/lib/widgets/input.dart +++ b/lib/widgets/input.dart @@ -9,6 +9,7 @@ import 'package:fl_clash/widgets/null_status.dart'; import 'package:fl_clash/widgets/pop_scope.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'effect.dart'; @@ -102,6 +103,8 @@ class InputDialog extends StatefulWidget { final AutovalidateMode? autovalidateMode; final bool? obscureText; final int? maxLength; + final List? inputFormatters; + final TextInputType? keyboardType; const InputDialog({ super.key, @@ -114,6 +117,8 @@ class InputDialog extends StatefulWidget { this.obscureText, this.labelText, this.maxLength, + this.inputFormatters, + this.keyboardType, this.autovalidateMode = AutovalidateMode.onUserInteraction, }); @@ -189,8 +194,9 @@ class _InputDialogState extends State { children: [ TextFormField( maxLength: widget.maxLength, + inputFormatters: widget.inputFormatters, obscureText: widget.obscureText ?? false, - keyboardType: TextInputType.url, + keyboardType: widget.keyboardType ?? TextInputType.url, maxLines: widget.obscureText == true ? 1 : 5, minLines: 1, controller: _textController, @@ -219,6 +225,7 @@ class ListInputPage extends ConsumerStatefulWidget { final Widget Function(String item)? subtitleBuilder; final Widget Function(String item)? leadingBuilder; final String? valueLabel; + final int? itemMaxLength; const ListInputPage({ super.key, @@ -228,6 +235,7 @@ class ListInputPage extends ConsumerStatefulWidget { this.leadingBuilder, this.valueLabel, this.subtitleBuilder, + this.itemMaxLength, }); @override @@ -247,13 +255,7 @@ class _ListInputPageState extends ConsumerState { } void _handleReorder(int oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final nextItems = List.from(_items); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(newIndex, item); - _items = nextItems; + _items = _items.copyAndReorder(oldIndex, newIndex); setState(() {}); } @@ -291,6 +293,7 @@ class _ListInputPageState extends ConsumerState { value: item ?? '', validator: uniqueValidator, ), + valueMaxLength: widget.itemMaxLength, title: item != null ? appLocalizations.edit : appLocalizations.add, ), ); @@ -447,7 +450,7 @@ class _ListInputPageState extends ConsumerState { animation, ); }, - onReorder: _handleReorder, + onReorderItem: _handleReorder, ), ), ); @@ -462,6 +465,8 @@ class MapInputPage extends ConsumerStatefulWidget { final Widget Function(MapEntry item)? leadingBuilder; final String? keyLabel; final String? valueLabel; + final int? keyMaxLength; + final int? valueMaxLength; const MapInputPage({ super.key, @@ -472,6 +477,8 @@ class MapInputPage extends ConsumerStatefulWidget { this.keyLabel, this.valueLabel, this.subtitleBuilder, + this.keyMaxLength, + this.valueMaxLength, }); @override @@ -491,13 +498,7 @@ class _MapInputPageState extends ConsumerState { } void _handleReorder(int oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final nextItems = List>.from(_items); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(newIndex, item); - _items = nextItems; + _items = _items.copyAndReorder(oldIndex, newIndex); setState(() {}); } @@ -543,6 +544,8 @@ class _MapInputPageState extends ConsumerState { child: AddDialog( keyField: keyField, valueField: valueField, + keyMaxLength: widget.keyMaxLength, + valueMaxLength: widget.valueMaxLength, title: item != null ? appLocalizations.edit : appLocalizations.add, ), ); @@ -702,7 +705,7 @@ class _MapInputPageState extends ConsumerState { animation, ); }, - onReorder: _handleReorder, + onReorderItem: _handleReorder, ), ), ); @@ -713,12 +716,16 @@ class AddDialog extends StatefulWidget { final String title; final Field? keyField; final Field valueField; + final int? keyMaxLength; + final int? valueMaxLength; const AddDialog({ super.key, required this.title, this.keyField, required this.valueField, + this.keyMaxLength, + this.valueMaxLength, }); @override @@ -779,6 +786,9 @@ class _AddDialogState extends State { TextFormField( maxLines: 3, minLines: 1, + inputFormatters: widget.keyMaxLength == null + ? null + : TextInputLimits.limit(widget.keyMaxLength!), controller: _keyController, decoration: InputDecoration( border: const OutlineInputBorder(), @@ -801,6 +811,9 @@ class _AddDialogState extends State { TextFormField( maxLines: 3, minLines: 1, + inputFormatters: widget.valueMaxLength == null + ? null + : TextInputLimits.limit(widget.valueMaxLength!), keyboardType: TextInputType.text, controller: _valueController, decoration: InputDecoration( diff --git a/lib/widgets/keep_scope.dart b/lib/widgets/keep_scope.dart index 4bb1879a..2296763a 100644 --- a/lib/widgets/keep_scope.dart +++ b/lib/widgets/keep_scope.dart @@ -4,11 +4,7 @@ class KeepScope extends StatefulWidget { final Widget child; final bool keep; - const KeepScope({ - super.key, - required this.child, - this.keep = true, - }); + const KeepScope({super.key, required this.child, this.keep = true}); @override State createState() => _KeepContainerState(); diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 7580d6eb..c71bd537 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -1,3 +1,4 @@ +import 'package:animations/animations.dart'; import 'package:collection/collection.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -9,7 +10,6 @@ import 'package:flutter/material.dart'; import 'card.dart'; import 'input.dart'; -import 'open_container.dart'; import 'scaffold.dart'; import 'sheet.dart'; @@ -84,6 +84,8 @@ class InputDelegate extends Delegate { final String? suffixText; final Function(String? value) onChanged; final FormFieldValidator? validator; + final int? maxLength; + final TextInputType? keyboardType; final String? resetValue; @@ -94,6 +96,8 @@ class InputDelegate extends Delegate { required this.onChanged, this.resetValue, this.validator, + this.maxLength, + this.keyboardType, }); } @@ -299,10 +303,16 @@ class ListItem extends StatelessWidget { final child = openDelegate.widget; final onChanged = openDelegate.onChanged; return OpenContainer( - // closedColor: context.colorScheme.surface, - // openColor: context.colorScheme.surface, - // closedElevation: 0, - // openElevation: 0, + closedColor: context.colorScheme.surface, + openColor: context.colorScheme.surface, + closedElevation: 0, + openElevation: 0, + openShape: const RoundedSuperellipseBorder( + borderRadius: BorderRadius.zero, + ), + closedShape: const RoundedSuperellipseBorder( + borderRadius: BorderRadius.zero, + ), closedBuilder: (context, action) { Future openAction() async { final isMobile = globalState.container.read(isMobileViewProvider); @@ -379,6 +389,10 @@ class ListItem extends StatelessWidget { value: inputDelegate.value, suffixText: inputDelegate.suffixText, resetValue: inputDelegate.resetValue, + inputFormatters: inputDelegate.maxLength == null + ? null + : TextInputLimits.limit(inputDelegate.maxLength!), + keyboardType: inputDelegate.keyboardType, validator: inputDelegate.validator, ), ); diff --git a/lib/widgets/notification.dart b/lib/widgets/notification.dart index 5d871405..06361d52 100644 --- a/lib/widgets/notification.dart +++ b/lib/widgets/notification.dart @@ -17,14 +17,14 @@ class TextScaleNotification extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (_, ref, child) { - ref.listen( - themeSettingProvider.select((state) => state.textScale), - (prev, next) { - if (prev != next) { - onNotification(next); - } - }, - ); + ref.listen(themeSettingProvider.select((state) => state.textScale), ( + prev, + next, + ) { + if (prev != next) { + onNotification(next); + } + }); return child!; }, child: child, diff --git a/lib/widgets/palette.dart b/lib/widgets/palette.dart index 5504a910..0933f461 100644 --- a/lib/widgets/palette.dart +++ b/lib/widgets/palette.dart @@ -1,7 +1,12 @@ -import 'dart:math' as math; +import 'dart:math'; -import 'package:flutter/gestures.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/widgets/card.dart'; import 'package:flutter/material.dart'; +import 'package:material_color_utilities/hct/hct.dart'; + +import 'color_scheme_box.dart'; +import 'theme.dart'; @immutable class Palette extends StatefulWidget { @@ -14,120 +19,38 @@ class Palette extends StatefulWidget { } class _PaletteState extends State { - final double _thickness = 20; - final double _radius = 4; - final double _padding = 8; - final GlobalKey renderBoxKey = GlobalKey(); - bool isSquare = false; - bool isTrack = false; - late double colorHue; - late double colorSaturation; - late double colorValue; - - late FocusNode _focusNode; - - Color get value => widget.controller.value; - - HSVColor get color => HSVColor.fromColor(value); + double _hue = 0; + double _chroma = 0; + double _tone = 0; @override void initState() { super.initState(); - colorHue = color.hue; - colorSaturation = color.saturation; - colorValue = color.value; - _focusNode = FocusNode(); + _initFromColor(widget.controller.value); } - void _handleChange() { - widget.controller.value = HSVColor.fromAHSV( - color.alpha, - colorHue, - colorSaturation, - colorValue, - ).toColor(); + void _initFromColor(Color color) { + final hct = Hct.fromInt(color.toARGB32()); + _hue = hct.hue; + _chroma = hct.chroma; + _tone = hct.tone; } - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); + Color _toColor() => Color(Hct.from(_hue, _chroma, _tone).toInt()); + + void _onHueChanged(double value) { + setState(() => _hue = value); + widget.controller.value = _toColor(); } - double trackRadius(Size size) => - math.min(size.width, size.height) / 2 - _thickness; - - static double squareRadius(double radius, double trackSquarePadding) => - (radius - trackSquarePadding) / math.sqrt(2); - - void onStart(Offset offset) { - final RenderBox renderBox = - renderBoxKey.currentContext!.findRenderObject()! as RenderBox; - final size = renderBox.size; - final radius = trackRadius(size); - final radiusOuter = radius + _thickness; - final effectiveSquareRadius = squareRadius(radius, _padding); - final startPosition = renderBox.localToGlobal(Offset.zero); - final center = Offset(size.width / 2, size.height / 2); - final vector = offset - startPosition - center; - final vectorLength = _Computer.vectorLength(vector); - isSquare = - vector.dx.abs() < effectiveSquareRadius && - vector.dy.abs() < effectiveSquareRadius; - isTrack = vectorLength >= radius && vectorLength <= radiusOuter; - if (isSquare) { - colorSaturation = _Computer.vectorToSaturation( - vector.dx, - effectiveSquareRadius, - ).clamp(0.0, 1.0); - colorValue = _Computer.vectorToValue( - vector.dy, - effectiveSquareRadius, - ).clamp(0.0, 1.0); - _handleChange(); - } else if (isTrack) { - colorHue = _Computer.vectorToHue(vector); - _handleChange(); - } else { - isTrack = false; - isSquare = false; - } + void _onChromaChanged(double value) { + setState(() => _chroma = value); + widget.controller.value = _toColor(); } - void onUpdate(Offset offset) { - final RenderBox renderBox = - renderBoxKey.currentContext!.findRenderObject()! as RenderBox; - final size = renderBox.size; - final radius = trackRadius(size); - final effectiveSquareRadius = squareRadius(radius, _padding); - final startPosition = renderBox.localToGlobal(Offset.zero); - final center = Offset(size.width / 2, size.height / 2); - final vector = offset - startPosition - center; - if (isSquare) { - isTrack = false; - colorSaturation = _Computer.vectorToSaturation( - vector.dx, - effectiveSquareRadius, - ).clamp(0.0, 1.0); - colorValue = _Computer.vectorToValue( - vector.dy, - effectiveSquareRadius, - ).clamp(0.0, 1.0); - - _handleChange(); - } else if (isTrack) { - isSquare = false; - colorHue = _Computer.vectorToHue(vector); - _handleChange(); - } else { - isTrack = false; - isSquare = false; - } - } - - void onEnd() { - isTrack = false; - isSquare = false; + void _onToneSelected(double tone) { + setState(() => _tone = tone); + widget.controller.value = _toColor(); } @override @@ -135,321 +58,368 @@ class _PaletteState extends State { return ValueListenableBuilder( valueListenable: widget.controller, builder: (_, _, _) { - return GestureDetector( - dragStartBehavior: DragStartBehavior.down, - onVerticalDragDown: (DragDownDetails details) => - onStart(details.globalPosition), - onVerticalDragUpdate: (DragUpdateDetails details) => - onUpdate(details.globalPosition), - onHorizontalDragUpdate: (DragUpdateDetails details) => - onUpdate(details.globalPosition), - onVerticalDragEnd: (DragEndDetails details) => onEnd(), - onHorizontalDragEnd: (DragEndDetails details) => onEnd(), - onTapUp: (TapUpDetails details) => onEnd(), - child: SizedBox( - key: renderBoxKey, - child: Focus( - focusNode: _focusNode, - child: MouseRegion( - cursor: WidgetStateMouseCursor.clickable, - child: Stack( - fit: StackFit.expand, - children: [ - RepaintBoundary( - child: CustomPaint( - painter: _ShadePainter( - colorHue: colorHue, - colorSaturation: colorSaturation, - colorValue: colorValue, - thickness: _thickness, - padding: _padding, - trackBorderRadius: _radius, - ), - ), - ), - CustomPaint( - painter: _ShadeThumbPainter( - colorSaturation: colorSaturation, - colorValue: colorValue, - thickness: _thickness, - padding: _padding, - ), - ), - RepaintBoundary( - child: CustomPaint( - painter: _TrackPainter( - thickness: _thickness, - ticks: 360, - ), - ), - ), - CustomPaint( - painter: _TrackThumbPainter( - colorHue: colorHue, - thickness: _thickness, - ), - ), - ], - ), - ), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _HueSlider(hue: _hue, onChanged: _onHueChanged), + const SizedBox(height: 16), + _ChromaSlider( + hue: _hue, + chroma: _chroma, + onChanged: _onChromaChanged, ), - ), + const SizedBox(height: 28), + _ToneGrid( + hue: _hue, + chroma: _chroma, + selectedTone: _tone, + onToneSelected: _onToneSelected, + ), + const SizedBox(height: 16), + InfoHeader(info: Info(label: context.appLocalizations.preview)), + PrimaryColorBox( + primaryColor: widget.controller.value, + child: const _ColorSchemePreview(), + ), + ], ); }, ); } } -class _ShadePainter extends CustomPainter { - const _ShadePainter({ - required this.colorHue, - required this.colorSaturation, - required this.colorValue, - required this.thickness, - required this.padding, - required this.trackBorderRadius, - }) : super(); +class _HueSlider extends StatelessWidget { + const _HueSlider({required this.hue, required this.onChanged}); - final double colorHue; - final double colorSaturation; - final double colorValue; - - final double thickness; - final double padding; - final double trackBorderRadius; - - static double trackRadius(Size size, double trackWidth) => - math.min(size.width, size.height) / 2 - trackWidth / 2; - - static double squareRadius( - double radius, - double trackWidth, - double padding, - ) => (radius - trackWidth / 2 - padding) / math.sqrt(2); + final double hue; + final ValueChanged onChanged; @override - void paint(Canvas canvas, Size size) { - final Offset center = Offset(size.width / 2, size.height / 2); - final double radius = trackRadius(size, thickness); - final double effectiveSquareRadius = squareRadius( - radius, - thickness, - padding, + Widget build(BuildContext context) { + return SliderTheme( + data: SliderDefaultsM3(context).copyWith( + trackShape: _HueTrackShape(), + trackHeight: 24, + thumbSize: const WidgetStatePropertyAll(Size(6.0, 48.0)), + thumbColor: Color(Hct.from(hue, 100, 80).toInt()), + ), + child: Slider( + padding: EdgeInsets.zero, + value: hue, + min: 0, + max: 360, + onChanged: onChanged, + ), ); - - final Rect rectBox = Rect.fromLTWH( - center.dx - effectiveSquareRadius, - center.dy - effectiveSquareRadius, - effectiveSquareRadius * 2, - effectiveSquareRadius * 2, - ); - final RSuperellipse rSuperellipse = RSuperellipse.fromRectAndRadius( - rectBox, - Radius.circular(trackBorderRadius), - ); - - final Shader horizontal = LinearGradient( - colors: [ - Colors.white, - HSVColor.fromAHSV(1, colorHue, 1, 1).toColor(), - ], - ).createShader(rectBox); - canvas.drawRSuperellipse( - rSuperellipse, - Paint() - ..style = PaintingStyle.fill - ..shader = horizontal, - ); - - final Shader vertical = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.transparent, Colors.black], - ).createShader(rectBox); - canvas.drawRSuperellipse( - rSuperellipse, - Paint() - ..style = PaintingStyle.fill - ..shader = vertical, - ); - } - - @override - bool shouldRepaint(_ShadePainter oldDelegate) { - return oldDelegate.thickness != thickness || - oldDelegate.padding != padding || - oldDelegate.trackBorderRadius != trackBorderRadius || - oldDelegate.colorHue != colorHue || - oldDelegate.colorSaturation != colorSaturation || - oldDelegate.colorValue != colorValue; } } -class _TrackPainter extends CustomPainter { - const _TrackPainter({this.ticks = 360, required this.thickness}) : super(); - final int ticks; - final double thickness; +class _ChromaSlider extends StatelessWidget { + const _ChromaSlider({ + required this.hue, + required this.chroma, + required this.onChanged, + }); + + final double hue; + final double chroma; + final ValueChanged onChanged; @override - void paint(Canvas canvas, Size size) { - final Offset center = Offset(size.width / 2, size.height / 2); - - const double rads = (2 * math.pi) / 360; - const double step = 1; - const double aliasing = 0.5; - - final double shortestRectSide = math.min(size.width, size.height); - - final Rect rectCircle = Rect.fromCenter( - center: center, - width: shortestRectSide - thickness, - height: shortestRectSide - thickness, + Widget build(BuildContext context) { + return SliderTheme( + data: SliderDefaultsM3(context).copyWith( + trackShape: _ChromaTrackShape(hue: hue), + trackHeight: 24, + thumbSize: const WidgetStatePropertyAll(Size(6.0, 48.0)), + thumbColor: Color(Hct.from(hue, chroma, 80).toInt()), + ), + child: Slider( + padding: EdgeInsets.zero, + value: chroma.clamp(0, 10), + min: 0, + max: 10, + onChanged: onChanged, + ), ); + } +} - for (int i = 0; i < ticks; i++) { - final double sRad = (i - aliasing) * rads; - final double eRad = (i + step) * rads; - final Paint segmentPaint = Paint() - ..color = HSVColor.fromAHSV(1, i.toDouble(), 1, 1).toColor() - ..style = PaintingStyle.stroke - ..strokeWidth = thickness; - canvas.drawArc(rectCircle, sRad, sRad - eRad, false, segmentPaint); +class _HueTrackShape extends SliderTrackShape { + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final trackHeight = sliderTheme.trackHeight ?? 24; + final trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2; + return Rect.fromLTWH( + offset.dx, + trackTop, + parentBox.size.width, + trackHeight, + ); + } + + @override + void paint( + PaintingContext context, + Offset offset, { + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required Animation enableAnimation, + required TextDirection textDirection, + required Offset thumbCenter, + Offset? secondaryOffset, + bool isDiscrete = false, + bool isEnabled = false, + }) { + final rect = getPreferredRect( + parentBox: parentBox, + offset: offset, + sliderTheme: sliderTheme, + isEnabled: isEnabled, + isDiscrete: isDiscrete, + ); + final colors = []; + for (int i = 0; i <= 360; i += 10) { + colors.add(Color(Hct.from(i.toDouble(), 100, 60).toInt())); } - } - - @override - bool shouldRepaint(_TrackPainter oldDelegate) { - return oldDelegate.thickness != thickness || oldDelegate.ticks != ticks; + final shader = LinearGradient(colors: colors).createShader(rect); + final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(12)); + context.canvas.drawRRect(rrect, Paint()..shader = shader); + _paintTrackHighlight(context.canvas, rect); } } -class _ShadeThumbPainter extends CustomPainter { - const _ShadeThumbPainter({ - required this.colorSaturation, - required this.colorValue, - required this.thickness, - required this.padding, - }) : super(); +class _ChromaTrackShape extends SliderTrackShape { + const _ChromaTrackShape({required this.hue}); - final double colorSaturation; - final double colorValue; - final double thickness; - final double padding; - - static double trackRadius(Size size, double thickness) => - math.min(size.width, size.height) / 2 - thickness / 2; - - static double squareRadius( - double radius, - double thickness, - double trackSquarePadding, - ) => (radius - thickness / 2 - trackSquarePadding) / math.sqrt(2); + final double hue; @override - void paint(Canvas canvas, Size size) { - final Offset center = Offset(size.width / 2, size.height / 2); - final double radius = trackRadius(size, thickness); - final double effectiveSquareRadius = squareRadius( - radius, - thickness, - padding, + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final trackHeight = sliderTheme.trackHeight ?? 24; + final trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2; + return Rect.fromLTWH( + offset.dx, + trackTop, + parentBox.size.width, + trackHeight, ); - - final Paint paintBlack = Paint() - ..color = Colors.black - ..strokeWidth = 5 - ..style = PaintingStyle.stroke; - final Paint paintWhite = Paint() - ..color = Colors.white - ..strokeWidth = 3 - ..style = PaintingStyle.stroke; - - final double paletteX = _Computer.saturationToVector( - colorSaturation, - effectiveSquareRadius, - center.dx, - ); - final double paletteY = _Computer.valueToVector( - colorValue, - effectiveSquareRadius, - center.dy, - ); - final Offset paletteVector = Offset(paletteX, paletteY); - canvas.drawCircle(paletteVector, 12, paintBlack); - canvas.drawCircle(paletteVector, 12, paintWhite); } @override - bool shouldRepaint(_ShadeThumbPainter oldDelegate) { - return oldDelegate.thickness != thickness || - oldDelegate.colorSaturation != colorSaturation || - oldDelegate.colorValue != colorValue || - oldDelegate.padding != padding; + void paint( + PaintingContext context, + Offset offset, { + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required Animation enableAnimation, + required TextDirection textDirection, + required Offset thumbCenter, + Offset? secondaryOffset, + bool isDiscrete = false, + bool isEnabled = false, + }) { + final rect = getPreferredRect( + parentBox: parentBox, + offset: offset, + sliderTheme: sliderTheme, + isEnabled: isEnabled, + isDiscrete: isDiscrete, + ); + final colors = []; + for (int i = 0; i <= 49; i++) { + colors.add(Color(Hct.from(hue, (i / 49) * 150, 60).toInt())); + } + final shader = LinearGradient(colors: colors).createShader(rect); + final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(12)); + context.canvas.drawRRect(rrect, Paint()..shader = shader); + _paintTrackHighlight(context.canvas, rect); } } -class _TrackThumbPainter extends CustomPainter { - const _TrackThumbPainter({required this.colorHue, required this.thickness}) - : super(); - - final double colorHue; - final double thickness; - - static double trackRadius(Size size, double thickness) => - math.min(size.width, size.height) / 2 - thickness / 2; - - @override - void paint(Canvas canvas, Size size) { - final Offset center = Offset(size.width / 2, size.height / 2); - final double radius = trackRadius(size, thickness); - final Paint paintBlack = Paint() - ..color = Colors.black - ..strokeWidth = 5 - ..style = PaintingStyle.stroke; - final Paint paintWhite = Paint() - ..color = Colors.white - ..strokeWidth = 3 - ..style = PaintingStyle.stroke; - final Offset track = _Computer.hueToVector( - (colorHue + 360.0) * math.pi / 180.0, - radius, - center, - ); - canvas.drawCircle(track, thickness / 2 + 4, paintBlack); - canvas.drawCircle(track, thickness / 2 + 4, paintWhite); - } - - @override - bool shouldRepaint(_TrackThumbPainter oldDelegate) { - return oldDelegate.thickness != thickness || - oldDelegate.colorHue != colorHue; - } -} - -class _Computer { - static double vectorLength(Offset vector) => - math.sqrt(vector.dx * vector.dx + vector.dy * vector.dy); - - static double vectorToHue(Offset vector) => - (((math.atan2(vector.dy, vector.dx)) * 180.0 / math.pi) + 360.0) % 360.0; - - static double vectorToSaturation(double vectorX, double squareRadius) => - vectorX * 0.5 / squareRadius + 0.5; - - static double vectorToValue(double vectorY, double squareRadius) => - 0.5 - vectorY * 0.5 / squareRadius; - - static Offset hueToVector(double h, double radius, Offset center) => Offset( - math.cos(h) * radius + center.dx, - math.sin(h) * radius + center.dy, +void _paintTrackHighlight(Canvas canvas, Rect rect) { + final rrect = RRect.fromRectAndRadius( + rect.deflate(1), + const Radius.circular(12), + ); + canvas.drawRRect( + rrect, + Paint() + ..color = Colors.white.withValues(alpha: 0.35) + ..style = PaintingStyle.stroke + ..strokeWidth = 1, + ); + canvas.drawLine( + Offset(rect.left + 12, rect.bottom - 2), + Offset(rect.right - 12, rect.bottom - 2), + Paint() + ..color = Colors.black.withValues(alpha: 0.12) + ..strokeWidth = 1, ); - - static double saturationToVector( - double s, - double squareRadius, - double centerX, - ) => (s - 0.5) * squareRadius / 0.5 + centerX; - - static double valueToVector(double l, double squareRadius, double centerY) => - (0.5 - l) * squareRadius / 0.5 + centerY; +} + +class _ToneGrid extends StatelessWidget { + const _ToneGrid({ + required this.hue, + required this.chroma, + required this.selectedTone, + required this.onToneSelected, + }); + + final double hue; + final double chroma; + final double selectedTone; + final ValueChanged onToneSelected; + + static const _tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; + static const _spacing = 8.0; + + @override + Widget build(BuildContext context) { + final selectedBorderColor = Theme.of(context).colorScheme.primary; + return LayoutBuilder( + builder: (_, constraints) { + final columns = min( + max((constraints.maxWidth / 40).floor(), 1), + _tones.length, + ); + final itemWidth = + (constraints.maxWidth - (columns - 1) * _spacing) / columns; + return Wrap( + spacing: _spacing, + runSpacing: _spacing, + children: _tones.map((tone) { + final color = Color(Hct.from(hue, chroma, tone.toDouble()).toInt()); + final isSelected = tone == selectedTone.round(); + final textColor = tone <= 50 ? Colors.white : Colors.black; + return GestureDetector( + onTap: () => onToneSelected(tone.toDouble()), + child: SizedBox( + width: itemWidth, + height: itemWidth, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + '$tone', + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (isSelected) + Positioned.fill( + top: -4, + right: -4, + bottom: -4, + left: -4, + child: IgnorePointer( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selectedBorderColor, + width: 4, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }).toList(), + ); + }, + ); + } +} + +class _ColorSchemePreview extends StatelessWidget { + const _ColorSchemePreview(); + + static const _spacing = 8.0; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final roles = [ + (colorScheme.primary, colorScheme.onPrimary, 'Primary'), + (colorScheme.secondary, colorScheme.onSecondary, 'Secondary'), + (colorScheme.tertiary, colorScheme.onTertiary, 'Tertiary'), + (colorScheme.error, colorScheme.onError, 'Error'), + (colorScheme.surface, colorScheme.onSurface, 'Surface'), + ( + colorScheme.primaryContainer, + colorScheme.onPrimaryContainer, + 'Primary\nCont.', + ), + ( + colorScheme.secondaryContainer, + colorScheme.onSecondaryContainer, + 'Secondary\nCont.', + ), + ( + colorScheme.tertiaryContainer, + colorScheme.onTertiaryContainer, + 'Tertiary\nCont.', + ), + ]; + return LayoutBuilder( + builder: (_, constraints) { + final columns = min( + max((constraints.maxWidth / 68).floor(), 1), + roles.length, + ); + final itemWidth = + (constraints.maxWidth - (columns - 1) * _spacing) / columns; + return Wrap( + spacing: _spacing, + runSpacing: _spacing, + children: [ + for (final (bg, fg, label) in roles) + Container( + width: itemWidth, + height: 44, + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + style: TextStyle(color: fg, fontSize: 9), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + }, + ); + } } diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index fa50e90a..10ae08cb 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -203,6 +203,7 @@ class CommonScaffoldState extends State { ? TextField( autofocus: true, controller: _textController, + inputFormatters: TextInputLimits.limit(TextInputLimits.search), style: context.textTheme.titleLarge, onChanged: (value) { if (startState != null) { diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index d81d0b20..a9475822 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/providers/app.dart'; @@ -252,7 +250,10 @@ class _AdaptiveSheetScaffoldState extends State { ), ), ), - Padding(padding: const EdgeInsets.symmetric(horizontal: 4), child: appBar), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: appBar, + ), const SizedBox(height: 6), ], ); @@ -290,10 +291,7 @@ class _AdaptiveSheetScaffoldState extends State { top: Radius.circular(28), ), child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 12.0, - sigmaY: 12.0, - ), + filter: commonFilter, child: ColoredBox( color: isScrolled ? backgroundColor.opacity60 diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index 6aa4847e..fba78ae9 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -41,3 +41,111 @@ class CommonMinIconButtonTheme extends StatelessWidget { ); } } + +class SliderDefaultsM3 extends SliderThemeData { + SliderDefaultsM3(this.context) : super(trackHeight: 16.0); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + Color? get activeTrackColor => _colors.primary; + + @override + Color? get inactiveTrackColor => _colors.secondaryContainer; + + @override + Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54); + + @override + Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12); + + @override + Color? get disabledSecondaryActiveTrackColor => + _colors.onSurface.withOpacity(0.38); + + @override + Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0); + + @override + Color? get inactiveTickMarkColor => + _colors.onSecondaryContainer.withOpacity(1.0); + + @override + Color? get disabledActiveTickMarkColor => _colors.onInverseSurface; + + @override + Color? get disabledInactiveTickMarkColor => _colors.onSurface; + + @override + Color? get thumbColor => _colors.primary; + + @override + Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get overlayColor => + WidgetStateColor.resolveWith((Set states) { + if (states.contains(WidgetState.dragged)) { + return _colors.primary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withOpacity(0.1); + } + + return Colors.transparent; + }); + + @override + TextStyle? get valueIndicatorTextStyle => Theme.of( + context, + ).textTheme.labelLarge!.copyWith(color: _colors.onInverseSurface); + + @override + Color? get valueIndicatorColor => _colors.inverseSurface; + + @override + SliderComponentShape? get valueIndicatorShape => + const RoundedRectSliderValueIndicatorShape(); + + @override + SliderComponentShape? get thumbShape => const HandleThumbShape(); + + @override + SliderTrackShape? get trackShape => const GappedSliderTrackShape(); + + @override + SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); + + @override + SliderTickMarkShape? get tickMarkShape => + const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2); + + @override + WidgetStateProperty? get thumbSize { + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return const Size(4.0, 44.0); + } + if (states.contains(WidgetState.hovered)) { + return const Size(4.0, 44.0); + } + if (states.contains(WidgetState.focused)) { + return const Size(2.0, 44.0); + } + if (states.contains(WidgetState.pressed)) { + return const Size(2.0, 44.0); + } + return const Size(4.0, 44.0); + }); + } + + @override + double? get trackGap => 6.0; +} diff --git a/lib/widgets/wave.dart b/lib/widgets/wave.dart index f9c7ef3d..af16c899 100644 --- a/lib/widgets/wave.dart +++ b/lib/widgets/wave.dart @@ -27,10 +27,8 @@ class _WaveViewState extends State @override void initState() { super.initState(); - _controller = AnimationController( - vsync: this, - duration: widget.duration, - )..repeat(); + _controller = AnimationController(vsync: this, duration: widget.duration) + ..repeat(); } @override @@ -41,27 +39,26 @@ class _WaveViewState extends State @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (_, constraints) { - return RepaintBoundary( - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return CustomPaint( - painter: WavePainter( - animationValue: _controller.value, - waveAmplitude: widget.waveAmplitude, - waveFrequency: widget.waveFrequency, - waveColor: widget.waveColor, - ), - size: Size( - constraints.maxWidth, - constraints.maxHeight, - ), - ); - }, - ), - ); - }); + return LayoutBuilder( + builder: (_, constraints) { + return RepaintBoundary( + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + painter: WavePainter( + animationValue: _controller.value, + waveAmplitude: widget.waveAmplitude, + waveFrequency: widget.waveFrequency, + waveColor: widget.waveColor, + ), + size: Size(constraints.maxWidth, constraints.maxHeight), + ); + }, + ), + ); + }, + ); } } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ae80e6e0..611f1624 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,27 +8,15 @@ PODS: - HotKey - rust_api (0.0.1): - FlutterMacOS - - screen_retriever_macos (0.0.1): - - FlutterMacOS - setup (0.0.1): - FlutterMacOS - - tray_manager (0.0.1): - - FlutterMacOS - - wifi_ssid (0.0.1): - - FlutterMacOS - - window_ext (0.0.1): - - FlutterMacOS DEPENDENCIES: - flutter_js (from `Flutter/ephemeral/.symlinks/plugins/flutter_js/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - rust_api (from `Flutter/ephemeral/.symlinks/plugins/rust_api/macos`) - - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - setup (from `Flutter/ephemeral/.symlinks/plugins/setup/macos`) - - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - - wifi_ssid (from `Flutter/ephemeral/.symlinks/plugins/wifi_ssid/macos`) - - window_ext (from `Flutter/ephemeral/.symlinks/plugins/window_ext/macos`) SPEC REPOS: trunk: @@ -43,16 +31,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos rust_api: :path: Flutter/ephemeral/.symlinks/plugins/rust_api/macos - screen_retriever_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos setup: :path: Flutter/ephemeral/.symlinks/plugins/setup/macos - tray_manager: - :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos - wifi_ssid: - :path: Flutter/ephemeral/.symlinks/plugins/wifi_ssid/macos - window_ext: - :path: Flutter/ephemeral/.symlinks/plugins/window_ext/macos SPEC CHECKSUMS: flutter_js: 79c3c70d33ba464c67d8eb88639a61aa718cd8b8 @@ -60,11 +40,7 @@ SPEC CHECKSUMS: HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe rust_api: 112f3cfd993afe61c6662bdb30fd6e3509fb3bf8 - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f setup: 8b838da8e99b84b526ac668469e84de76f32fb74 - tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166 - wifi_ssid: e8c7174872311b511ddfb86996e370a20739e4ad - window_ext: 4afef727fe428b30c68ce800ba92e890fd329f63 PODFILE CHECKSUM: b9facb167d6b7e90fef40306c3fe521e010924ac diff --git a/opencode.jsonc b/opencode.jsonc new file mode 100644 index 00000000..2c2509a0 --- /dev/null +++ b/opencode.jsonc @@ -0,0 +1,14 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "codegraph": { + "type": "local", + "command": [ + "codegraph", + "serve", + "--mcp" + ], + "enabled": true + } + } +} diff --git a/plugins/flutter_distributor b/plugins/flutter_distributor deleted file mode 160000 index cdeeef2d..00000000 --- a/plugins/flutter_distributor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cdeeef2d8f8325bb6ae0bc86b39f56e4325d1a58 diff --git a/plugins/rust_api/android/build.gradle b/plugins/rust_api/android/build.gradle index 75d7640b..a74dad19 100644 --- a/plugins/rust_api/android/build.gradle +++ b/plugins/rust_api/android/build.gradle @@ -29,23 +29,16 @@ android { namespace 'com.flutter_rust_bridge.rust_api' } - // Bumping the plugin compileSdkVersion requires all clients of this plugin - // to bump the version in their app. - compileSdkVersion 33 - - // Use the NDK version - // declared in /android/app/build.gradle file of the Flutter project. - // Replace it with a version number if this plugin requires a specfic NDK version. - // (e.g. ndkVersion "23.1.7779620") + compileSdkVersion 36 ndkVersion android.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } defaultConfig { - minSdkVersion 19 + minSdkVersion 23 } } diff --git a/plugins/setup/android/build.gradle b/plugins/setup/android/build.gradle index fd1e3d6b..84ba8bfb 100644 --- a/plugins/setup/android/build.gradle +++ b/plugins/setup/android/build.gradle @@ -24,16 +24,16 @@ android { if (project.android.hasProperty("namespace")) { namespace 'com.follow.clash.setup' } - compileSdkVersion 33 + compileSdkVersion 36 ndkVersion android.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } defaultConfig { - minSdkVersion 19 + minSdkVersion 23 } } diff --git a/plugins/setup/buildkit/build_tool/lib/src/rust_builder.dart b/plugins/setup/buildkit/build_tool/lib/src/rust_builder.dart index 78a38f63..308f5578 100644 --- a/plugins/setup/buildkit/build_tool/lib/src/rust_builder.dart +++ b/plugins/setup/buildkit/build_tool/lib/src/rust_builder.dart @@ -17,7 +17,8 @@ class RustBuilder { String get _helperPath => p.join(rootDir, config.helperDir); String get _outputPath => p.join(rootDir, config.outputDir); - Future build(Target target, String token, {bool release = true}) async { + Future build(Target target, String token, + {bool release = true}) async { final args = ['build']; if (release) { args.addAll(['--release', '--features', 'windows-service']); @@ -32,7 +33,9 @@ class RustBuilder { workingDirectory: _helperPath, environment: env); final srcPath = p.join( - _helperPath, 'target', release ? 'release' : 'debug', + _helperPath, + 'target', + release ? 'release' : 'debug', 'helper${target.executableExtension}', ); final destDir = p.join(_outputPath, target.goos); diff --git a/plugins/setup/buildkit/build_tool/lib/src/util.dart b/plugins/setup/buildkit/build_tool/lib/src/util.dart index 4ea7c86d..0616e62a 100644 --- a/plugins/setup/buildkit/build_tool/lib/src/util.dart +++ b/plugins/setup/buildkit/build_tool/lib/src/util.dart @@ -109,7 +109,6 @@ void copyFile(String source, String destination) { _log.fine('Copied $source -> $destination'); } -String joinPath(String part1, - [String? part2, String? part3, String? part4]) { +String joinPath(String part1, [String? part2, String? part3, String? part4]) { return p.join(part1, part2, part3, part4); } diff --git a/plugins/setup/buildkit/build_tool/test/util_test.dart b/plugins/setup/buildkit/build_tool/test/util_test.dart index 1e275c54..4ecaec57 100644 --- a/plugins/setup/buildkit/build_tool/test/util_test.dart +++ b/plugins/setup/buildkit/build_tool/test/util_test.dart @@ -44,8 +44,7 @@ void main() { group('copyFile', () { test('copies file to destination', () { - final src = - File('${Directory.systemTemp.path}/build_tool_test_src.txt'); + final src = File('${Directory.systemTemp.path}/build_tool_test_src.txt'); final dest = '${Directory.systemTemp.path}/build_tool_test_dest/sub/file.txt'; src.writeAsStringSync('content'); diff --git a/plugins/setup/buildkit/run_build_tool.cmd b/plugins/setup/buildkit/run_build_tool.cmd index f65d9b6c..c152533d 100644 --- a/plugins/setup/buildkit/run_build_tool.cmd +++ b/plugins/setup/buildkit/run_build_tool.cmd @@ -3,6 +3,8 @@ setlocal setlocal ENABLEDELAYEDEXPANSION +echo warning: [setup] Plugin triggered + SET BASEDIR=%~dp0 SET PROJECT_DIR=%CD% diff --git a/plugins/setup/buildkit/run_build_tool.sh b/plugins/setup/buildkit/run_build_tool.sh index 0af383f2..6b7e6ad3 100755 --- a/plugins/setup/buildkit/run_build_tool.sh +++ b/plugins/setup/buildkit/run_build_tool.sh @@ -2,6 +2,8 @@ set -e +echo "warning: [setup] Plugin triggered" + BASEDIR=$(cd "$(dirname "$0")" && pwd) PROJECT_DIR="${PROJECT_DIR:-$(pwd)}" diff --git a/plugins/tray_manager b/plugins/tray_manager deleted file mode 160000 index 6163dc8f..00000000 --- a/plugins/tray_manager +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6163dc8f62f6f177a85072b0e1e2eee76fed54f0 diff --git a/plugins/wifi_ssid/android/build.gradle b/plugins/wifi_ssid/android/build.gradle index 70cfc101..aa0e3f32 100644 --- a/plugins/wifi_ssid/android/build.gradle +++ b/plugins/wifi_ssid/android/build.gradle @@ -9,7 +9,6 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.12.2' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10' } } @@ -21,7 +20,6 @@ allprojects { } apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' android { namespace 'com.example.wifi_ssid' @@ -32,15 +30,17 @@ android { targetCompatibility JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = '17' - } - defaultConfig { minSdk 21 } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + dependencies { implementation 'androidx.core:core-ktx:1.15.0' } diff --git a/plugins/wifi_ssid/macos/wifi_ssid.podspec b/plugins/wifi_ssid/macos/wifi_ssid.podspec deleted file mode 100644 index 01e9a4ab..00000000 --- a/plugins/wifi_ssid/macos/wifi_ssid.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'wifi_ssid' - s.version = '0.0.1' - s.summary = 'A Flutter plugin to retrieve the current WiFi SSID.' - s.description = <<-DESC -A Flutter plugin to retrieve the current WiFi SSID. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' - s.frameworks = 'CoreWLAN', 'CoreLocation' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' -end diff --git a/plugins/wifi_ssid/macos/wifi_ssid/Package.swift b/plugins/wifi_ssid/macos/wifi_ssid/Package.swift new file mode 100644 index 00000000..74319af3 --- /dev/null +++ b/plugins/wifi_ssid/macos/wifi_ssid/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "wifi_ssid", + platforms: [ + .macOS(.v11) + ], + products: [ + .library(name: "wifi-ssid", targets: ["wifi_ssid"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "wifi_ssid", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework") + ], + path: "Sources/wifi_ssid", + linkerSettings: [ + .linkedFramework("CoreWLAN"), + .linkedFramework("CoreLocation") + ] + ) + ] +) diff --git a/plugins/wifi_ssid/macos/Classes/WifiSsidPlugin.swift b/plugins/wifi_ssid/macos/wifi_ssid/Sources/wifi_ssid/WifiSsidPlugin.swift similarity index 100% rename from plugins/wifi_ssid/macos/Classes/WifiSsidPlugin.swift rename to plugins/wifi_ssid/macos/wifi_ssid/Sources/wifi_ssid/WifiSsidPlugin.swift index 05955970..9f9d9b70 100644 --- a/plugins/wifi_ssid/macos/Classes/WifiSsidPlugin.swift +++ b/plugins/wifi_ssid/macos/wifi_ssid/Sources/wifi_ssid/WifiSsidPlugin.swift @@ -1,7 +1,7 @@ import Cocoa -import FlutterMacOS -import CoreWLAN import CoreLocation +import CoreWLAN +import FlutterMacOS // Permission values must match WifiSsidPermission enum index in Dart: // 0 = granted, 1 = denied, 2 = permanentlyDenied diff --git a/plugins/window_ext/lib/window_ext.dart b/plugins/window_ext/lib/window_ext.dart index a1c74b51..2b115b29 100644 --- a/plugins/window_ext/lib/window_ext.dart +++ b/plugins/window_ext/lib/window_ext.dart @@ -1,2 +1,2 @@ export 'window_ext_listener.dart'; -export 'window_ext_manager.dart'; \ No newline at end of file +export 'window_ext_manager.dart'; diff --git a/plugins/window_ext/macos/window_ext.podspec b/plugins/window_ext/macos/window_ext.podspec deleted file mode 100644 index fcf85805..00000000 --- a/plugins/window_ext/macos/window_ext.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint window_ext.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'window_ext' - s.version = '0.0.1' - s.summary = 'A new Flutter plugin project.' - s.description = <<-DESC -A new Flutter plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' -end diff --git a/plugins/window_ext/macos/window_ext/Package.swift b/plugins/window_ext/macos/window_ext/Package.swift new file mode 100644 index 00000000..48c2939c --- /dev/null +++ b/plugins/window_ext/macos/window_ext/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "window_ext", + platforms: [ + .macOS("10.13") + ], + products: [ + .library(name: "window-ext", targets: ["window_ext"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "window_ext", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework") + ], + path: "Sources/window_ext" + ) + ] +) diff --git a/plugins/window_ext/macos/Classes/WindowExtPlugin.swift b/plugins/window_ext/macos/window_ext/Sources/window_ext/WindowExtPlugin.swift similarity index 87% rename from plugins/window_ext/macos/Classes/WindowExtPlugin.swift rename to plugins/window_ext/macos/window_ext/Sources/window_ext/WindowExtPlugin.swift index b659b3ed..5e477dd9 100644 --- a/plugins/window_ext/macos/Classes/WindowExtPlugin.swift +++ b/plugins/window_ext/macos/window_ext/Sources/window_ext/WindowExtPlugin.swift @@ -2,24 +2,24 @@ import Cocoa import FlutterMacOS public class WindowExtPlugin: NSObject, FlutterPlugin { - public static var instance:WindowExtPlugin? - + public static var instance: WindowExtPlugin? + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "window_ext", binaryMessenger: registrar.messenger) instance = WindowExtPlugin(registrar, channel) registrar.addMethodCallDelegate(instance!, channel: channel) } - + private var registrar: FlutterPluginRegistrar! private var channel: FlutterMethodChannel! - + public init(_ registrar: FlutterPluginRegistrar, _ channel: FlutterMethodChannel) { super.init() self.registrar = registrar self.channel = channel } - - public func handleShouldTerminate(){ + + public func handleShouldTerminate() { channel.invokeMethod("shouldTerminate", arguments: nil) } } diff --git a/pubspec.lock b/pubspec.lock index fbdaed33..78082c16 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" + sha256: "4ec328cd9fd51fd0e7eb8870a9612c1fde0f091901932238c4f4e60b5f7f1315" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.1.2" app_links_linux: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + sha256: "78a18580eecac98108d1eef52a7db668bc317714f5205e616973363326efe333" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" app_links_web: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" code_builder: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c" + sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.2.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.15.1" cross_file: dependency: transitive description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.14" defer_pointer: dependency: "direct main" description: @@ -317,18 +317,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd + sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65" url: "https://pub.dev" source: hosted - version: "12.4.0" + version: "13.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" url: "https://pub.dev" source: hosted - version: "7.0.3" + version: "8.1.0" dio: dependency: "direct main" description: @@ -409,6 +409,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -421,10 +429,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 + sha256: fdc6a37f715d19f35b131decf1ce39242eeed5ddae18c0818c3eccb731ab76be url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "12.0.0-beta.7" file_selector_linux: dependency: transitive description: @@ -481,12 +489,11 @@ packages: flutter_js: dependency: "direct main" description: - path: "." - ref: master - resolved-ref: c0e10a5dab0e22cd616ec92ae71471d76369cbfe - url: "https://github.com/chen08209/flutter_js" - source: git - version: "0.8.3" + name: flutter_js + sha256: "046428059ac7bef71e305dc35fc24be933b684684a43e83eab7ebd0dc407ad85" + url: "https://pub.dev" + source: hosted + version: "0.8.7" flutter_lints: dependency: "direct dev" description: @@ -504,10 +511,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" url: "https://pub.dev" source: hosted - version: "2.0.34" + version: "2.0.35" flutter_riverpod: dependency: "direct main" description: @@ -528,10 +535,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -594,18 +601,18 @@ packages: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" hooks: dependency: transitive description: name: hooks - sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.2" hotkey_manager: dependency: "direct main" description: @@ -682,10 +689,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" url: "https://pub.dev" source: hosted - version: "0.8.13+17" + version: "0.8.13+19" image_picker_for_web: dependency: transitive description: @@ -809,10 +816,11 @@ packages: launch_at_startup: dependency: "direct main" description: - name: launch_at_startup - sha256: "7db33398b76ec0ed9e27f9f4640553e239977437564046625e215be89c91f084" - url: "https://pub.dev" - source: hosted + path: "." + ref: main + resolved-ref: e930ce65c5804343103447e01d39fccabedc8681 + url: "https://github.com/chen08209/launch_at_startup.git" + source: git version: "0.5.1" leak_tracker: dependency: transitive @@ -882,10 +890,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -918,14 +926,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" navigator_resizable: dependency: transitive description: @@ -954,10 +954,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.1" package_config: dependency: transitive description: @@ -970,18 +970,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" url: "https://pub.dev" source: hosted - version: "9.0.1" + version: "10.1.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "4.1.0" path: dependency: "direct main" description: @@ -1002,10 +1002,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_android: dependency: transitive description: @@ -1034,10 +1034,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -1112,12 +1112,11 @@ packages: re_editor: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: "8f1c28514cc1730cffd5b927e715e8b83cc417c1" - url: "https://github.com/chen08209/re-editor" - source: git - version: "0.8.0" + name: re_editor + sha256: "73e5daf7041b382c07ed707d5efd2d0a53851bb9eb6d6987260229ae0ed2f458" + url: "https://pub.dev" + source: hosted + version: "0.9.0" re_highlight: dependency: "direct main" description: @@ -1209,42 +1208,42 @@ packages: dependency: "direct main" description: name: screen_retriever - sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_linux: dependency: transitive description: name: screen_retriever_linux - sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_macos: dependency: transitive description: name: screen_retriever_macos - sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_platform_interface: dependency: transitive description: name: screen_retriever_platform_interface - sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_windows: dependency: transitive description: name: screen_retriever_windows - sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" setup: dependency: "direct main" description: @@ -1264,10 +1263,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.26" shared_preferences_foundation: dependency: transitive description: @@ -1405,42 +1404,42 @@ packages: dependency: transitive description: name: sqflite - sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" + sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082" url: "https://pub.dev" source: hosted - version: "2.4.2+1" + version: "2.4.3" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b url: "https://pub.dev" source: hosted - version: "2.4.2+3" + version: "2.4.3" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "5e8377564d95166761a968ed96104e0569b6b6cc611faac92a36ab8a169112c3" + sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590" url: "https://pub.dev" source: hosted - version: "2.5.6+1" + version: "2.5.11" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + sha256: "164a5d73ab87a134566057219988bafde837029a64264e61f1f04376ef3cfcd2" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" sqflite_platform_interface: dependency: transitive description: name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" sqlite3: dependency: transitive description: @@ -1525,10 +1524,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" url: "https://pub.dev" source: hosted - version: "3.4.0+1" + version: "3.4.1" term_glyph: dependency: transitive description: @@ -1541,33 +1540,35 @@ packages: dependency: "direct dev" description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" tray_manager: dependency: "direct main" description: - path: "plugins/tray_manager/packages/tray_manager" - relative: true - source: path - version: "0.5.2" + path: "packages/tray_manager" + ref: main + resolved-ref: da2d83dd1cc3c9709bf708ff827ad9072504ce52 + url: "https://github.com/chen08209/tray_manager.git" + source: git + version: "0.5.3" typed_data: dependency: transitive description: @@ -1596,10 +1597,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.32" url_launcher_ios: dependency: transitive description: @@ -1636,10 +1637,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -1660,10 +1661,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "6409a25046024f0f8c5d8a59fec314081e81f9d436b66ca4015a8b49772bf445" + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" vector_graphics_codec: dependency: transitive description: @@ -1676,10 +1677,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.6" vector_math: dependency: transitive description: @@ -1755,18 +1756,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 url: "https://pub.dev" source: hosted - version: "5.15.0" + version: "6.3.0" win32_registry: dependency: "direct main" description: name: win32_registry - sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.3" window_ext: dependency: "direct main" description: @@ -1825,5 +1826,5 @@ packages: source: git version: "2.1.0" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml old mode 100755 new mode 100644 index 15ac08ea..1e5d8f80 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.93+2026052901 +version: 0.8.94+2026071102 environment: sdk: '>=3.8.0 <4.0.0' @@ -24,19 +24,25 @@ dependencies: path: plugins/proxy window_ext: path: plugins/window_ext - launch_at_startup: ^0.5.1 + launch_at_startup: + git: + url: https://github.com/chen08209/launch_at_startup.git + ref: main json_annotation: ^4.11.0 - file_picker: ^11.0.2 - mobile_scanner: ^7.0.1 - app_links: ^7.0.0 - win32_registry: ^2.0.0 + file_picker: ^12.0.0-beta.7 + mobile_scanner: ^7.2.0 + app_links: ^7.1.2 + win32_registry: ^3.0.3 tray_manager: - path: plugins/tray_manager/packages/tray_manager + git: + url: https://github.com/chen08209/tray_manager.git + ref: main + path: packages/tray_manager wifi_ssid: path: plugins/wifi_ssid collection: ^1.19.1 animations: ^2.1.1 - package_info_plus: ^9.0.0 + package_info_plus: ^10.1.0 url_launcher: ^6.3.2 freezed_annotation: ^3.1.0 image_picker: ^1.2.0 @@ -44,28 +50,22 @@ dependencies: webdav_client: ^1.2.2 dio: ^5.8.0+1 - win32: ^5.5.1 - re_editor: - git: - url: https://github.com/chen08209/re-editor - ref: main + win32: ^6.0.1 + re_editor: ^0.9.0 re_highlight: ^0.0.3 archive: ^4.0.7 emoji_regex: ^0.0.5 hotkey_manager: ^0.2.3 uni_platform: ^0.1.3 - device_info_plus: ^12.2.0 - connectivity_plus: ^7.0.0 - screen_retriever: ^0.2.0 + device_info_plus: ^13.1.0 + connectivity_plus: ^7.2.0 + screen_retriever: ^0.2.1 defer_pointer: ^0.0.2 flutter_riverpod: ^3.3.1 riverpod_annotation: ^4.0.2 riverpod: ^3.2.1 material_color_utilities: ^0.13.0 - flutter_js: - git: - url: https://github.com/chen08209/flutter_js - ref: master + flutter_js: ^0.8.7 flutter_svg: ^2.1.0 flutter_cache_manager: ^3.4.1 crypto: ^3.0.3 @@ -122,4 +122,4 @@ flutter_intl: enabled: true class_name: AppLocalizations arb_dir: arb - output_dir: lib/l10n \ No newline at end of file + output_dir: lib/l10n diff --git a/setup.dart b/setup.dart index 542b5768..e46f5bcd 100755 --- a/setup.dart +++ b/setup.dart @@ -136,26 +136,6 @@ Future _package( String? androidArch, required bool verbose, }) async { - final distributorDir = p.join( - rootDir, - 'plugins', - 'flutter_distributor', - 'packages', - 'flutter_distributor', - ); - final activateResult = await Process.run('dart', [ - 'pub', - 'global', - 'activate', - '-s', - 'path', - distributorDir, - ]); - if (activateResult.exitCode != 0) { - stderr.write(activateResult.stderr); - return activateResult.exitCode; - } - final coreSha256 = platform == 'windows' ? await _buildGoCore(rootDir) : null; final file = File(p.join(rootDir, 'env.json')); @@ -176,6 +156,23 @@ Future _package( final depExit = await _ensureDependencies(platform, arch); if (depExit != 0) return depExit; + final activateResult = await Process.run('dart', [ + 'pub', + 'global', + 'activate', + '-s', + 'git', + 'https://github.com/chen08209/flutter_distributor.git', + '--git-ref', + 'FlClash', + '--git-path', + 'packages/flutter_distributor', + ]); + if (activateResult.exitCode != 0) { + stderr.write(activateResult.stderr); + return activateResult.exitCode; + } + final process = await Process.start( 'flutter_distributor', [ diff --git a/test/common/converter_test.dart b/test/common/converter_test.dart new file mode 100644 index 00000000..c47d6bb0 --- /dev/null +++ b/test/common/converter_test.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +import 'package:fl_clash/common/converter.dart'; +import 'package:test/test.dart'; + +void main() { + group('Uint8ListToListIntConverter', () { + test('converts bytes to a growable int list copy', () { + final source = Uint8List.fromList([1, 2, 255]); + + final result = uint8ListToListIntConverter.convert(source); + source[0] = 9; + result.add(3); + + expect(result, [1, 2, 255, 3]); + }); + + test('forwards chunked conversions and closes target sink', () { + final sink = _CollectingSink(); + final converterSink = uint8ListToListIntConverter.startChunkedConversion( + sink, + ); + + converterSink.add(Uint8List.fromList([1, 2])); + converterSink.add(Uint8List.fromList([3])); + converterSink.close(); + + expect(sink.values, [ + [1, 2], + [3], + ]); + expect(sink.isClosed, isTrue); + }); + }); +} + +class _CollectingSink implements Sink> { + final values = >[]; + var isClosed = false; + + @override + void add(List data) { + values.add(data); + } + + @override + void close() { + isClosed = true; + } +} diff --git a/test/common/datetime_test.dart b/test/common/datetime_test.dart new file mode 100644 index 00000000..a649ff15 --- /dev/null +++ b/test/common/datetime_test.dart @@ -0,0 +1,29 @@ +import 'package:fl_clash/common/datetime.dart'; +import 'package:test/test.dart'; + +void main() { + group('DateTimeExtension', () { + test('detects dates before now', () { + expect( + DateTime.now().subtract(const Duration(milliseconds: 1)).isBeforeNow, + isTrue, + ); + }); + + test('isBeforeSecure keeps null as false and non-null as true', () { + final dateTime = DateTime(2026, 5, 27); + + expect(dateTime.isBeforeSecure(null), isFalse); + expect(dateTime.isBeforeSecure(DateTime(2026, 5, 26)), isTrue); + expect(dateTime.isBeforeSecure(DateTime(2026, 5, 28)), isTrue); + }); + + test('formats date, full datetime, and time slices', () { + final dateTime = DateTime(2026, 5, 27, 14, 3, 9); + + expect(dateTime.show, '2026-05-27'); + expect(dateTime.showFull, '2026-05-27 14:03:09'); + expect(dateTime.showTime, ' 14:03:09'); + }); + }); +} diff --git a/test/common/delay_test.dart b/test/common/delay_test.dart index ac6e7396..d6b41167 100644 --- a/test/common/delay_test.dart +++ b/test/common/delay_test.dart @@ -45,19 +45,23 @@ void main() { expect(b.compareTo(a), greaterThan(0)); }); - test('group=true sorts before group=false within same priority and delay', - () { - const a = DelayState(delay: 100, group: true); - const b = DelayState(delay: 100, group: false); - expect(a.compareTo(b), lessThan(0)); - }); + test( + 'group=true sorts before group=false within same priority and delay', + () { + const a = DelayState(delay: 100, group: true); + const b = DelayState(delay: 100, group: false); + expect(a.compareTo(b), lessThan(0)); + }, + ); - test('group=false sorts after group=true within same priority and delay', - () { - const a = DelayState(delay: 100, group: false); - const b = DelayState(delay: 100, group: true); - expect(a.compareTo(b), greaterThan(0)); - }); + test( + 'group=false sorts after group=true within same priority and delay', + () { + const a = DelayState(delay: 100, group: false); + const b = DelayState(delay: 100, group: true); + expect(a.compareTo(b), greaterThan(0)); + }, + ); test('returns 0 for equal states', () { const a = DelayState(delay: 100, group: true); diff --git a/test/common/iterable_test.dart b/test/common/iterable_test.dart index 34362cf4..b01e4e72 100644 --- a/test/common/iterable_test.dart +++ b/test/common/iterable_test.dart @@ -134,6 +134,28 @@ void main() { }); }); + group('ListExt.copyAndReorder', () { + test('uses final insertion index when moving an item forward', () { + final result = ['a', 'b', 'c', 'd'].copyAndReorder(1, 3); + + expect(result, ['a', 'c', 'd', 'b']); + }); + + test('uses final insertion index when moving an item backward', () { + final result = ['a', 'b', 'c', 'd'].copyAndReorder(3, 1); + + expect(result, ['a', 'd', 'b', 'c']); + }); + + test('does not mutate original list', () { + final list = ['a', 'b', 'c']; + final result = list.copyAndReorder(0, 2); + + expect(list, ['a', 'b', 'c']); + expect(result, ['b', 'c', 'a']); + }); + }); + group('ListExt.safeSublist', () { test('returns full list when start <= 0', () { final result = [1, 2, 3].safeSublist(0); diff --git a/test/common/picker_test.dart b/test/common/picker_test.dart new file mode 100644 index 00000000..5843ba79 --- /dev/null +++ b/test/common/picker_test.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:fl_clash/common/picker.dart'; +import 'package:test/test.dart'; + +void main() { + group('PlatformFileExt.readBytes', () { + test('loads bytes from the picked file path', () async { + final directory = await Directory.systemTemp.createTemp( + 'fl_clash_picker_test_', + ); + addTearDown(() => directory.delete(recursive: true)); + + final file = File('${directory.path}/profile.yaml'); + await file.writeAsString('mixed-port: 7890'); + + final platformFile = PlatformFile( + name: 'profile.yaml', + path: file.path, + size: await file.length(), + ); + + final bytes = await platformFile.readBytes(); + + expect(String.fromCharCodes(bytes), 'mixed-port: 7890'); + }); + }); +} diff --git a/test/common/protocol_test.dart b/test/common/protocol_test.dart new file mode 100644 index 00000000..f13f204f --- /dev/null +++ b/test/common/protocol_test.dart @@ -0,0 +1,19 @@ +import 'package:fl_clash/common/protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('ProtocolRegistrationPlan', () { + test('builds registry writes for URL protocol registration', () { + const plan = ProtocolRegistrationPlan( + scheme: 'flclash', + executable: r'C:\Program Files\FlClash\FlClash.exe', + ); + + expect(plan.protocolKey, r'Software\Classes\flclash'); + expect(plan.commandKey, r'shell\open\command'); + expect(plan.protocolValueName, 'URL Protocol'); + expect(plan.protocolValue, ''); + expect(plan.command, r'"C:\Program Files\FlClash\FlClash.exe" "%1"'); + }); + }); +} diff --git a/test/core/controller_test.dart b/test/core/controller_test.dart index 154eff0c..f5f81db1 100644 --- a/test/core/controller_test.dart +++ b/test/core/controller_test.dart @@ -246,9 +246,8 @@ void main() { }); test('updateGeoData delegates', () async { - const params = UpdateGeoDataParams(geoType: 'mmdb', geoName: 'Country'); - when(() => mock.updateGeoData(params)).thenAnswer((_) async => 'ok'); - final result = await controller.updateGeoData(params); + when(() => mock.updateGeoData('MMDB')).thenAnswer((_) async => 'ok'); + final result = await controller.updateGeoData('MMDB'); expect(result, 'ok'); }); diff --git a/test/enum/enum_test.dart b/test/enum/enum_test.dart new file mode 100644 index 00000000..22fcf972 --- /dev/null +++ b/test/enum/enum_test.dart @@ -0,0 +1,114 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; + +void main() { + group('GroupType', () { + test('parses clash group type aliases case-insensitively', () { + expect(GroupType.parse('url-test'), GroupType.URLTest); + expect(GroupType.parse('URLTEST'), GroupType.URLTest); + expect(GroupType.parse('selector'), GroupType.Selector); + expect(GroupType.parse('loadbalance'), GroupType.LoadBalance); + }); + + test('throws for unsupported group type', () { + expect(() => GroupType.parse('unknown'), throwsUnimplementedError); + }); + }); + + group('GroupTypeExtension', () { + test('maps display names back to enum values', () { + expect(GroupTypeExtension.getGroupType('Selector'), GroupType.Selector); + expect(GroupTypeExtension.getGroupType('URLTest'), GroupType.URLTest); + expect(GroupTypeExtension.getGroupType('missing'), isNull); + }); + + test('marks only computed selection types', () { + expect(GroupType.URLTest.isComputedSelected, isTrue); + expect(GroupType.Fallback.isComputedSelected, isTrue); + expect(GroupType.Selector.isComputedSelected, isFalse); + }); + }); + + group('UsedProxyExtension', () { + test('returns enum names as proxy values', () { + expect(UsedProxy.GLOBAL.value, 'GLOBAL'); + expect(UsedProxy.DIRECT.value, 'DIRECT'); + expect(UsedProxy.REJECT.value, 'REJECT'); + }); + }); + + group('KeyboardModifierExt', () { + test('maps keyboard modifiers to hotkey modifiers', () { + expect(KeyboardModifier.alt.toHotKeyModifier(), HotKeyModifier.alt); + expect( + KeyboardModifier.control.toHotKeyModifier(), + HotKeyModifier.control, + ); + expect(KeyboardModifier.shift.toHotKeyModifier(), HotKeyModifier.shift); + }); + + test('keeps platform key variants for paired modifiers', () { + expect(KeyboardModifier.alt.physicalKeys, [ + PhysicalKeyboardKey.altLeft, + PhysicalKeyboardKey.altRight, + ]); + expect(KeyboardModifier.meta.physicalKeys, [ + PhysicalKeyboardKey.metaLeft, + PhysicalKeyboardKey.metaRight, + ]); + }); + }); + + group('RuleAction', () { + test('excludes actions that cannot be manually added', () { + expect(RuleAction.addedRuleActions, isNot(contains(RuleAction.MATCH))); + expect(RuleAction.addedRuleActions, isNot(contains(RuleAction.RULE_SET))); + expect(RuleAction.addedRuleActions, isNot(contains(RuleAction.SUB_RULE))); + expect(RuleAction.addedRuleActions, contains(RuleAction.DOMAIN)); + }); + + test('identifies actions with extra params', () { + expect(RuleAction.GEOIP.hasParams, isTrue); + expect(RuleAction.IP_CIDR.hasParams, isTrue); + expect(RuleAction.DOMAIN.hasParams, isFalse); + }); + }); + + group('RuleTarget', () { + test('contains built-in target names', () { + expect(RuleTarget.baseTargets, {'DIRECT', 'REJECT'}); + }); + }); + + group('ItemPosition', () { + test('calculates simple list positions', () { + expect(ItemPosition.get(0, 1), ItemPosition.startAndEnd); + expect(ItemPosition.get(0, 3), ItemPosition.start); + expect(ItemPosition.get(1, 3), ItemPosition.middle); + expect(ItemPosition.get(2, 3), ItemPosition.end); + }); + + test('calculates visual positions after deleted items are skipped', () { + final items = ['a', 'b', 'c', 'd']; + final deletedItems = {'a', 'c'}; + + expect( + ItemPosition.calculateVisualPosition(1, items, deletedItems), + ItemPosition.start, + ); + expect( + ItemPosition.calculateVisualPosition(3, items, deletedItems), + ItemPosition.end, + ); + }); + + test('deleted current item has no visual position', () { + expect( + ItemPosition.calculateVisualPosition(1, ['a', 'b'], {'b'}), + ItemPosition.middle, + ); + }); + }); +} diff --git a/test/models/common_test.dart b/test/models/common_test.dart new file mode 100644 index 00000000..37357cad --- /dev/null +++ b/test/models/common_test.dart @@ -0,0 +1,150 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('PackagesExt', () { + const packages = [ + Package( + packageName: 'system.app', + label: 'System', + system: true, + internet: true, + lastUpdateTime: 1, + ), + Package( + packageName: 'user.old', + label: 'Alpha', + system: false, + internet: false, + lastUpdateTime: 2, + ), + Package( + packageName: 'user.new', + label: 'Beta', + system: false, + internet: true, + lastUpdateTime: 3, + ), + ]; + + test('filters system and non-internet apps', () { + final result = packages.getViewList( + pinedList: [], + sortType: AccessSortType.none, + isFilterSystemApp: true, + isFilterNonInternetApp: true, + ); + + expect(result.map((item) => item.packageName), ['user.new']); + }); + + test('pins selected packages before sorted packages', () { + final result = packages.getViewList( + pinedList: ['user.old'], + sortType: AccessSortType.name, + isFilterSystemApp: false, + isFilterNonInternetApp: false, + ); + + expect(result.map((item) => item.packageName), [ + 'user.old', + 'user.new', + 'system.app', + ]); + }); + }); + + group('TrackerInfoExt', () { + test('builds destination description and process text', () { + final trackerInfo = TrackerInfo( + id: '1', + start: DateTime(2026), + metadata: const Metadata( + network: 'tcp', + host: 'example.com', + destinationIP: '1.1.1.1', + destinationPort: '443', + process: 'Browser', + uid: 501, + ), + chains: const ['Proxy'], + rule: 'MATCH', + rulePayload: '', + ); + + expect(trackerInfo.desc, 'tcp://example.com/1.1.1.1:443'); + expect(trackerInfo.progressText, 'Browser(501)'); + }); + }); + + group('TrafficExt', () { + test('formats speed, description, tray title, and total speed', () { + const traffic = Traffic(up: 1024, down: 2048); + + expect(traffic.speedText, '↑ 1KB/s ↓ 2KB/s'); + expect(traffic.desc, '1KB ↑ 2KB ↓'); + expect(traffic.trayTitle, '1 KB/s\n2 KB/s'); + expect(traffic.speed, 3072); + }); + }); + + group('GroupsExt', () { + test('finds group by name and resolves current selection', () { + const groups = [ + Group(name: 'Auto', type: GroupType.URLTest, now: 'Proxy A'), + Group(name: 'Manual', type: GroupType.Selector, now: 'Proxy B'), + ]; + + expect( + groups.getGroup('Auto')?.getCurrentSelectedName('Proxy C'), + 'Proxy A', + ); + expect( + groups.getGroup('Manual')?.getCurrentSelectedName('Proxy C'), + 'Proxy C', + ); + expect(groups.getGroup('Missing'), isNull); + }); + }); + + group('IpInfo parsers', () { + test('parse supported response shapes', () { + expect( + IpInfo.fromIpInfoIoJson({'ip': '1.1.1.1', 'country': 'US'}), + const IpInfo(ip: '1.1.1.1', countryCode: 'US'), + ); + expect( + IpInfo.fromMyIpJson({'ip': '2.2.2.2', 'cc': 'JP'}), + const IpInfo(ip: '2.2.2.2', countryCode: 'JP'), + ); + expect( + IpInfo.fromIpAPIJson({'query': '3.3.3.3', 'countryCode': 'CN'}), + const IpInfo(ip: '3.3.3.3', countryCode: 'CN'), + ); + }); + + test('throw FormatException for unsupported response shapes', () { + expect( + () => IpInfo.fromIpInfoIoJson({'ip': '1.1.1.1'}), + throwsFormatException, + ); + expect( + () => IpInfo.fromIpApiCoJson({'ip': '1.1.1.1'}), + throwsFormatException, + ); + }); + }); + + group('ResultExt', () { + test('identifies success and error results', () { + final success = Result.success('ok'); + final error = Result.error('failed'); + + expect(success.isSuccess, isTrue); + expect(success.isError, isFalse); + expect(error.isSuccess, isFalse); + expect(error.isError, isTrue); + }); + }); +} diff --git a/test/models/config_test.dart b/test/models/config_test.dart index 563078b4..bf037ac8 100644 --- a/test/models/config_test.dart +++ b/test/models/config_test.dart @@ -17,6 +17,79 @@ T roundTrip( } void main() { + group('GeoResource JSON', () { + test('exposes lowercase GeoResource values', () { + expect(GeoResource.MMDB.value, 'mmdb'); + expect(GeoResource.ASN.value, 'asn'); + expect(GeoResource.GEOIP.value, 'geo-ip'); + expect(GeoResource.GEOSITE.value, 'geo-site'); + }); + + test('parses current lowercase GeoResource keys from config JSON', () { + final config = PatchClashConfig.fromJson({ + 'geox-url': { + 'mmdb': 'https://example.com/mmdb', + 'asn': 'https://example.com/asn.mmdb', + 'geo-ip': 'https://example.com/geoip.dat', + 'geo-site': 'https://example.com/geosite.dat', + }, + }); + + expect(config.geoXUrl, { + GeoResource.MMDB: 'https://example.com/mmdb', + GeoResource.ASN: 'https://example.com/asn.mmdb', + GeoResource.GEOIP: 'https://example.com/geoip.dat', + GeoResource.GEOSITE: 'https://example.com/geosite.dat', + }); + }); + + test('parses legacy GeoResource keys from config JSON', () { + final config = PatchClashConfig.fromJson({ + 'geox-url': { + 'geoip': 'https://example.com/legacy-geoip.dat', + 'geosite': 'https://example.com/legacy-geosite.dat', + }, + }); + + expect(config.geoXUrl, { + GeoResource.GEOIP: 'https://example.com/legacy-geoip.dat', + GeoResource.GEOSITE: 'https://example.com/legacy-geosite.dat', + }); + }); + + test('GeoXUrl defaults use GeoResource keys', () { + expect(defaultGeoXUrl.keys, GeoResource.values); + }); + + test('PatchClashConfig serializes geoXUrl map with lowercase keys', () { + final json = const PatchClashConfig( + geoXUrl: {GeoResource.GEOIP: 'https://example.com/geoip.dat'}, + ).toJson(); + + expect(json['geox-url'], {'geo-ip': 'https://example.com/geoip.dat'}); + }); + + test('converts geoXUrl map to raw config map', () { + const geoXUrl = { + GeoResource.MMDB: 'https://example.com/mmdb', + GeoResource.GEOSITE: 'https://example.com/geosite.dat', + }; + + expect(geoXUrl.raw, { + 'mmdb': 'https://example.com/mmdb', + 'geo-site': 'https://example.com/geosite.dat', + }); + }); + + test('PatchClashConfig parses geoXUrl map with GeoResource keys', () { + final config = PatchClashConfig.fromJson({ + 'geox-url': {'mmdb': 'https://example.com/mmdb'}, + }); + + expect(config.geoXUrl, {GeoResource.MMDB: 'https://example.com/mmdb'}); + }); + }); + group('AppSettingProps JSON round-trip', () { test('default values survive round-trip', () { const props = AppSettingProps(); @@ -35,6 +108,7 @@ void main() { expect(restored.showLabel, false); expect(restored.minimizeOnExit, true); expect(restored.restoreStrategy, RestoreStrategy.compatible); + expect(restored.customUserAgent, ''); expect(restored.testUrl, defaultTestUrl); }); @@ -45,6 +119,7 @@ void main() { autoLaunch: true, closeConnections: false, testUrl: 'https://custom.test', + customUserAgent: 'CustomUA/1.0', ); final restored = roundTrip( () => props.toJson(), @@ -55,6 +130,7 @@ void main() { expect(restored.autoLaunch, true); expect(restored.closeConnections, false); expect(restored.testUrl, 'https://custom.test'); + expect(restored.customUserAgent, 'CustomUA/1.0'); }); test('safeFromJson returns default on null', () { @@ -162,6 +238,41 @@ void main() { }); }); + group('PatchClashConfig JSON round-trip', () { + test('defaults match Clash patch defaults', () { + const config = PatchClashConfig(); + + expect(config.mixedPort, defaultMixedPort); + expect(config.allowLan, false); + expect(config.mode, Mode.rule); + expect(config.externalController, ExternalControllerStatus.close); + expect(config.geodataLoader, GeodataLoader.memconservative); + }); + + test('custom values survive round-trip', () { + const config = PatchClashConfig( + mixedPort: 7890, + allowLan: true, + mode: Mode.rule, + logLevel: LogLevel.debug, + externalController: ExternalControllerStatus.open, + geodataLoader: GeodataLoader.memconservative, + ); + + final restored = roundTrip( + () => config.toJson(), + PatchClashConfig.fromJson, + ); + + expect(restored.mixedPort, 7890); + expect(restored.allowLan, true); + expect(restored.mode, Mode.rule); + expect(restored.logLevel, LogLevel.debug); + expect(restored.externalController, ExternalControllerStatus.open); + expect(restored.geodataLoader, GeodataLoader.memconservative); + }); + }); + group('ProxiesStyleProps JSON round-trip', () { test('default values', () { const props = ProxiesStyleProps(); diff --git a/test/models/profile_test.dart b/test/models/profile_test.dart new file mode 100644 index 00000000..e1a9f1c4 --- /dev/null +++ b/test/models/profile_test.dart @@ -0,0 +1,107 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('SubscriptionInfo', () { + test('parses subscription-userinfo header values', () { + final info = SubscriptionInfo.formHString( + 'upload=10; download=20; total=100; expire=200', + ); + + expect(info.upload, 10); + expect(info.download, 20); + expect(info.total, 100); + expect(info.expire, 200); + }); + + test('falls back to zero for null and invalid values', () { + expect(SubscriptionInfo.formHString(null), const SubscriptionInfo()); + + final info = SubscriptionInfo.formHString( + 'upload=bad; download=20; total=; expire=abc', + ); + + expect(info.upload, 0); + expect(info.download, 20); + expect(info.total, 0); + expect(info.expire, 0); + }); + }); + + group('ProfileExtension', () { + test('derives type, label, filename, and updating key', () { + const fileProfile = Profile( + id: 7, + autoUpdateDuration: defaultUpdateDuration, + ); + const urlProfile = Profile( + id: 8, + label: 'Remote', + url: 'https://example.com/profile.yaml', + autoUpdate: true, + autoUpdateDuration: defaultUpdateDuration, + ); + + expect(fileProfile.type, ProfileType.file); + expect(fileProfile.realAutoUpdate, false); + expect(fileProfile.realLabel, '7'); + expect(fileProfile.fileName, '7.yaml'); + expect(fileProfile.updatingKey, 'profile_7'); + + expect(urlProfile.type, ProfileType.url); + expect(urlProfile.realAutoUpdate, true); + expect(urlProfile.realLabel, 'Remote'); + }); + }); + + group('ProfilesExt', () { + test('gets profile by id', () { + const profiles = [ + Profile(id: 1, label: 'A', autoUpdateDuration: defaultUpdateDuration), + Profile(id: 2, label: 'B', autoUpdateDuration: defaultUpdateDuration), + ]; + + expect(profiles.getProfile(2)?.label, 'B'); + expect(profiles.getProfile(3), isNull); + expect(profiles.getProfile(null), isNull); + }); + + test('optimizes duplicate labels with incremented suffix', () { + const profiles = [ + Profile( + id: 1, + label: 'Work', + autoUpdateDuration: defaultUpdateDuration, + ), + Profile( + id: 2, + label: 'Work(1)', + autoUpdateDuration: defaultUpdateDuration, + ), + ]; + const newProfile = Profile( + id: 3, + label: 'Work', + autoUpdateDuration: defaultUpdateDuration, + ); + + expect(profiles.optimizeLabel(newProfile).label, 'Work(2)'); + }); + }); + + group('ProfileRuleLinkExt', () { + test('builds stable key from non-null parts', () { + const link = ProfileRuleLink( + profileId: 1, + ruleId: 2, + scene: RuleScene.added, + ); + const globalLink = ProfileRuleLink(ruleId: 3); + + expect(link.key, '1_2_added'); + expect(globalLink.key, '3'); + }); + }); +} diff --git a/test/providers/action_test.dart b/test/providers/action_test.dart index 3304e9b7..e8cc6164 100644 --- a/test/providers/action_test.dart +++ b/test/providers/action_test.dart @@ -1,5 +1,7 @@ +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/action.dart'; +import 'package:fl_clash/providers/app.dart'; import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/providers/database.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -36,6 +38,29 @@ void main() { expect(profile?.url, edited.url); }); }); + + group('GeoResourceAction', () { + test('GeoResource has correct updatingKey', () { + expect(GeoResource.MMDB.updatingKey, 'geo_resource_MMDB'); + expect(GeoResource.ASN.updatingKey, 'geo_resource_ASN'); + expect(GeoResource.GEOIP.updatingKey, 'geo_resource_GEOIP'); + expect(GeoResource.GEOSITE.updatingKey, 'geo_resource_GEOSITE'); + }); + + test('IsUpdating provider works with geo resource key', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + final key = GeoResource.MMDB.updatingKey; + expect(container.read(isUpdatingProvider(key)), false); + + container.read(isUpdatingProvider(key).notifier).value = true; + expect(container.read(isUpdatingProvider(key)), true); + + container.read(isUpdatingProvider(key).notifier).value = false; + expect(container.read(isUpdatingProvider(key)), false); + }); + }); } class _TestProfiles extends Profiles { diff --git a/test/providers/app_test.dart b/test/providers/app_test.dart index 142078b6..6e052d8c 100644 --- a/test/providers/app_test.dart +++ b/test/providers/app_test.dart @@ -52,6 +52,42 @@ void main() { }); }); + group('Providers provider', () { + test('setProvider replaces provider with matching name', () { + final oldProvider = ExternalProvider( + name: 'Proxy', + type: 'Proxy', + count: 1, + vehicleType: 'HTTP', + updateAt: DateTime(2026), + ); + final newProvider = oldProvider.copyWith(count: 2); + container.read(providersProvider.notifier).update((_) => [oldProvider]); + + container.read(providersProvider.notifier).setProvider(newProvider); + + expect(container.read(providersProvider).single.count, 2); + }); + + test('setProvider ignores null and missing provider names', () { + final provider = ExternalProvider( + name: 'Proxy', + type: 'Proxy', + count: 1, + vehicleType: 'HTTP', + updateAt: DateTime(2026), + ); + container.read(providersProvider.notifier).update((_) => [provider]); + + container.read(providersProvider.notifier).setProvider(null); + container + .read(providersProvider.notifier) + .setProvider(provider.copyWith(name: 'Other', count: 9)); + + expect(container.read(providersProvider).single, provider); + }); + }); + group('SystemBrightness provider', () { test('default is dark', () { expect(container.read(systemBrightnessProvider), Brightness.dark); @@ -176,9 +212,12 @@ void main() { expect(container.read(backBlockProvider), false); }); - test('can update', () { - container.read(backBlockProvider.notifier).update((_) => true); + test('can block and unblock back navigation', () { + container.read(backBlockProvider.notifier).backBlock(); expect(container.read(backBlockProvider), true); + + container.read(backBlockProvider.notifier).unBackBlock(); + expect(container.read(backBlockProvider), false); }); }); @@ -221,9 +260,72 @@ void main() { expect(container.read(checkIpNumProvider), 0); }); - test('increment works', () { - container.read(checkIpNumProvider.notifier).update((_) => 3); - expect(container.read(checkIpNumProvider), 3); + test('increment returns previous value and updates state', () { + final value = container.read(checkIpNumProvider.notifier).add(); + + expect(value, 0); + expect(container.read(checkIpNumProvider), 1); + }); + }); + + group('SortNum provider', () { + test('increment returns previous value and updates state', () { + final value = container.read(sortNumProvider.notifier).add(); + + expect(value, 0); + expect(container.read(sortNumProvider), 1); + }); + }); + + group('DelayDataSource provider', () { + test('sets delay by url and proxy name', () { + container + .read(delayDataSourceProvider.notifier) + .setDelay( + const Delay(name: 'Proxy', url: 'https://test.example', value: 120), + ); + + expect(container.read(delayDataSourceProvider), { + 'https://test.example': {'Proxy': 120}, + }); + }); + + test('keeps same state instance when delay value is unchanged', () { + final notifier = container.read(delayDataSourceProvider.notifier); + const delay = Delay(name: 'Proxy', url: 'https://test.example', value: 1); + notifier.setDelay(delay); + final state = container.read(delayDataSourceProvider); + + notifier.setDelay(delay); + + expect(identical(container.read(delayDataSourceProvider), state), isTrue); + }); + }); + + group('Loading provider', () { + test('stop without start sets loading false immediately', () async { + final notifier = container.read( + loadingProvider(LoadingTag.profiles).notifier, + ); + + await notifier.stop(); + + expect(container.read(loadingProvider(LoadingTag.profiles)), false); + }); + + test('stop keeps loading visible for minimum duration', () async { + final notifier = container.read( + loadingProvider(LoadingTag.profiles).notifier, + ); + + notifier.start(); + await notifier.stop(); + + expect(container.read(loadingProvider(LoadingTag.profiles)), true); + + await Future.delayed(const Duration(milliseconds: 1100)); + + expect(container.read(loadingProvider(LoadingTag.profiles)), false); }); }); diff --git a/test/providers/config_test.dart b/test/providers/config_test.dart index 4902a886..5687d27e 100644 --- a/test/providers/config_test.dart +++ b/test/providers/config_test.dart @@ -1,3 +1,4 @@ +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart'; @@ -25,9 +26,9 @@ void main() { }); test('can update state', () { - container.read(appSettingProvider.notifier).update( - (_) => const AppSettingProps(autoLaunch: true), - ); + container + .read(appSettingProvider.notifier) + .update((_) => const AppSettingProps(autoLaunch: true)); final value = container.read(appSettingProvider); expect(value.autoLaunch, true); }); @@ -41,9 +42,9 @@ void main() { }); test('can update state', () { - container.read(windowSettingProvider.notifier).update( - (_) => const WindowProps(width: 1024, height: 768), - ); + container + .read(windowSettingProvider.notifier) + .update((_) => const WindowProps(width: 1024, height: 768)); final value = container.read(windowSettingProvider); expect(value.width, 1024); expect(value.height, 768); @@ -58,9 +59,9 @@ void main() { }); test('can update state', () { - container.read(vpnSettingProvider.notifier).update( - (_) => const VpnProps(enable: false), - ); + container + .read(vpnSettingProvider.notifier) + .update((_) => const VpnProps(enable: false)); expect(container.read(vpnSettingProvider).enable, false); }); }); @@ -73,9 +74,9 @@ void main() { }); test('can update state', () { - container.read(networkSettingProvider.notifier).update( - (_) => const NetworkProps(systemProxy: false), - ); + container + .read(networkSettingProvider.notifier) + .update((_) => const NetworkProps(systemProxy: false)); expect(container.read(networkSettingProvider).systemProxy, false); }); }); @@ -87,13 +88,10 @@ void main() { }); test('can update state', () { - container.read(themeSettingProvider.notifier).update( - (_) => const ThemeProps(primaryColor: 0xFF123456), - ); - expect( - container.read(themeSettingProvider).primaryColor, - 0xFF123456, - ); + container + .read(themeSettingProvider.notifier) + .update((_) => const ThemeProps(primaryColor: 0xFF123456)); + expect(container.read(themeSettingProvider).primaryColor, 0xFF123456); }); }); @@ -112,6 +110,18 @@ void main() { test('default is null', () { expect(container.read(davSettingProvider), null); }); + + test('can update WebDAV settings', () { + const davProps = DAVProps( + uri: 'https://dav.example.com', + user: 'user', + password: 'password', + ); + + container.read(davSettingProvider.notifier).update((_) => davProps); + + expect(container.read(davSettingProvider), davProps); + }); }); group('OverrideDns provider', () { @@ -125,10 +135,43 @@ void main() { }); }); + group('ExcludeSSIDs provider', () { + test('reorders with final insertion index semantics', () { + container + .read(excludeSSIDsProvider.notifier) + .update((_) => ['Home', 'Office', 'Cafe', 'Hotel']); + + container.read(excludeSSIDsProvider.notifier).update((value) { + return value.copyAndReorder(1, 3); + }); + + expect(container.read(excludeSSIDsProvider), [ + 'Home', + 'Cafe', + 'Hotel', + 'Office', + ]); + }); + }); + group('HotKeyActions provider', () { test('default is empty list', () { expect(container.read(hotKeyActionsProvider), isEmpty); }); + + test('can update hotkey actions', () { + const actions = [ + HotKeyAction( + action: HotAction.start, + key: 1, + modifiers: {KeyboardModifier.control}, + ), + ]; + + container.read(hotKeyActionsProvider.notifier).update((_) => actions); + + expect(container.read(hotKeyActionsProvider), actions); + }); }); group('ProxiesStyleSetting provider', () { @@ -138,7 +181,9 @@ void main() { }); test('can update state', () { - container.read(proxiesStyleSettingProvider.notifier).update( + container + .read(proxiesStyleSettingProvider.notifier) + .update( (_) => const ProxiesStyleProps(sortType: ProxiesSortType.delay), ); expect( @@ -158,15 +203,25 @@ void main() { expect(config.currentProfileId, null); expect(config.overrideDns, false); expect(config.hotKeyActions, isEmpty); + expect(config.patchClashConfig, const PatchClashConfig()); + expect(config.excludeSSIDs, isEmpty); }); test('reflects updated sub-provider values', () { container.read(currentProfileIdProvider.notifier).update((_) => 99); container.read(overrideDnsProvider.notifier).update((_) => true); + container + .read(patchClashConfigProvider.notifier) + .update((_) => const PatchClashConfig(mixedPort: 7890)); + container + .read(excludeSSIDsProvider.notifier) + .update((_) => ['Office Wi-Fi']); final config = container.read(configProvider); expect(config.currentProfileId, 99); expect(config.overrideDns, true); + expect(config.patchClashConfig.mixedPort, 7890); + expect(config.excludeSSIDs, ['Office Wi-Fi']); }); }); @@ -185,6 +240,11 @@ void main() { expect(overrideContainer.read(currentProfileIdProvider), 7); expect(overrideContainer.read(overrideDnsProvider), true); + expect( + overrideContainer.read(patchClashConfigProvider), + config.patchClashConfig, + ); + expect(overrideContainer.read(excludeSSIDsProvider), config.excludeSSIDs); expect( overrideContainer.read(appSettingProvider).onlyStatisticsProxy, false, diff --git a/test/widgets/input_test.dart b/test/widgets/input_test.dart new file mode 100644 index 00000000..4768d34b --- /dev/null +++ b/test/widgets/input_test.dart @@ -0,0 +1,111 @@ +import 'package:fl_clash/common/theme.dart'; +import 'package:fl_clash/l10n/l10n.dart'; +import 'package:fl_clash/providers/app.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ListItem.input limits dialog text by maxLength', (tester) async { + String? changedValue; + + await tester.pumpWidget( + ProviderScope( + overrides: [ + viewSizeProvider.overrideWithBuild((_, _) => const Size(1200, 1000)), + ], + child: _TestApp( + child: Scaffold( + body: ListItem.input( + title: const Text('Port'), + delegate: InputDelegate( + title: 'Port', + value: '', + maxLength: 5, + onChanged: (value) { + changedValue = value; + }, + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Port')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextFormField), '123456789'); + await tester.tap(find.text('Submit')); + await tester.pumpAndSettle(); + + expect(changedValue, '12345'); + }); + + testWidgets('ListInputPage reorders using final insertion index', ( + tester, + ) async { + tester.view.physicalSize = const Size(1200, 1000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + const ProviderScope( + child: _TestApp( + child: ListInputPage( + title: 'Items', + items: ['a', 'b', 'c'], + titleBuilder: _textBuilder, + ), + ), + ), + ); + + final listView = tester.widget( + find.byType(ReorderableListView), + ); + + listView.onReorderItem!(0, 2); + await tester.pump(); + + expect(_top(tester, 'b'), lessThan(_top(tester, 'c'))); + expect(_top(tester, 'c'), lessThan(_top(tester, 'a'))); + }); +} + +Widget _textBuilder(String value) { + return Text(value); +} + +double _top(WidgetTester tester, String text) { + return tester.getTopLeft(find.text(text)).dy; +} + +class _TestApp extends StatelessWidget { + final Widget child; + + const _TestApp({required this.child}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + navigatorKey: globalState.navigatorKey, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.delegate.supportedLocales, + builder: (context, child) { + globalState.theme = CommonTheme.of(context, 1); + return child!; + }, + home: child, + ); + } +} diff --git a/test/widgets/state_test.dart b/test/widgets/state_test.dart new file mode 100644 index 00000000..e8961713 --- /dev/null +++ b/test/widgets/state_test.dart @@ -0,0 +1,95 @@ +import 'package:fl_clash/widgets/activate_box.dart'; +import 'package:fl_clash/widgets/builder.dart'; +import 'package:fl_clash/widgets/disabled_mask.dart'; +import 'package:fl_clash/widgets/inherited.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ActivateBox blocks pointer events when inactive', ( + tester, + ) async { + var taps = 0; + + await tester.pumpWidget( + MaterialApp( + home: ActivateBox( + active: false, + child: TextButton(onPressed: () => taps++, child: const Text('tap')), + ), + ), + ); + + await tester.tap(find.text('tap'), warnIfMissed: false); + + expect(taps, 0); + }); + + testWidgets('ActivateBox allows pointer events when active', (tester) async { + var taps = 0; + + await tester.pumpWidget( + MaterialApp( + home: ActivateBox( + active: true, + child: TextButton(onPressed: () => taps++, child: const Text('tap')), + ), + ), + ); + + await tester.tap(find.text('tap')); + + expect(taps, 1); + }); + + testWidgets('DisabledMask applies filter only when status is true', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: DisabledMask(status: true, child: Text('disabled')), + ), + ); + + expect(find.byType(ColorFiltered), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp( + home: DisabledMask(status: false, child: Text('enabled')), + ), + ); + + expect(find.byType(ColorFiltered), findsNothing); + }); + + testWidgets('FloatingActionButtonExtendedBuilder defaults to extended', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: FloatingActionButtonExtendedBuilder( + builder: (isExtended) => Text('extended: $isExtended'), + ), + ), + ); + + expect(find.text('extended: true'), findsOneWidget); + }); + + testWidgets('FloatingActionButtonExtendedBuilder reads inherited state', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: CommonScaffoldFabExtendedProvider( + isExtended: false, + child: FloatingActionButtonExtendedBuilder( + builder: (isExtended) => Text('extended: $isExtended'), + ), + ), + ), + ); + + expect(find.text('extended: false'), findsOneWidget); + }); +} diff --git a/tool/bump_version.sh b/tool/bump_version.sh new file mode 100755 index 00000000..6cad3a50 --- /dev/null +++ b/tool/bump_version.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode="${1:-minor}" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +pubspec_file="${PUBSPEC_FILE:-"$script_dir/../pubspec.yaml"}" + +usage() { + cat <<'EOF' +Usage: tool/bump_version.sh [major|minor|all] + +major Bump app version patch: 0.8.94 -> 0.8.95 +minor Bump build number: YYYYMMDDNN, NN is today's build count from 01 to 99 (default) +all Bump both app version patch and build number +EOF +} + +if [[ "$mode" != "major" && "$mode" != "minor" && "$mode" != "all" ]]; then + usage >&2 + exit 64 +fi + +version_line="$(grep -E '^version: [0-9]+\.[0-9]+\.[0-9]+\+[0-9]{10}$' "$pubspec_file" || true)" +if [[ -z "$version_line" ]]; then + echo "No valid version line found in $pubspec_file" >&2 + exit 1 +fi + +version="${version_line#version: }" +app_version="${version%%+*}" +build_number="${version##*+}" + +if [[ "$mode" == "major" || "$mode" == "all" ]]; then + IFS='.' read -r major minor patch <<<"$app_version" + patch=$((patch + 1)) + app_version="$major.$minor.$patch" +fi + +if [[ "$mode" == "minor" || "$mode" == "all" ]]; then + today="$(date +%Y%m%d)" + build_date="${build_number:0:8}" + build_count="${build_number:8:2}" + if [[ "$build_date" == "$today" ]]; then + count=$((10#$build_count + 1)) + if ((count > 99)); then + echo "Build count for $today exceeds 99" >&2 + exit 1 + fi + else + count=1 + fi + build_number="$today$(printf '%02d' "$count")" +fi + +new_version="$app_version+$build_number" +tmp_file="$(mktemp)" +sed "s/^version: .*/version: $new_version/" "$pubspec_file" >"$tmp_file" +mv "$tmp_file" "$pubspec_file" + +echo "$version -> $new_version" diff --git a/tool/generate_status_icons.dart b/tool/generate_status_icons.dart new file mode 100644 index 00000000..37f7c594 --- /dev/null +++ b/tool/generate_status_icons.dart @@ -0,0 +1,115 @@ +import 'dart:io'; +import 'dart:typed_data'; + +const sourceDir = 'assets_source/images/icon'; +const outputDir = 'assets/images/icon'; +const statusIconNames = ['status_1', 'status_2', 'status_3']; + +Future main() async { + final rsvgConvert = await _findExecutable('rsvg-convert'); + if (rsvgConvert == null) { + stderr.writeln( + 'rsvg-convert is required. Install librsvg before generating icons.', + ); + exitCode = 1; + return; + } + + await Directory(outputDir).create(recursive: true); + final tempDir = await Directory.systemTemp.createTemp('status_icons_'); + try { + for (final name in statusIconNames) { + final source = File('$sourceDir/$name.svg'); + if (!source.existsSync()) { + stderr.writeln('Missing source SVG: ${source.path}'); + exitCode = 1; + return; + } + + final png = File('$outputDir/$name.png'); + final icoPng = File('${tempDir.path}/$name-32.png'); + final ico = File('$outputDir/$name.ico'); + + await _renderSvg( + rsvgConvert: rsvgConvert, + source: source, + output: png, + width: 108, + height: 108, + ); + await _renderSvg( + rsvgConvert: rsvgConvert, + source: source, + output: icoPng, + width: 32, + height: 32, + ); + await ico.writeAsBytes(_buildIco(await icoPng.readAsBytes())); + + stdout.writeln('Generated ${png.path} and ${ico.path}'); + } + } finally { + if (tempDir.existsSync()) { + await tempDir.delete(recursive: true); + } + } +} + +Future _findExecutable(String executable) async { + final result = await Process.run('which', [executable]); + if (result.exitCode != 0) { + return null; + } + return (result.stdout as String).trim(); +} + +Future _renderSvg({ + required String rsvgConvert, + required File source, + required File output, + required int width, + required int height, +}) async { + final result = await Process.run(rsvgConvert, [ + '-w', + '$width', + '-h', + '$height', + '-o', + output.path, + source.path, + ]); + if (result.exitCode != 0) { + stderr + ..writeln('Failed to render ${source.path} -> ${output.path}') + ..writeln(result.stderr); + exit(result.exitCode); + } +} + +Uint8List _buildIco(List pngBytes) { + const headerSize = 6; + const directoryEntrySize = 16; + const imageOffset = headerSize + directoryEntrySize; + + final bytes = BytesBuilder(copy: false) + ..add(_uint16(0)) + ..add(_uint16(1)) + ..add(_uint16(1)) + ..add([32, 32, 0, 0]) + ..add(_uint16(1)) + ..add(_uint16(32)) + ..add(_uint32(pngBytes.length)) + ..add(_uint32(imageOffset)) + ..add(pngBytes); + + return bytes.toBytes(); +} + +Uint8List _uint16(int value) { + return (ByteData(2)..setUint16(0, value, Endian.little)).buffer.asUint8List(); +} + +Uint8List _uint32(int value) { + return (ByteData(4)..setUint32(0, value, Endian.little)).buffer.asUint8List(); +}