Compare commits

...

53 Commits

Author SHA1 Message Date
chen08209 a8655b0809 chore: bump version to 0.9.1+2026101701 2026-10-17 09:55:19 +08:00
chen08209 6c52d7a39a fix(ui): stop non-secret fields from opening a password keyboard
Changelog: Fix pasting and Chinese input in text fields
2026-10-17 09:55:10 +08:00
chen08209 4d68938d22 docs(agents): document surfaces, typography and the lib/ui layer 2026-10-17 09:54:29 +08:00
chen08209 213a13f62f feat(l10n): add French, Spanish and Korean
Changelog: French, Spanish and Korean translations
2026-10-17 09:54:29 +08:00
chen08209 fefbcf4598 feat(android): drop Firebase analytics and crash reporting
Changelog: No more Firebase analytics or crash reporting
2026-10-17 09:54:29 +08:00
chen08209 9f48781341 feat(network)!: set desktop TUN key by key over each profile's own
BREAKING CHANGE: Desktop TUN keys follow the profile unless picked; changed values carry over
Changelog: Per-key desktop TUN settings; system proxy and tray fixes
2026-10-17 09:54:29 +08:00
chen08209 dd952dd600 feat(theme): redesign theme colors with an iOS-style picker
Changelog: Redesigned theme colors and color styles
2026-10-17 09:54:29 +08:00
chen08209 bdbf4ab024 feat(dashboard): redesign the dashboard around the current exit
Changelog: A redesigned dashboard around the current exit
2026-10-17 09:54:29 +08:00
chen08209 347a74b940 feat(profiles): manage external resources from the profile details
Changelog: Profile-detail resources and include-all in custom groups
2026-10-17 09:54:29 +08:00
chen08209 eb47ee261f feat(proxies): show groups as cards and group tabs as chips
Changelog: Proxy groups as cards, group tabs as chips
2026-10-17 09:54:29 +08:00
chen08209 e82ebf785b feat(views): show connections, requests and DNS as tables
Changelog: Sortable tables for connections, requests and DNS
2026-10-17 09:54:29 +08:00
chen08209 ba15c313b4 feat(ui): add Liquid Glass and slide-to-pick popup menus
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
2026-10-17 09:54:29 +08:00
chen08209 568c49bb3a feat(ui): layer surfaces with fills and follow the iOS type scale
Changelog: iOS-style surfaces, alerts and sheets; clearer TV focus
2026-10-16 22:54:53 +08:00
chen08209 2201c199b8 perf(android): keep Core calls and events off the main thread
Changelog: Smoother Android UI while the proxy is busy
2026-10-16 22:54:53 +08:00
chen08209 5984c161c5 fix(core): keep very large profiles from crashing the app
Changelog: Large profiles no longer crash; downloads trust core CAs
2026-10-16 22:54:53 +08:00
chen08209 a5b6d300f6 test: run the suite from a few shared entrypoints 2026-10-16 22:54:53 +08:00
chen08209 03f9553824 chore(release): v0.9.0 2026-10-14 09:16:30 +08:00
chen08209 8d93268de5 docs(agents): document custom profiles and setting changes 2026-10-12 10:34:14 +08:00
chen08209 491fae10f5 feat(views)!: set DNS, NTP, sniffer and TUN key by key
BREAKING CHANGE: DNS and NTP override toggles are removed; picked keys apply to every profile
Changelog: Per-key DNS, NTP, sniffer and TUN settings
2026-10-12 10:34:14 +08:00
chen08209 3abda78af4 feat(profiles): add a custom profile editor
Changelog: Custom profiles
2026-10-12 10:17:54 +08:00
chen08209 8c43053951 feat(ui): stack bottom sheets and add context menus
Changelog: Stacked sheets, context menus and a sort mode
2026-10-12 10:17:54 +08:00
chen08209 532ebf07e9 feat(providers): assemble custom profiles and import configs
Changelog: DNS, Hosts and ports apply without a restart
2026-10-11 12:28:52 +08:00
chen08209 16770249f1 feat(database)!: store custom profiles and their settings
BREAKING CHANGE: Custom overwrites are removed; rebuild them as custom profiles
Changelog: Unreadable settings no longer block startup or restores
Changelog-Type: fix
2026-10-11 12:28:52 +08:00
chen08209 d3fa2252b0 feat(android): add launcher shortcuts and one-step on-demand access
Changelog: Launcher shortcuts and one-step on-demand access
2026-10-11 12:28:52 +08:00
chen08209 94a25f4827 fix(desktop): fix the system proxy, tray and Helper on Windows and Linux
Changelog: Windows and Linux desktop fixes
2026-10-11 12:28:52 +08:00
chen08209 2fa2f7f816 feat(core): bind the Android Core with gomobile
Changelog: Share link import and mrs rule sets
2026-10-11 12:28:52 +08:00
chen08209 6c661f662e build: move to Flutter 3.47.6, Go 1.27 and Rust 1.99.0 2026-10-11 12:28:52 +08:00
chen08209 d780cb863d fix(core,helper): only let the owner drive the elevated Core
Changelog: The elevated Core only answers its owner
2026-10-11 12:28:52 +08:00
chen08209 4b59eca853 docs(site): drive the scroll effects from script and drop the trailing slash from links
CSS scroll-driven animations made touch scrolling drop frames in Android
WebView on a 120 Hz phone, so the progress bar and the preview tilt now
follow the scroll position from script. The release pulse animates a
transform instead of `left` and `right`, which ran layout on every frame.

GitHub Pages redirects the slashless project URL, so the links still resolve.
2026-10-03 20:54:21 +08:00
chen08209 68c71b8ef9 chore(release): v0.8.99 2026-10-03 19:03:12 +08:00
chen08209 ff23f30c31 fix(core): send changed geo resource URLs to the Core without a full apply
The Core only learned geox-url from a full config apply, so after a link
was edited on the resources page both a manual sync and the auto updater
kept downloading from the previous link until the profile was applied
again. The URLs now travel with the update params, which are pushed
whenever the patch config changes. Only a link that changed is written,
because mihomo's updaters read these links without a lock.

Changelog: A changed resource link is now used by the next sync and by auto update
2026-10-02 21:10:36 +08:00
chen08209 e55f6d1004 fix(core): keep the system resolver fallback when no DNS servers are reported
An Android service with no DNS servers sends an empty list, which split into
a single empty address that mihomo builds into a "udp://:" nameserver. The
system resolver then counted it as configured and never fell back to its
default nameservers, so lookups through it failed while the preferred network
reported no DNS.

Changelog: DNS lookups no longer fail on networks that report no DNS servers
2026-10-02 21:10:36 +08:00
chen08209 65176f6019 docs(readme): rewrite the README in both languages
Open with the project links and a feature list checked against the app,
list the downloads per platform, document link imports and Android
automation, and describe what a source build needs today. The Linux
note now names the runtime tray library and applies only to the
AppImage and .rpm, since the .deb declares its dependencies, and the
Android build no longer asks for an ANDROID_NDK variable.

The preview mockups keep their frames and now show the current UI: the
sidebar, the reworked dashboard and the floating dock.
2026-10-02 21:10:36 +08:00
chen08209 6041cd9289 docs(site): add a GitHub Pages site with a download guide and changelog
The page picks the build for the visitor's platform and architecture,
lists each release file with its size and SHA-256 digest, and renders
CHANGELOG.md with search. tool/build_site.sh assembles it from the
repository and the latest release, and the pages workflow deploys it
after every stable release build and on site changes to main.
2026-10-02 21:10:36 +08:00
chen08209 91e524c354 chore: exempt generated code from git whitespace checks
freezed emits lines of trailing spaces into the .freezed.dart files, which must not be edited by hand, so git diff --check failed on every change that regenerated them.
2026-10-02 20:32:42 +08:00
chen08209 786a6abca9 fix(proxies): shrink the plain group icon so it no longer fills the header
Changelog: Smaller icons in icon-only group headers
2026-10-02 20:32:42 +08:00
chen08209 1e9955dec6 fix(core,android): stop suspending the Core in Doze
The suspended tunnel rejected the traffic Doze still lets through, which
kept FCM from reconnecting, and the re-probe on resume tested every node,
select groups included.

Changelog: Push notifications reconnect in Doze
2026-10-02 20:32:42 +08:00
chen08209 e131675bdf feat(editor): move code_forge into the repository and add snippets
Changelog: Editor word wrap and snippets
2026-10-02 20:32:42 +08:00
chen08209 e62afb784a feat(dashboard): add cards and an animated, editable grid
Changelog: More dashboard cards
2026-10-02 20:32:42 +08:00
chen08209 8378c7081c feat(views): rework the proxies and scan pages, lay out records by their parts and unify search
A queued delay test shows a static clock until the task pool runs it, so
only the running tests animate while the list scrolls.

Changelog: Hide timed-out nodes and search every list
2026-10-02 20:32:42 +08:00
chen08209 841d6035d5 feat(ui): add a sidebar, floating dock, snap sheets and drag to go back
Changelog: Sidebar, floating dock and drag to go back
2026-10-02 20:32:41 +08:00
chen08209 ee6a49713a feat(ui): draw the app's own glyphs and redraw the theme page
Drops the Material compatibility bridge and the icon font.

Changelog: New app glyphs, empty states and theme page
2026-10-02 02:51:32 +08:00
chen08209 46fb7fee04 feat(overwrite): add app-level providers, scripts and validated custom proxies
Providers and custom proxies stay behind feature flags.

Changelog: Invalid override warnings and rule presets
2026-10-02 02:51:32 +08:00
chen08209 a390a42032 feat(config): log DNS queries, override only picked keys and rework settings
TUN now defaults to the mips stack.

Changelog: DNS query log and per-key DNS/NTP overrides
2026-10-02 02:51:32 +08:00
chen08209 bee8f24269 fix(backup,database): stage restores and enforce foreign keys
Changelog: Restores never apply halfway
2026-10-02 02:51:32 +08:00
chen08209 8068e686a3 feat(core): check services and the outbound IP through the Core
Changelog: Proxied service checks and outbound IP risk
2026-10-02 02:51:32 +08:00
chen08209 1e97716636 feat(platform): move desktop windows to plugins/window and rework the tray
plugins/window replaces the window_manager fork on every desktop.

Changelog: Tray hotkeys, node delays and a delay test
2026-09-30 11:39:58 +08:00
chen08209 2a2522bbc6 docs(agents): correct stale facts and sharpen skill triggers 2026-09-30 11:39:58 +08:00
chen08209 12403b063a build: target Android SDK 37, use Rust 1.98.1 and add a safe mode run
Moves to AGP 9.4.1 and Gradle 9.8.0; Rust 1.98.1 lets macOS 27 load
rust_api. Android 17 treats the tun subnet as local network, so starting
the VPN asks for that permission and falls back to gvisor without it.
Signed release builds upload native symbols to Crashlytics.
2026-09-30 11:39:58 +08:00
chen08209 c7be7023d3 docs(readme): replace the demo gifs with a desktop and mobile device mockup 2026-09-17 09:50:07 +08:00
chen08209 7c61c90ac2 chore(release): v0.8.98 2026-09-14 10:40:41 +08:00
chen08209 c5bf5bd487 fix(resources): refresh geo file info once the core finishes updating
The file info was re-read as soon as the update RPC returned, before the
core had finished writing the file, so the list kept showing the old size
and time. Refresh when the updating key clears instead, which also covers
automatic and core-initiated updates.

Changelog: Refresh the geo file size and time after an update finishes
2026-09-14 10:40:15 +08:00
chen08209 60f371afd8 fix(core): keep the IPC connection while a half-written frame waits on a suspended host
Windows Modern Standby suspends the app while the Helper's Core keeps
running, so a frame that was half written when the host stopped draining
timed out and closed the connection, which ended the Core with it. A
timed-out write on a half-written frame is now resumed for as long as the
stall lasts, and only a hard error still closes. go-winio reports the
expiry as its own ErrTimeout, so the check goes through Timeout().

Changelog: Keep the core running while Windows sleeps with the app suspended
2026-09-14 10:40:15 +08:00
1189 changed files with 223404 additions and 55039 deletions
+4 -4
View File
@@ -5,11 +5,11 @@ Use the smallest durable surface that matches the job.
## Surfaces
- `AGENTS.md`: repository entry point. Keep it small and reserve it for always-on rules, routing, and high-priority expectations.
- `CLAUDE.md`: the `@AGENTS.md` import and nothing that is not Claude-only. Claude Code loads `CLAUDE.md` into the system prompt at startup; the import makes `AGENTS.md` arrive with it. Without it, `AGENTS.md` reaches the session only as a tool result some agent chose to fetch, which is the weakest position in context and the first thing dropped when the conversation is summarized. Do not let rules accumulate in this file.
- No `CLAUDE.md`: Claude Code v2.1.277+ loads `AGENTS.md` at startup only while no `CLAUDE.md`, `.claude/CLAUDE.md`, or `CLAUDE.local.md` sits in the working directory or above it, so adding one silently replaces `AGENTS.md` for Claude. Sessions that cannot load `AGENTS.md` directly (third-party providers, telemetry disabled) need an uncommitted `CLAUDE.local.md` containing `@AGENTS.md`. The one nested `CLAUDE.md`, in `plugins/rust_api/`, imports the root `AGENTS.md` for the same reason.
- `.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.
- `.agents/skills/*/SKILL.md`: repo skills shared by Codex and Claude Code. Use these for repeatable workflows that should be discoverable by name and description; `.claude/skills/` holds one symlink per skill so Claude Code finds the same files.
- `.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/rules/*.rules`: command permission rules for running commands outside the sandbox. None exist yet. 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
@@ -19,7 +19,7 @@ Use the smallest durable surface that matches the job.
- Put reusable task workflows in `.agents/skills/<skill-name>/SKILL.md`.
- Put mechanical enforcement in linters, tests, hooks, or `.codex/rules`; do not rely on prose when tooling can enforce the rule.
- Split a rule into the part that can be counted and the part that cannot, and enforce them differently. A rule resting entirely on judgment gets decided at the keyboard, at the moment the exception looks most justified; the comment policy is the worked example, where "is this comment useful" stays prose and review while "are these comments too frequent" became a measured gate. Find the countable half before writing more prose.
- Restate a rule verbatim in every context that must obey it, and prefer a runnable check to a description of the rule. A gate an agent trips before reporting done outperforms a paragraph asking it to have been careful.
- Restate the one-line form of a rule in every context that must obey it, and keep its explanation in one `.agents/*.md` file that the others link to. Prefer a runnable check to either: a gate an agent trips before reporting done outperforms a paragraph asking it to have been careful.
- State a gate's real force. Enforcement differs per tool, and a rule that claims uniform coverage it does not have is worse than one that names the gap.
- Keep user-specific preferences out of the repository. They belong in user-level Codex configuration or user-level skills.
+1257 -161
View File
File diff suppressed because it is too large Load Diff
+133 -25
View File
@@ -17,12 +17,13 @@ dart setup.dart windows
dart setup.dart android
```
The Go core and the Rust helper build automatically: Flutter runs
The desktop Go core and the Rust helper build automatically: Flutter runs
`plugins/setup/hook/build.dart` on every `flutter build` and `flutter test`,
and the hook drives `CoreBuilder` from the `setup_hooks` package in
`plugins/setup/setup_hooks/`. Android builds take the NDK from the C compiler
Flutter hands the hook; no `ANDROID_NDK` variable is needed.
Artifacts land in `libclash/`. The hook reruns only when Go, Rust, or
`plugins/setup/setup_hooks/`. Artifacts land in `libclash/`. Android's Core is
the AAR the `:core` Gradle module builds with `gomobile bind` before anything
compiles against it; that needs `go` on `PATH` and nothing else, since the
gomobile tools come from `core/go.mod` and the NDK from AGP. The hook reruns only when Go, Rust, or
`setup_hooks` inputs change; the fingerprint cache lives in
`.dart_tool/setup_build_cache/`. To force a rebuild, delete that directory:
@@ -49,13 +50,18 @@ hooks:
build_assets: true # false does the same for the Rust library
```
CI flips both to `false` with `yq` before `dart run tool/changelog.dart` and
`flutter test`, and restores the file afterwards; no test loads either library.
CI flips `setup` to `false` with `yq` in its throwaway checkout before
`dart run tool/changelog.dart` and `dart run tool/test.dart`; no test loads the Go core. `rust_api`
stays on there because the editor tests in `test/plugins/code_forge/` and the socket
test in `test/core/desktop/ipc_native_test.dart` load the Rust library the hook
builds; the release-notes job, which runs no tests, flips both.
Never commit `false`: a build with it set stages whatever `libclash/` already
holds and bundles no Rust library, which is why `setup.dart` refuses to package
while it is set. Locally, `false` is worth setting for a Dart-only test loop,
but the hook cache keys on the user-define, so the first build afterwards runs
the hooks again.
the hooks again. The editor and IPC socket tests still need the Rust library: keep `rust_api`
on, or point `FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR` at a directory that
holds a `rust_api` library built from the same sources.
## Flutter Development
@@ -69,6 +75,23 @@ flutter test
Use `flutter test`, not `dart test`, because models pull in Flutter types.
A development build takes over the host exactly like a release: it creates the TUN device, sets the system proxy, on
macOS rewrites the system DNS, registers itself as the login item, and on Windows and Linux starts its Core through the
shared privileged Helper, which stops the Core an installed FlClash is running. Only the data directory is kept apart,
and only for some builds: a macOS debug build is `com.follow.clash.debug` and an Android debug build `com.follow.clash.dev`,
while profile and release builds, and every build on Windows and Linux, share the directory and preferences of the
installed app. Safe mode keeps all of the host changes off and never opens the Core's listeners or external
controller, so it does not compete with the installed app for the mixed, DNS, RESTful API and other loopback ports:
```bash
flutter run --dart-define-from-file=env.json --dart-define=SAFE_MODE=true
```
`.run/main_safe_mode.run.xml` is the same launch for IntelliJ, and `Flutter App (Safe Mode)` in `.zed/debug.json` (debugger) or `flutter run (Safe Mode)` in `.zed/tasks.json`
(plain run) for Zed. The build is bannered `SAFE MODE`, the tray shows the
`status_4` bug icon with a menu of only show and exit and no traffic title, and the settings keep their values, so
nothing has to be flipped back before a normal run. See the safe mode section of `architecture.md` for the owners that consult the flag.
## Code Generation
Run code generation after modifying models, providers, or database schema:
@@ -84,6 +107,14 @@ Code generation covers:
- Models through `freezed` and `json_serializable`.
- Database tables through `drift_dev`.
The Dart-Rust bindings in `plugins/rust_api` are generated by `flutter_rust_bridge_codegen` from
`plugins/rust_api/flutter_rust_bridge.yaml`; rerun it from that directory after changing anything under
`rust/src/api/`:
```bash
cd plugins/rust_api && flutter_rust_bridge_codegen generate
```
Generated output paths, configured in `build.yaml`:
- `lib/models/generated/*.g.dart`, `*.freezed.dart`.
@@ -99,7 +130,11 @@ dart run tool/generate_status_icons.dart
It writes the tray PNGs with Flutter `2.0x/`–`4.0x/` resolution variants to `assets/images/tray/unix/`,
multi-size tray `.ico` files to `assets/images/tray/windows/`, and `windows/runner/resources/app_icon.ico`
from `assets/images/icon.png`. `pubspec.yaml` declares the two tray directories with `platforms:` so each
from `assets/images/icon.png`. macOS draws its menu bar icon as a template, so it skips the ringed color
icons, whose logo shrinks to a small glyph once only alpha shows: `status_1` renders the bare logo from
`assets_source/images/icon/glyph.svg` and `status_4` reuses `status_4.svg`, both recolored to black into
`assets/images/tray/macos/`; the sources keep their colors. `glyph.svg` is also the geometry of the Android notification drawables
`android/service/src/main/res/drawable/ic.xml` and `ic_service.xml`; change them together. `pubspec.yaml` declares the three tray directories with `platforms:` so each
build only bundles the format its tray loads; a new status icon needs a source SVG and an entry in the
script's `statusIconNames`, nothing in `pubspec.yaml`.
@@ -119,10 +154,30 @@ flutter test test/setup_test.dart
flutter test plugins/proxy/test/proxy_test.dart
```
For the whole suite, or a directory of more than a few files, use `tool/test.dart`, which is also what CI runs:
```bash
dart run tool/test.dart # all of test/
dart run tool/test.dart test/widgets test/views # some paths
dart run tool/test.dart -- --reporter expanded --coverage
```
Plain `flutter test` compiles each file into its own program and starts an engine for it, one file after another, and
that dominates a full run: 329 files took 128 s on a 12-core Mac with at most four running at once. `tool/test.dart`
imports the files into 32 generated entrypoints under `.dart_tool/test_bundles/` and runs those with one worker per
core: about 60 s for the same run, and 67 s instead of 180–213 s with `--coverage`. Files in one entrypoint share an
isolate, so a test can see state a file before it left behind; the script runs every file that failed there again on
its own, lets that run decide, and names the files that only failed together. See the Shared Test Isolate section of
`rules.md` before fixing one.
`plugins/setup/setup_hooks/` is a pure Dart package and is tested with `dart test` from its own directory, which is
also what CI runs; `tool/check_plugins.sh` does not descend into it. It must stay out of `flutter test`, because a test
run of `plugins/setup` would execute the build hook itself.
Native plugin code under `plugins/*/{linux,macos,windows}` is compiled by the CI `desktop` job, which runs
`flutter build <platform> --debug` on every branch push, and by the release matrix on a tag; `tool/check_plugins.sh`
only analyzes and tests the Dart side.
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, or run `bash tool/check_plugins.sh` to analyze and test every plugin package the way CI does. Native plugin tests under platform folders are not run by `flutter test`.
For the current Core/service architecture, useful focused checks are:
@@ -139,7 +194,8 @@ flutter test test/widgets/core_status_button_test.dart
What those suites own:
- `test/core/desktop/`: replaceable IPC transport, RPC request correlation/failure, direct/Helper process leases, and
latest-intent desktop lifecycle convergence.
latest-intent desktop lifecycle convergence. `ipc_native_test.dart` runs the transport over a real socket through
the Rust `IpcServer`; the server's own behavior is covered by `cargo test` in `plugins/rust_api/rust`.
- `test/core/service_test.dart`: `CoreService` composition and terminal close behavior.
- `test/core/protocol_contract_test.dart`: shared Dart/Go method and event-envelope compatibility, including event batches.
- `test/providers/action_test.dart`: Core start/restart orchestration and overlapping restart requests.
@@ -149,12 +205,13 @@ What those suites own:
## Native Component Verification
The CI Go-wrapper checks can be reproduced without CGO:
The CI Go-wrapper checks can be reproduced without CGO, the Android files included:
```bash
cd core
CGO_ENABLED=0 go test .
CGO_ENABLED=0 go vet .
CGO_ENABLED=0 go vet . ./cmd/...
GOOS=android GOARCH=arm64 CGO_ENABLED=0 go vet -tags=with_gvisor . ./mobile ./tun ./platform
```
The Windows Helper's loopback/session protocol tests are host-independent by default. Windows CI additionally enables its
@@ -167,12 +224,15 @@ cargo test --manifest-path services/helper/Cargo.toml --features windows-service
```
The last command requires Windows for meaningful service coverage. Native Android lifecycle edits should at minimum
compile the modules they touch; use JDK 17 in this checkout:
compile the modules they touch, with the JDK 17 that CI installs. On macOS the Homebrew `openjdk@17` keg is not
registered with `java_home`, so export it by prefix:
```bash
cd android
JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home ./gradlew :service:compileDebugKotlin
JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home ./gradlew :app:compileDebugKotlin
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home" # elsewhere: any JDK 17
./gradlew :core:compileDebugKotlin -Ptarget-platform=android-arm64 # builds the gomobile AAR for one ABI
./gradlew :service:compileDebugKotlin
./gradlew :app:compileDebugKotlin
```
Always-on VPN entry, system VPN revoke, actual permission UI, and rapid device start/stop still require Android device or
@@ -238,14 +298,43 @@ Telegram post. They publish no GitHub release, so the update dialog never sees t
version from `pubspec.yaml` rather than the tag, so the patch has to be bumped before the first `-pre.N` of a cycle:
while `v<pubspec version>` is still tagged it refuses to collect anything and the release job fails.
## Verify
### Website
Every branch push runs the `dart` job, which performs these root-package checks
in order:
`site/` is the GitHub Pages site: a static page that renders `CHANGELOG.md` and the download links in the browser, and
works as is for local previews. `tool/build_site.sh [dir]` assembles it with the preview images, the app icon,
`CHANGELOG.md` read at the latest release tag, and a `release.json` of asset names, sizes and digests fetched through
`gh`. Without `gh` access the page falls back to the newest version in `CHANGELOG.md` and lists the files without sizes.
It then runs `tool/render_site.mjs` (Node), which writes the English page and a `zh/` copy for search engines: page
text, the first changelog releases, canonical and `hreflang` links, JSON-LD and `sitemap.xml`, all from the strings,
changelog parser and release markup in `site/assets/shared.js`, plus the GitHub star count from `release.json` so the
nav does not grow when the page loads. `SITE_URL` overrides the published URL these point at.
On the built site each URL keeps its language: the language button links to the other page and carries the section in
view, the chosen platform and the changelog search. Only the English root sends readers who chose or prefer Chinese on
to `zh/`, unless its URL carries `?lang=en`: the button on `zh/` links there with it, so getting back to English never
depends on browser storage. A project site cannot serve the `robots.txt` that would announce the sitemap, so submit
`sitemap.xml` in Google Search Console and Bing Webmaster Tools once.
```bash
bash tool/check_commit_msg_test.sh
bash tool/check_comment_density_test.sh
tool/build_site.sh && python3 -m http.server -d build/site
```
The page's look is deliberate: low-saturation flat colour, no blur, glow, glass or soft shadow, and motion that follows
the logo's 30° lattice. `site/assets/field.js` draws the hero's capsules on that lattice with the glyph's own
proportions, and the slanted wipes, the dot-to-bar marks and the theme transition reuse the same angle. Text that sits
inside the hero has to be listed in `wireField` in `app.js`, or the capsules run behind it.
`.github/workflows/pages.yaml` deploys it after every successful stable tag build and on pushes to `main` that touch the
site, in the public repository only; its Pages source has to be set to GitHub Actions once. The file names come from the
release matrix in `build.yaml`, so a renamed artifact needs the same change in `buildsFor` in `site/assets/app.js`.
## Verify
In the public repository every branch push runs the `dart` job; in the private
`origin` every check job runs only when started by hand (`workflow_dispatch`).
The `dart` job performs these root-package checks in order:
```bash
bash tool/hooks/test.sh
flutter pub get
dart format --output=none --set-exit-if-changed lib test tool plugins setup.dart
flutter analyze --no-fatal-infos
@@ -254,17 +343,23 @@ flutter test --reporter expanded --coverage
dart run tool/check_coverage.dart coverage/lcov.info 75
```
Run `flutter analyze` locally before committing when practical.
`tool/hooks/` holds the scripts the git hooks run, and `tool/hooks/test.sh` runs every `*_test.sh` beside them, so a
new hook script needs only its test file next to it, not a workflow step.
Run `flutter analyze` before committing; CI fails on anything it reports.
Release builds run only for `v*` tag pushes; pull requests trigger nothing.
Root analysis excludes `plugins/**`, and root tests do not discover nested
plugin packages, so parallel jobs validate the rest from their own package
directories: `plugins` (local Flutter packages and the setup build tool), `go`
(the Core wrapper, plus an NDK-backed vet of the Android files), `android`
(JVM unit tests for `:common`, `:service` and `:app`, with the Flutter compile
tasks excluded so no native build hook runs), `rust` (both crates), and a
Windows runner for the helper's `windows-service` feature. Release builds start
once all of them pass.
(the Core wrapper, plus a vet of the Android files), `android` (JVM unit tests
for `:common`, `:service` and `:app`, with the Flutter compile tasks excluded
and the gomobile AAR built for one ABI), `rust` (both crates), and a
Windows runner for the helper's `windows-service` feature. On branch pushes a
`desktop` job also compiles a debug build per desktop platform; a tag skips it,
since its release matrix compiles the same native code. Release builds run only
in the public repository and start alongside the checks; `upload` publishes
only once every build and every check job passes.
`bash tool/check_plugins.sh` is that plugin gate, and CI runs the same script.
It discovers every `plugins/*/pubspec.yaml`, analyzes each package, and runs
@@ -272,6 +367,19 @@ It discovers every `plugins/*/pubspec.yaml`, analyzes each package, and runs
no workflow edit; enumerating packages by hand in the workflow is what
previously left `plugins/tray` unanalyzed and untested.
### CI Caches
A run restores only cache entries saved on its own branch or tag, or on the default branch (`main`), and the
repository holds 10 GB before evicting the least recently used entries. Every release is a new tag that no later run
reads from, so `BRANCH_CACHE` turns caching off in tag runs: one release used to write about 9 GB of such entries and
evict everything `dev` had saved. Release builds therefore start cold; branch runs, chiefly `dev`, keep the caches.
- setup-go keys its entry on the `cache-dependency-path` files alone and saves only on a miss, so jobs listing the
same files share one entry holding the build cache of whichever finished first. `desktop` and `android` each add
the file that drives their Core build to get an entry of their own.
- setup-gradle writes only on the default branch unless told otherwise, so `android` lets `dev` write as well. Its
entries are keyed by job, which is one more reason the release build gains nothing from caching.
## Worktree Tooling
```bash
+103
View File
@@ -0,0 +1,103 @@
# Flutter Upgrade Checklist
Read this before changing `FLUTTER_VERSION` in `.github/workflows/build.yaml` or `.fvmrc`. Each entry is a
workaround tied to a Flutter bug: what it does, where it lives, and what has to be true before it can go. Remove an
entry together with its workaround; add one whenever a new workaround is pinned to the SDK.
## Stable cadence
Flutter ships a stable release about once a quarter, three minor versions apart (3.41 in February, 3.44 in May, 3.47
in August 2026). The two minors in between (3.48, 3.49) are beta only, so a fix that lands on `main` reaches stable
in the next multiple, 3.50, expected around mid-November 2026. Hotfix releases (3.47.x) carry only cherry-picks
listed in the SDK's `CHANGELOG.md`.
## Impeller disabled on Intel macOS release builds
- **Bug:** [flutter/flutter#191538](https://github.com/flutter/flutter/issues/191538). From 3.47, when Impeller
became the default desktop renderer, Intel (x86_64) Macs flicker across the whole window while scrolling or
animating, and text can render garbled. Apple Silicon is not affected.
- **Fix upstream:** landed on `main` before 2026-08-25 and verified on `3.48.0-1.0.pre-404`; it is not in any
3.47.x hotfix (checked through 3.47.6).
- **Workaround:** the release job for `macos-15-intel` (`arch: amd64`) runs
`plutil -replace FLTEnableImpeller -bool NO macos/Runner/Info.plist` before `dart setup.dart`, so only the Intel
DMG falls back to Skia. The checked-in `Info.plist` does not set the key, so local builds and the arm64 DMG keep
Impeller. The macOS embedder reads the key at launch in `FlutterDartProject.mm` (`enableImpeller`), release
builds included.
- **Side effect:** Skia on Intel renders blur, shadows and text slightly differently; judge Intel-only visual reports
against Skia, not Impeller.
- **Remove when:** `FLUTTER_VERSION` moves to a stable that contains the fix (3.50 or later, or a 3.47.x whose
changelog lists #191538). Before dropping the step, build the Intel DMG without it and scroll the proxies and
dashboard pages on an Intel Mac with its integrated GPU driving the built-in display; an external display can
switch to the discrete GPU and hide the flicker.
## Wrong-size back buffer on macOS
- **Bug:** [flutter/flutter#185394](https://github.com/flutter/flutter/issues/185394). `removeSurfaceForSize:` in
the macOS engine's `FlutterSurfaceManager.mm` hands out the youngest idle cached surface without checking its size.
When the window flips between sizes while the window server still holds the previous surface, which dragging a
window edge back and forth does, a request for one size gets a surface of another. Impeller's Metal texture then
disagrees with its descriptor, the render target drops it, and `Canvas::SetupRenderPass` dereferences a null
texture on the raster thread. Debug builds log `The texture and its descriptor disagree about its size` and
`Store action needs resolve but no valid resolve texture specified` first; release builds just crash.
- **Fix upstream:** [flutter/flutter#192522](https://github.com/flutter/flutter/pull/192522), still open; 3.47.6
ships the same `FlutterSurfaceManager.mm` as 3.47.4.
- **Workaround:** `BackBufferCacheGuard` in the `window` plugin's
`macos/window/Sources/window/BackBufferCacheGuard.swift`, installed from `WindowPlugin.register`, replaces
`-[FlutterBackBufferCache removeSurfaceForSize:]` at runtime and answers nil when the cached surface is not the
requested size, so the engine allocates a new one. It does nothing if the engine no longer has that class or
selector.
- **Remove when:** `FLUTTER_VERSION` moves to a stable that contains #192522 (check the SDK `CHANGELOG.md` for a
3.47.x cherry-pick). Before dropping it, run a debug build without the guard on that SDK and drag a window edge
back and forth for a minute; neither validation line may appear.
## Windows view visibility forwarded by the runner
- **Bug:** the Windows embedder's `WindowsLifecycleManager` counts the Flutter view as a window of its own and keeps
it visible until the view is sized to zero. Hiding the top-level window never resizes the view, and the `window`
plugin keeps it sized while minimized, so the app was never told `hidden`: animations kept rendering in the tray
and `appVisibleProvider` stayed true.
- **Workaround:** `WindowPluginForwardVisibility` in the `window` plugin's
`windows/include/window/window_plugin_runner.h` hands the top-level window's `WM_SHOWWINDOW` and `WM_SIZE` to
`ProcessExternalWindowMessage` for the view's HWND, so the view shows and hides with its window.
`FlutterWindow::MessageHandler` in `windows/runner/flutter_window.cpp` calls it before `HandleTopLevelWindowProc`;
the Flutter runner template has no such call, so keep it when regenerating the runner.
- **Remove when:** the engine hides the view together with its top-level window. Check with the proxy running and
the window focused: hiding it to the tray and minimizing it must each log `AppLifecycleState.hidden`.
## macOS activation resumes a hidden window
- **Bug:** `handleWillBecomeActive` in the macOS engine's `FlutterEngine.mm` resumes when any window in
`NSApp.windows` is visible, and the tray's `NSStatusBarWindow` always is. Activating the app while its window was
minimized or hidden, such as Cmd-Tab back to a minimized window, reported `resumed`, and the next deactivation
`inactive`, so frames ran until the window was shown again.
- **Workaround:** the `window` plugin observes will-become-active and will-resign-active and, once the engine has
answered, sends `AppLifecycleState.hidden` on `flutter/lifecycle` while its window is not visible
(`applicationActivityChanged` in `WindowPlugin.swift`). A window brought back without another activation change
would then wait on an occlusion notification macOS does not always post, keeping its last frame, so `show` and a
deminiaturize hand the state back (`windowBecameVisible`).
- **Remove when:** the engine ignores status item windows on activation. Check without the workaround: minimize the
window, switch to another app, then Cmd-Tab back without restoring it; the log must stay at
`AppLifecycleState.hidden`.
## Linux minimize on Wayland reads as inactive
- **Bug:** the Linux engine reports `hidden` for `GDK_WINDOW_STATE_ICONIFIED`, but xdg-shell has no minimized state,
so GTK 3 never sets it on Wayland. A minimized window reported only its focus loss, as `inactive`, and kept
rendering at the display rate (GNOME 46).
- **Workaround:** the `window` plugin's Linux `minimize` records the time, and a focus loss within 500 ms of it sends
`AppLifecycleState.hidden`. Its `window-state-event` handler is connected after the engine's, so that message
follows the engine's `inactive`; the compositor focuses the window when it restores it, which the engine reports as
`resumed`. A minimize the compositor starts itself (Super+H, the dock) is not covered.
- **Remove when:** the Linux engine reports minimized windows on Wayland, which needs GTK 4 or xdg-shell's
`suspended` state. Check in a GNOME Wayland session without the workaround: minimizing from the caption button
must log `AppLifecycleState.hidden`.
## Liquid glass saves around the transform of a blurring layer
- **Bug:** not filed upstream. On 3.47 Impeller scales a `saveLayer`'s image filter by a transform set inside the
layer that is still in effect when the layer is restored. A host's capture shrinks each pane's slot inside the
layer that blurs it, so its blur vanished at a quarter scale and spread half again as far at 1.5.
- **Workaround:** `_LiquidGlassHostLayer._snapshot` sets each slot's transform inside a `save` of its own, and each
band's offset, restored before the layer is.
- **Remove when:** without that `save`, the frosted-pane case of `test/widgets/liquid_glass_host_test.dart` passes
under `flutter test --enable-impeller`.
+49 -85
View File
@@ -1,24 +1,27 @@
# 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.
FlClash is a multi-platform proxy client based on ClashMeta (mihomo), built with Flutter. It supports Android, Windows, macOS, and Linux, using Material You from `material_ui` as the base with Apple's Human Interface interaction conventions layered on top.
## Version Notes
- Release CI pins Flutter 3.47.1. Local SDK may diverge, so trust the CI
version as the source of truth for release builds.
- Dart SDK constraint: `>=3.8.0 <4.0.0`. The lower bound is load-bearing and
must not be raised to the Dart version the SDK actually ships; see
Dependency Ceilings.
- Flutter stays on the 3.47.x line. Release CI pins 3.47.6 and is the source
of truth for release builds; `.fvmrc` and local SDKs may sit on another 3.47
patch. The editor in `plugins/code_forge` came from `code_forge` 10.14.0, which
needs Dart 3.13.2, so the CI pin cannot go below Flutter 3.47.2.
- Before moving the pin, walk `flutter-upgrade.md`: it lists the workarounds tied to
the current SDK, such as Impeller disabled on the Intel macOS release build.
- The Dart SDK lower bound in `pubspec.yaml` sets the package language
version. `flutter analyze` excludes `lib/**/generated/**`, so a raise is
proven by `flutter test`, which parses the generated code, not by analyze.
## Forked Dependencies
Three `pubspec.yaml` dependencies are pinned to a fork — `window_manager` by tag,
the other two by commit SHA. All three
forks live under `chen08209`, the same account that owns this repository, so they
are maintained in-house rather than tracked from a third party: advancing a pin
is a local decision, and there is no external maintainer to wait on for the patch
itself. What each fork still waits on is the *upstream* fix that would let the
pin be dropped entirely, recorded below.
One `pubspec.yaml` dependency is pinned to a fork by commit SHA. The fork lives
under `chen08209`, the same account that owns this repository, so it is
maintained in-house rather than tracked from a third party: advancing a pin is a
local decision, and there is no external maintainer to wait on for the patch
itself. What the fork still waits on is the *upstream* fix that would let the pin
be dropped entirely, recorded below.
Each entry records what the fork changes and what has to be true before it can go
back to the published package, so a future upgrade does not have to rediscover it.
@@ -29,25 +32,11 @@ of the same number:
diff -ru ~/.pub-cache/hosted/pub.dev/<name>-<version> ~/.pub-cache/git/<name>-<sha>
```
`window_manager` — `chen08209/window_manager`, path `packages/window_manager`,
version 0.5.1, pinned to the tag `v0.5.1-flclash.1` because the fork carries
commits of its own rather than a single patch on top of a release.
- `windows/window_manager_plugin.cpp`: with `titleBarStyle: hidden` a maximized
window uses the monitor work area (`GetMonitorInfo().rcWork`) instead of
upstream's `adjustNCCALCSIZE` border fudge, so it no longer covers the taskbar.
- `linux/window_manager_plugin.cc`: GTK drops the placement of an unmapped
window, so `hide` saves the geometry and the `map-event` handler applies it
again. Upstream only moves the window while it is hidden, which a window
manager is free to ignore — the window then reappears wherever it decides to
place it, which on this repository's Linux runner is every appearance after the
first, because `my_application.cc` never shows the toplevel itself.
- Adds `setWindowCornerPreference` (Windows) and `handleShouldTerminate` /
`onWindowShouldTerminate` (macOS). These lived in a local `window_ext` plugin
until they moved here; `lib/manager/window_manager.dart` and
`macos/Runner/AppDelegate.swift` are the callers.
- Drop the fork once upstream carries all three. The added APIs have call sites,
so this is not a pin change alone.
The `window_manager` fork (`chen08209/window_manager`, tag `v0.5.1-flclash.3`)
was replaced by the in-repo `plugins/window` package; the behaviors it carried
(work-area maximize with a hidden title bar on Windows, geometry restore on
re-map on Linux, corner preference, terminate and activate events) live there
now, see the window plugin section of `architecture.md`.
`launch_at_startup` — `chen08209/launch_at_startup`, version 0.5.1.
@@ -58,68 +47,43 @@ commits of its own rather than a single patch on top of a release.
directly, and upstream's `^2.0.0` constraint cannot co-resolve with it.
- Drop the fork when upstream publishes a release that accepts `win32_registry` 3.x.
`yaml_writer` — `chen08209/yaml_writer`, version 2.1.0.
The `yaml_writer` fork (`chen08209/yaml_writer`) was replaced by
`lib/common/yaml_writer.dart`. Its key quoting let keys such as `!a`, `'a` or
`0x1F` reach the Core's yaml.v3 as a parse error or a non-string; a merge key
`<<` stays plain on purpose, because `package:yaml` keeps it as an ordinary key
and the Core applies the merge.
- Adds `StringNode.quoteKey()` and applies it to map keys in `lib/src/node.dart`.
Upstream quotes values but emits keys verbatim, so a profile key needing quotes
is written as invalid YAML.
- Drop the fork once upstream quotes map keys by the same
`isValidUnquotedString` rule it already applies to values.
The `code_forge` fork (`chen08209/code_forge`) is frozen: its Dart code now lives
in the in-repo `plugins/code_forge` package and its Rust code in the `editor/`
module of `plugins/rust_api`, both trimmed to what FlClash uses.
`plugins/code_forge/README.md` records what was kept and what was removed;
upstream's MIT notice stays in `plugins/code_forge/LICENSE`.
## Dependency Ceilings
Several dependencies cannot be advanced from this repository, and re-running
`flutter pub outdated` will keep listing them. The blocker is upstream in every
case, so treat the list as resolved-until-the-ceiling-moves rather than as debt:
`flutter pub outdated` lists only upstream-held entries, so treat them as
resolved-until-the-ceiling-moves rather than as debt:
`freezed` is pinned exactly to `3.2.6-dev.1`, which is a pre-release *ahead* of
the newest stable `3.2.5`. It is not a stale pin and must not be "fixed" by
moving to `3.2.5`: stable `3.2.5` requires `analyzer >=9.0.0 <11.0.0`, while the
pinned Flutter SDK resolves `analyzer` 12. `3.2.6-dev.1` is the only published
freezed release that accepts `analyzer` 12. Move to a stable release only once
one exists that accepts the analyzer the SDK actually resolves.
- `intl` is intentionally unbounded (`any`) and `material_color_utilities` is
resolved by the Flutter SDK; neither is a bound this repository sets.
The `>=3.8.0` Dart lower bound is a language-version floor, not a stale minimum.
Dart 3.13 makes `final` on a parameter an error, and `freezed` still emits it in
the constructors it generates for every collection field it backs with a private
field (`const _LogsState({final List<Log> logs = const [], ...})`); 4.0.0-dev.3
emits it too. A pubspec's lower bound sets the package language version, so
`>=3.8.0` keeps that generated code legal while the SDK itself runs 3.13. Raising
the bound makes every `*.freezed.dart` fail to parse, which `flutter analyze`
does not catch because `lib/**/generated/**` is excluded. Raise it only once
freezed stops emitting the modifier.
The `riverpod` set, `build_runner`, `drift_dev` and `intl_utils` resolve to their
latest releases. Two are held back:
One `analyzer` ceiling holds most of the remaining `flutter pub outdated` list.
The newest `build_runner`, `drift_dev`, `intl_utils`, `test`, and
`riverpod_generator` all require `analyzer` 13; `test` 1.31.2 additionally
requires `test_api` 0.7.13, while `flutter_test` from the pinned SDK depends on
`test_api` 0.7.12. Raising any of those bounds therefore fails version solving.
- `freezed` 4.0.2 needs `analyzer` 14, while `intl_utils` 2.8.16 still requires
`analyzer` ^13. It moves once `intl_utils` accepts analyzer 14.
- `test` 1.32.0 needs `test_api` 0.7.14, and `flutter_test` on Flutter 3.47 pins
0.7.12. It moves with the SDK.
The `riverpod` chain is the visible symptom. It is held at
`riverpod`/`flutter_riverpod` 3.3.2, `riverpod_annotation` 4.0.3, and
`riverpod_generator` 4.0.4 as one rigid unit, because `riverpod_annotation`
4.0.3 depends on `riverpod` exactly 3.3.2: the runtime cannot move without the
generator, and the generator cannot move without `analyzer` 13. Retry the whole
set after a Flutter SDK bump raises `test_api`, not before.
When an upgrade fails version solving, record the blocker here with
what has to change upstream before the pin can move, so the next
`flutter pub outdated` run does not rediscover it.
`intl` is intentionally unbounded (`any`) and `material_color_utilities` is
resolved by the SDK; neither is a bound this repository sets.
`isolate_contactor` is discontinued and `isolate_manager` is several majors
behind. Both arrive through `re_editor`, which pins `isolate_manager: ^4.1.5+1`
and is already at its own latest release. Nothing in this repository can advance
them.
`CorePalette` is deprecated in `material_color_utilities` in favour of
`DynamicScheme`/`CorePalettes`, but `dynamic_color` 1.9.0 — its newest release —
still exposes only `DynamicColorPlugin.getCorePalette()`, which returns the
deprecated type. There is no migration available from this repository short of
calling the `io.material.plugins/dynamic_color` method channel directly and
decoding the palette int list by hand, which is not worth owning. The deprecated
type is therefore confined to `GlobalState._initDynamicColor`, which converts it
to the two plain seed colours the app actually consumes; everything downstream
sees `DynamicColorSeeds`. Revisit when `dynamic_color` ships a non-deprecated
accessor — only that one function has to change.
`_initDynamicColor` in `lib/bootstrap.dart` reads the `dynamic_color` method
channel directly and decodes the palette list into a `TonalPalette`, instead of
going through `DynamicColorPlugin.getCorePalette()` and its deprecated
`CorePalette` return type. Everything downstream sees `DynamicColorSeeds`, so a
non-deprecated plugin accessor changes only that one function.
## Build Dependencies
+260 -84
View File
@@ -29,7 +29,9 @@ raw string.
### Corner Radius
Rounded corners are superellipses everywhere, not circular arcs. Use the superellipse API at each layer:
Rounded corners are superellipses (continuous corners) everywhere, never circular arcs; when a component offers
both, pick the superellipse. `test/lint/superellipse_corners_test.dart` fails on the circular APIs below in `lib/`. Use
the superellipse API at each layer:
- Shapes: `RoundedSuperellipseBorder` instead of `RoundedRectangleBorder`.
- Clips: `ClipRSuperellipse` instead of `ClipRRect`.
@@ -37,25 +39,71 @@ Rounded corners are superellipses everywhere, not circular arcs. Use the superel
`BoxDecoration(borderRadius: ...)`; borders move to the shape's `side`, and a `Container` with
`clipBehavior` still clips to the shape path.
- Canvas: `canvas.drawRSuperellipse(RSuperellipse.fromRectAndRadius(...))` instead of `drawRRect`.
- Ink: `InkWell.customBorder` with an `AppShape` instead of `InkWell.borderRadius`.
Passing `BorderRadius.circular(x)` as the `borderRadius` argument of these APIs is expected — it only
carries the corner magnitude; the rendered geometry stays a superellipse.
APIs that accept only `BorderRadius` keep circular corners, with the superellipse supplied by an
enclosing clip or shape where one is needed: `InkWell.borderRadius`, `OutlineInputBorder`,
`ScrollbarThemeData.radius`, and `smooth_sheets`' `MaterialSheetDecoration`. Fully round pills
(`BorderRadius.circular(999)` or half the shortest side) may stay circular — both geometries coincide
there.
Pills are no exception. `StadiumBorder` joins its round caps to the straight sides with a curvature jump, while
`AppShape.full`, a superellipse at full radius, blends them, and on a square it renders the exact circle, so it serves
circles too. Material 3 defaults every button, icon button, segmented button, and search bar to `StadiumBorder`;
`ThemeData.withAppShapes` replaces that for the button themes, and a local `FilledButtonTheme` or `IconButtonTheme`
replaces the app's, so merge the ambient `XButtonTheme.of(context).style` into it, as `CommonMinFilledButtonTheme`
does. `CircleBorder` and `BoxShape.circle` are fine for something that is always square.
APIs that accept only a `Radius`, such as `ScrollbarThemeData.radius`, keep circular corners; supply the superellipse
with an enclosing clip or shape where it shows. Framework widgets that paint their own circular corners with no shape or
decoration to override, such as the `Switch` track, are left as Material draws them. Tooltips take theirs from
`withAppShapes`, which sets `tooltipTheme.decoration`.
### Typography
Text sizes follow Apple's iOS Dynamic Type scale at its default size, on every platform. `AppTextStyle` in
`lib/ui/typography.dart` holds its eleven styles, `largeTitle` to `caption2`, with Apple's leading and weights, plus two
steps the scale lacks: `display` for a dashboard figure and `pageTitle` for an app bar. Take one with
`AppTextStyle.body.of(context)`, pass `emphasized: true` for the scale's emphasized weight (bold for the titles,
semibold for the rest), and change color or family with `copyWith`; a weight is either regular or emphasized. Text
sized to the shape it marks, such as a letter scaled to its icon, comes from `AppTextStyle.custom`. The dashboard's
roles (`DashboardText` in `lib/views/dashboard/style.dart`) are built on the same types. `ThemeData.withAppTypography`
maps Material's `TextTheme` slots onto the scale so Material components follow it; app code never reads those slots.
App bar titles sit at the leading edge in `pageTitle` on a root page and on any desktop page, at the leading edge in
`headline` on a pushed page in the mobile view, and in the bar's centre in `headline` on a bottom sheet. A title widget,
such as the editor's name field, stays at the leading edge. Past a back button a title keeps the bar's action inset
clear of the button on either side.
`test/lint/typography_test.dart` fails on `textTheme` anywhere in `lib/` but `typography.dart`, and on `fontSize:` or
`fontWeight:` outside `lib/ui/`, except in the code editor, whose text follows the editor font-size setting.
### Text Field Suggestions
On Android, Flutter maps `enableSuggestions: false` to the visible-password input type, not only to "no suggestions"
(`TextInputPlugin.inputTypeFromTextInputType` in the engine). Input methods then treat the field as a password: no
clipboard panel, no Chinese layouts, and on some systems a secure keyboard. With the app's clipboard permission denied,
that leaves no way to paste a link at all. Only the secret fields, `SecretTextFormField` and `FormRowSecretTextField`,
ask for a password keyboard; a link, key, rule or regex field that should not be corrected sets `autocorrect: false`
alone. `test/lint/text_field_suggestions_test.dart` fails on `enableSuggestions: false` or
`TextInputType.visiblePassword` anywhere else in `lib/`.
### Length Units
Flutter lengths are logical pixels, the unit Android calls dp. Comments, test names, docs and reports give them in dp,
as in `48 dp`, never px: a reader takes px for physical pixels and misjudges the size by the device pixel ratio. Write
"physical pixel" in full where the code really works in them, as the hairline snapping in
`lib/ui/list_row_separator.dart` and the Win32 frame metrics in `plugins/window` do; raster image sizes in
`tool/` and CSS under `site/` keep px. `test/lint/length_unit_test.dart` fails on a number followed by px in `lib/`
and `test/`.
CI gates formatting: `dart format --output=none --set-exit-if-changed lib test
tool plugins setup.dart` runs before `flutter analyze`.
Generated directories are excluded from analysis:
These directories are excluded from analysis:
- `build/**`
- `lib/l10n/intl/**`
- `lib/**/generated/**`
- `plugins/**`
- the platform runner directories: `android/`, `ios/`, `web/`, `windows/`, `macos/`, `linux/`
## Comments
@@ -83,11 +131,11 @@ judgment, and it lives in the rules below and in review. Whether comments are to
that contains them. This does not need approval.
- These are not comments and are exempt from every rule here, including the density count: analyzer and linter
directives (`// ignore:`, `// ignore_for_file:`, `// coverage:ignore`), license and copyright headers,
code-generation markers, and comments inside vendored upstream code such as `lib/widgets/open_container.dart`.
code-generation markers, and comments inside vendored upstream code such as `lib/ui/open_container.dart`.
### Density
`tool/check_comment_density.sh` fails a file whose added lines are more than 5% standalone comment lines, ignoring
`tool/hooks/comment_density.sh` fails a file whose added lines are more than 5% standalone comment lines, ignoring
diffs under 20 added lines so small edits are never caught. The number is calibrated on this repository's own history:
healthy changes sit at or under 3.6%, while the core fix that prompted the gate ran 22.4%.
@@ -97,7 +145,7 @@ it is forked upstream code, not a house style.
Three gates run the same script, and they do not have equal force. The `PostToolUse` hooks in `.claude/settings.json`
and `.codex/config.toml` run after the tool and hand offending lines back as feedback; neither can undo the completed
write. The `comment-density` pre-commit hook fails the commit, and it is the only hard gate that covers every tool.
Behavior is pinned by `tool/check_comment_density_test.sh`, which CI runs directly.
Behavior is pinned by `tool/hooks/comment_density_test.sh`, which CI runs through `tool/hooks/test.sh`.
When a change genuinely warrants more, raise the ceiling for that run rather than working around it:
`COMMENT_DENSITY_MAX=20 git commit`, or `SKIP=comment-density git commit` to step past it entirely.
@@ -115,7 +163,7 @@ how important it feels.
- **A fact that is true only at one call site, and is not visible from that call site, is the one thing a comment does
better than a test or a document.** Its value is being in the reader's line of sight at the moment of the edit.
`lib/common/constant.dart` carries one: the delay-test concurrency cap is bound to `delayTestConcurrency` in
`core/common.go`, and whoever changes that number must see the constraint on the same screen. This is the case a
`core/delay.go`, and whoever changes that number must see the constraint on the same screen. This is the case a
comment is for; the bar is that no test and no `.agents/` entry could hold the fact instead.
Both failure directions are real. Moving a local constraint into `.agents/` hides it from the person editing the line;
@@ -132,29 +180,27 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
a scope-specific cleanup API instead.
- The desktop IPC socket in `plugins/rust_api` admits a Unix peer only when its effective uid is the app's own or root:
the socket lives in `/tmp`, and the Core connects as root whenever it runs setuid for TUN or under the Linux Helper.
A launch mode that runs the Core as any other user has to widen `authorize_peer` in `ipc/platform.rs`, and Windows
A launch mode that runs the Core as any other user has to widen `admit` in `ipc/access.rs`, and Windows
keeps its identity check on the Dart side, which compares the named-pipe peer PID with the Core it launched.
- Keep the shared `CoreMethodCall`/`CoreMethodResponse` JSON envelope structurally identical across Dart, Go, JNI, and
desktop IPC. Do not double-encode `arguments`, `result`, or event batches.
- `core/message.go` carries three event queues, and the split is load-bearing: state (loaded, geo-update), delay, and
bulk (log, request). Delay and bulk evict their own oldest entry under backpressure; state uses `enqueueState`, which
never evicts, because a dropped `geoUpdate{updating:false}` leaves `isUpdatingProvider` stuck at true in the UI until
- Keep the shared `CoreMethodCall`/`CoreMethodResponse` JSON envelope structurally identical across Dart, Go, the
gomobile binding, and desktop IPC. Do not double-encode `arguments`, `result`, or event batches. A response whose
`file` is set was spilled to disk; a transport resolves it with `resolveSpilledResponse` before reading `result`.
- `core/message.go` carries three event queues, and the split is load-bearing: state (loaded, geo-update,
route-changed), priority (every other event), and bulk (log, request, DNS). Priority and bulk evict their own oldest
entry under backpressure, except DNS, which enters the bulk queue through `enqueueState` so a lookup burst cannot push
out the request events the connection list is built from; state uses `enqueueState`, which never evicts, because a dropped `geoUpdate{updating:false}` leaves `isUpdatingProvider` stuck at true in the UI until
`UpdatingAction` sweeps it as stale minutes later. Do not merge the tiers or give state eviction semantics. `enqueueState` drops silently on a full
queue and must stay that way: reaching it means the host stopped reading, which `logDeliveryError` already reports,
and reporting it from the message layer feeds the same batcher.
- `jni_get_string` in `android/core/src/main/cpp/jni_helper.cpp` `malloc`s and hands ownership to Go, which frees through
`free_string_func`. `quickSetup` relies on that: it reads its `*C.char` arguments inside a goroutine, after the JNI
wrapper has already returned. Switching the wrapper to `GetStringUTFChars`/`ReleaseStringUTFChars`, or freeing on the
C side, turns that read into a use-after-free.
- Go goroutines reach Java through `ATTACH_JNI()`, which attaches once with `AttachCurrentThreadAsDaemon` and detaches
from a `pthread_key` destructor at thread death. Do not restore a detach-per-call: `protect` runs once per outbound
socket and `onResult` once per event batch, and attach/detach takes ART's thread-list lock each time.
- Every JNI call into Kotlin must be followed by `jni_clear_exception`. A pending exception left in place aborts the
process on the next JNI call on that thread, so a throw in `protect`/`resolveUid`/`resolvePackage`/`onResult` becomes a
crash in unrelated code. For the same reason every one of those wrappers checks its `tun_interface`/`invoke_interface`
for `nullptr` first: ART aborts on a call through a null object, and a callback released by `TunHandler.clear` while a
connection is still resolving is exactly that.
- The Android bridge resolves an owner in two steps — `resolve_uid` then `resolve_package` — because mihomo fills
- Every method of the callback interfaces in `core/mobile` (`TunHandler`, `ResultHandler`) returns an `error`. gobind
clears a Java exception and hands it to Go only for a method that declares one; otherwise the exception stays pending
and the next JNI call on that thread aborts the process, so a throw in `protect`/`resolveUid`/`resolvePackage`/
`onResult` would crash unrelated code. `core/mobile` turns the error into the answer the system gives when it will not
help: `false`, `-1`, or an empty package name.
- `TunHandler`'s hooks in `core/lib.go` check `callbacks` under `th.mu` before every call. `clear` drops the callbacks
while connections may still be resolving, and a call through the nil interface panics on a mihomo goroutine, outside
every recovery.
- The Android bridge resolves an owner in two steps — `ResolveUid` then `ResolvePackage` — because mihomo fills
`metadata.Uid` from a procfs lookup that Android Q closed off. Collapsing them back into one call that returns only a
package name is what left every connection reporting uid 0, so `UID` rules matched nothing.
- The desktop delivery path in `core/server.go` must not report failures through `logError`. A log event is published to
@@ -162,12 +208,28 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
`logDeliveryError`, which writes to stderr and latches until a frame gets through or the next connection is installed.
A write that fails without putting a byte on the wire — host backpressure hitting `ipcWriteTimeout`, or a payload above
`maxIPCFrameSize` — drops that one frame and keeps the connection: the stream is still framed correctly, and tearing it
down here ends the read loop, and with it the Core process. Only a half-written frame desynchronizes the stream, and
that is the one case `send` closes on.
- Core method handlers in `core/hub.go` are synchronous. Anything that must not block the dispatcher is spawned by
`safeGo`/`safeGoDetached` in `core/method.go`, which recover; a bare `go` in a handler puts a panic outside every
recovery and kills the process, which on Android is the whole application. The `//export` entry points in
`core/lib.go` do not reach `handleMethodCall`, so each one carries its own recovery.
down here ends the read loop, and with it the Core process. A half-written frame whose write merely timed out is
resumed for as long as the stall lasts: Windows Modern Standby suspends the app while the Helper's Core keeps running,
so the host can stop draining for hours and still come back, and go-winio reports the expiry as its own `ErrTimeout`
rather than `os.ErrDeadlineExceeded`, which is why `send` checks `Timeout()`. The wait has no cap on purpose, and
`send` holds `writeMu` throughout, so every other frame — method responses and the single batcher goroutine behind
the event queues — waits behind the stalled one. Memory is bounded by the queues; what gives is delivery: the state
queue fills and `enqueueState` starts dropping, which is the case the `UpdatingAction` sweep above exists for. Nothing
on the Dart side restarts the Core over a stall: `CoreRpcClient` times each pending request out on its own and hands
the caller `null` (a `no_response` exception for message methods), and the sweep clears core-scope updating state
minutes later. Only a half-written frame that fails outright desynchronizes the stream, and that is the one case
`send` closes on.
- Core method handlers are plain functions that return their answer, registered in the `methodHandlers` table in
`core/method.go` through the adapter matching their shape. `dispatchMethodCall` already runs every call on its own
goroutine and `handleMethodCall` turns a panic into an `internal_error` answer, so a handler blocks for as long as its
work takes and never spawns a goroutine just to answer. Work that outlives the answer goes through `safeGo`, which
recovers; a bare `go` puts a panic outside every recovery and kills the process, which on Android is the whole
application. The exported Android entry points in `core/lib.go` do not reach `handleMethodCall`, so each one carries
its own recovery.
- A handler fails by returning an `error`: a `*MethodError` travels as it is and anything else under `core_error`. The
exception is `withMessage`, for the methods whose result the host reads as a message that is empty on success
(`_invokeMessage` in `lib/core/interface.dart`): there a plain error becomes that message and only a `*MethodError`
fails the call. Moving a method between the two changes what Dart receives.
- `dialer.DefaultSocketHook` and `process.DefaultPackageNameResolver` are installed exactly once, by `installHooks` in
`core/lib.go`, and never cleared. mihomo checks `DefaultSocketHook` for nil once and dereferences it again when the
socket is created (`component/dialer/socket_hook.go`), so clearing it while a dial is in flight calls a nil func
@@ -186,18 +248,21 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
mutual exclusion against each other and against `patchSelectGroup` — but not against a whole config apply, which is
what `configMu` made a proxy switch wait for, provider downloads included. `patchSelectGroup` takes `selectMu` under
`configMu`, fixing the order as `configMu` → `selectMu`.
- The delay-test semaphore is acquired with a slice of the caller's budget (`budget/delayTestQueueShare`), not
unconditionally and not with the whole deadline. Queueing and probing come out of one budget, so a test handed all of
it can spend it waiting and reach `URLTest` with nothing left, reporting a proxy it never contacted as unreachable.
The probe keeps the caller's original deadline, so whatever the queue did not use is still its own.
- Queueing for the delay-test semaphore and probing the node each get the full timeout in `handleTestDelay`. With one
shared deadline, a node that waited behind a saturated semaphore reached `URLTest` with almost nothing left, so a bulk
test of a large subscription reported whatever sat at the back of the queue as unreachable.
- A delay test that the Core does not answer is a fault of the Core or the channel, never a verdict on the proxy:
`handleTestDelay` returns inside its own budget on every path. `asyncTestDelay` therefore returns null instead of a
`handleTestDelay` returns within its two timeouts on every path. `asyncTestDelay` therefore returns null instead of a
`-1` delay, and `ProxiesAction` leaves the last measurement in place and abandons the rest of the run. Writing a
timeout there is what made a reachable node read as unreachable whenever the host deadline beat the Core's.
- Delay-test progress lives in `pendingDelayTestsProvider`, not as a sentinel value in `DelayDataSource`. A delay of 0
used to mean "testing", which let a result and the state of a test overwrite each other and left cards spinning
forever when the Core restarted. The run owns its keys and releases them in a `finally`, so nothing depends on a
reply arriving; core status leaving `connected` cancels every run in flight.
- Some constants in `lib/common/constant.dart` mirror the Core and move with it. `delayTimedOutValue` is the `-1` that
`core/delay.go` reports for a delay test that ran out of time; any other negative value failed outright.
`downloadTimeoutDuration` matches mihomo's `resource.DefaultHttpTimeout`, which bounds the Core's provider downloads
end to end. `shareLinkSchemes` are the schemes mihomo's `ConvertsV2Ray` reads, less http(s), which is a subscription.
- Anything on the mihomo side that is reached from both a user-triggered core method and mihomo's own background
scheduler needs its in-flight guard on the FlClash side. `updater.UpdateMMDB` and its siblings have none — only the
batch `UpdateGeoDatabases` does — and two concurrent runs close the mmap'd database twice, so `handleUpdateGeoData`
@@ -211,9 +276,10 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
- Package `init` in the Android library runs while the `.so` is being loaded, so a panic there takes the application
down before it can report anything. `platform/limit.go` arms an fd-pressure probe and degrades to never blocking when
it cannot; keep that shape for anything else `init` sets up that correctness does not depend on.
- Every `android && cgo` file in `core/` is compiled only by the NDK-backed CI step in the `go` job. Keep the build
constraints as `android && cgo` / `!(android && cgo)`: a bare `cgo` constraint makes `go build ./...` fail in `core/`
on any developer machine, because the files it pulls in need the NDK.
- No file in `core/` uses cgo: the only C in the Android library is the glue gobind generates during `gomobile bind`.
Android-only files take `//go:build android` and desktop-only ones `!android`, so `GOOS=android CGO_ENABLED=0 go vet`
checks the Android side without an NDK. Keep it that way; cgo in `core/` puts the NDK back in front of that vet and of
`go build ./...` on every developer machine.
## Lifecycle Rules
@@ -224,15 +290,10 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
failure (user stop, low memory, signal, package change), never create one, and its timestamp is consumed through
`handledExitAt` so the same exit cannot be counted twice.
- `BootGuard` is an Android-only mechanism and gates itself: on every other platform `evaluate`, `markRunning` and
`markClosed` return without touching preferences. The desktop has neither of the two attribution sources — no
`ApplicationExitInfo` equivalent and no Crashlytics toggle — so a bare sentinel there would read a window closed on
the disclaimer dialog, or a session ended by shutdown, as a failed launch. Keep the platform check inside the guard;
callers in `bootstrap.dart` and `SystemAction` stay unconditional.
- `FirebaseCrashlytics.didCrashOnPreviousExecution()` corroborates a failure and must not trigger one. Its marker file is
cleared by a background initialization that `dataCollectionArbiter` blocks while collection is disabled, which is the
default here, so one real crash makes it return true on every later launch. That latch is what made the app clear the
selected profile on every cold start; it is also why the probe is only read when `crashlytics` is enabled, keeping
Firebase uninitialized until the user consents.
`markClosed` return without touching preferences. The desktop has no `ApplicationExitInfo` equivalent to attribute an
exit, so a bare sentinel there would read a window closed on the disclaimer dialog, or a session ended by shutdown,
as a failed launch. Keep the platform check inside the guard; callers in `bootstrap.dart` and `SystemAction` stay
unconditional.
- Recovery is graded: the first failed launch only skips `initStatus`, and `currentProfileId` is cleared only from
`crashRecoveryClearThreshold` consecutive failures on. Do not let a single interrupted launch write to the config.
- Desktop process ownership belongs to `DesktopCoreLifecycle`; do not start/kill `FlClashCore` from providers, widgets,
@@ -240,13 +301,24 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
- `CoreController.close()` and platform `close()` implementations are terminal and idempotent. Application shutdown must
stay centralized in `SystemAction`/`SystemExitCoordinator`.
- Android start/stop MethodChannel calls are optimistic UI commands. Keep latest-wins arbitration in native
`ServiceState`; do not add a Flutter completion callback that creates a second lifecycle owner.
`ServiceState`; do not add a Flutter completion callback that creates a second lifecycle owner. What flows back is
the revision-stamped `stopped` report, which Flutter mirrors with `setRunning(false)` and never answers with a
decision of its own.
- Android `RunState` is derived in `ServiceStateMachine.publish`; do not assign it from a transition step, and do not
add a second "is it running" field beside `ServiceController.runtime`. Every divergence between the tile and the
service so far came from a copy that one path forgot to update.
- Android service callbacks are not automatically user intent. Route explicit Quick Settings, Always-on VPN, and revoke
actions through `ServiceState` and keep `ServiceController` as the sole binding/run-time owner.
- Every `BroadcastReceiver.goAsync()` path must finish its `PendingResult` exactly once. A watchdog may release the
broadcast lease, but must not cancel, reverse, or otherwise redefine the service operation.
- Anything that probes through the outbound route is a `RoutedProbe` judged against `routeTrackerProvider` (Route
Consistency in `.agents/architecture.md`); the Core stamps every answer and publishes its picks, so do not add a
change signal, a trigger counter, or a `coreStatusProvider` watch of your own.
- Presentation smoothing such as `CoreStatusButton`'s connecting hold must remain local display state. It must not delay or
overwrite `coreStatusProvider`, and a real failure must bypass/cancel the hold immediately.
- Safe mode (`SAFE_MODE` define, see the safe mode section of `.agents/architecture.md`) is consulted by each host
integration at its owner, never by rewriting the user's settings. `safeModeBuild` is for handles created before the
container exists; everything else reads `safeModeProvider` so a test can override it.
- `Tray.hide()` is idempotent on all three desktop platforms and returns native state to "`show` was never called".
`AppTray.shutdown()` latches, so no later `update()`/`updateTitle()` can resurrect the icon once shutdown begins.
Keep it that way; a resurrected icon outlives `exit(0)` as a Windows ghost icon, because `setPreventClose(true)`
@@ -255,9 +327,11 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
declares desired state through one `Tray.show(TraySpec)` call and must not add platform branches to work around
ordering. Platform branches in `lib/common/tray.dart` are only for deliberate product differences (macOS speed title
and group submenus); query `Tray.instance.capabilities` for ability differences.
- Every native `show` returns whether the tray now reflects the payload, and reports `false` instead of showing a broken
- Every native `show` returns whether the tray accepted the payload, and reports `false` instead of showing a broken
icon. `Tray` caches the payload signature only on `true`, so a rejected `show` is retried by the next update rather
than suppressed until restart. Any test that mocks the `tray` channel must return `true` from `show`.
than suppressed until restart. Windows accepts a payload the shell refused, as at sign-in, and retries the icon
natively until it lands, so `Tray` still treats it as visible and `hide` reaches native to delete an add that may
have landed. Any test that mocks the `tray` channel must return `true` from `show`.
- Reading the Wi-Fi SSID is opt-in work, not ambient state. `ConnectivityManager` reads it only while `excludeSSIDs` is
non-empty, because that list is its only consumer through `suspendProvider`, and the read costs a blocking platform
call plus a location permission on Android and macOS. A second consumer must widen that gate, not drop it.
@@ -271,16 +345,40 @@ leaving a repo-wide policy as a comment reaches only the reader of that one file
outstanding exception: `WlanQueryInterface` still runs inline, since replying off-thread there needs a window-message
hop that the plugin API does not provide.
- The delayed DNS re-check `NetworkObserveModule.onLosing` posts is deliberately left un-deduplicated. The runnable
re-reads `networkInfos` and does nothing when `onLost` already dropped the network, `updateDns` returns early when the
resolved list is unchanged, and `stop()` clears the handler queue, so a network that reports `onLosing` repeatedly
costs one comparison per event. Holding the pending `Runnable` to `removeCallbacks` it changes no behaviour and adds
re-reads `networkInfos` and does nothing when `onLost` already dropped the network, `updateDns` calls into the Core
only when the resolved list or the preferred network changed, and `stop()` clears the handler queue, so a network that
reports `onLosing` repeatedly costs two comparisons per event. Holding the pending `Runnable` to `removeCallbacks` it changes no behaviour and adds
state that has to stay in sync with the map.
## UI Isolate Work
Everything the UI isolate runs synchronously shares a frame with build, layout and paint: 16 ms at 60 Hz, 8 ms at
120 Hz. Computation may take as long as it needs anywhere else, so this rule is about where work runs, not how much of
it there is.
- Work that grows with the user's data (profiles, rules, proxies, groups, dialers, icons, connections) and can take
more than about 8 ms on the largest data the app expects runs in an isolate, through `compute` or `Isolate.run`.
Thousands of rules or proxies and hundreds of groups are in that range. `makeRealProfileTask` in
`lib/common/task.dart` and the backup tasks in `lib/common/backup.dart` are the existing shape.
- Make the algorithm linear or cached first. Work superlinear in that data, such as a walk of the whole graph per
item, a scan of every group per group, or a list copy per inserted element, is a defect on any isolate; moving it
only hides the cost until the data outgrows the move.
- Sending data to an isolate copies it on the sending isolate, so hand the task the smallest input it reads. A value
the UI reads synchronously, such as a sync provider watched in `build`, becomes an async provider with a loading
state rather than a blocking call.
- drift runs SQL on its own isolate but maps rows into models on the caller's, so a watched query over a large table
costs the UI isolate on every emission, and any write to that table makes it emit again.
- Bounded work stays where it is: a provider recompute that reuses a cache, a single-item update, a search filter over
a list sized for scrolling.
- Decide on measurements taken at the expected maximum. A `flutter test` probe that counts provider recomputes, times
the provider body and hooks `debugOnRebuildDirtyWidget` shows what reruns and how it scales; its JIT timings are not
device numbers, so a case near the line is settled with a profile build. No gate checks this rule.
## Testing Rules
The `core/` directory is excluded from automated coverage accounting. Do not add coverage instrumentation or coverage
collection for code under `core/`. CI still runs `CGO_ENABLED=0 go test .` and `go vet .` to compile/check the Go wrapper,
plus an NDK-backed `GOOS=android` vet that covers the `android && cgo` files the first two exclude; verify cross-language
plus a `GOOS=android` vet that covers the `android` files the first two exclude; verify cross-language
protocol behavior through shared Dart contract tests under `test/core/` and native platform build checks.
A Go test in `core/` that reaches `sendMessage` — directly, or through `handleStartLog` or `updater.GeoUpdateHook` —
@@ -290,8 +388,6 @@ connection `captureFrames` has installed by then. Either keep the event out of t
connection with `swapConn`; a bare assignment races every event it happens to be delivering, and `go test -race` catches
it in an unrelated test.
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.
`tool/check_coverage.dart` enforces a total floor passed by CI plus per-group floors declared in `_groupFloors`. Raise a
@@ -304,7 +400,32 @@ land.
Prefer `coreHandlerProvider.overrideWithValue(CoreController.scoped(fake))` over `CoreController.test(fake)` in new and
touched tests. `CoreController.test` claims the process-wide singleton, which makes a global read and a provider read
resolve to the same fake, so it cannot fail on a call site that still reaches for the global.
resolve to the same fake, so it cannot fail on a call site that still reaches for the global. A test that does claim
the singleton calls `CoreController.resetInstance()` in `tearDown`. It also only reaches the global `coreController`
when nothing has read it yet: that is a top-level `final`, bound to whichever instance existed at the first read, which
in a shared isolate is another file's.
### Shared Test Isolate
`tool/test.dart` imports many test files into one entrypoint, so process-wide state outlives the file that set it, and
every file's `main()` runs before any test of the entrypoint does. Keep `main()` to declarations and
`TestWidgetsFlutterBinding.ensureInitialized()`: a mock installed there is replaced by the next file's before a test
sees it. Set up what the tests need in `setUp` or `setUpAll` instead, from the helpers that rebind the singletons:
- `appPath` resolves its directories once per process. Point them at a test's directory with `await useAppPaths(dir)`
from `test/helpers/app_paths.dart`, not with a `PathProviderPlatform` fake, which only works while nothing has asked
`appPath` yet.
- `preferences` opens its store once per process. Use `await mockPreferences(values)` from
`test/helpers/preferences.dart` rather than `SharedPreferences.setMockInitialValues` alone, which leaves `preferences`
on the store it already opened.
Both cache the resolved value, not the future that produced it. A future completed in one test's fake-async zone runs
its listeners in that zone, so a later test that awaits it never resumes. Await the helpers outside `testWidgets`
bodies for the same reason.
A test that only passes in a fresh isolate is leaning on a global it did not set up: `database_test.dart` once caught
its write failures only because `currentAppLocalizations` threw before anything had loaded them. The script reruns such
a file alone so it cannot fail the run, and lists it; fix it where it reads the global.
Construct the Android lib handler with `CoreLib.scoped(fakeService)`. The `service` global is gated on `Platform.isAndroid`
and is therefore null on every test host, so a `CoreLib()` built from it silently takes the null-service fallback on every
@@ -317,10 +438,10 @@ key), so a test that skips the seam registers the test binary on the machine tha
under `kDebugMode`, which is always true beneath `flutter test`, so its remaining branches cannot be reached from a test
at all; `test/common/launch_test.dart` pins the early return instead.
`pumpAndSettle` never returns on a page holding `EditorPage`: the code editor blinks its caret forever, so frames keep
being scheduled. Pump explicitly instead. `encodeYamlTask` and its neighbours in `common/task.dart` hand work to a real
isolate through `compute`, which only runs outside the fake-async zone, so a test awaiting one needs
`tester.runAsync(...)` between the pumps — see `test/views/profile_preview_test.dart`.
`pumpAndSettle` never returns on a page holding `EditorView` (what `EditorPage` embeds): the code editor blinks its
caret forever, so frames keep being scheduled. Pump explicitly instead. `encodeYamlTask` and its neighbours in
`common/task.dart` hand work to a real isolate through `compute`, which only runs outside the fake-async zone, so a test
awaiting one needs `tester.runAsync(...)` between the pumps.
The `@visibleForTesting` `database` setter in `lib/database/database.dart` deliberately does not close the instance it
replaces. Tests inject `NativeDatabase.memory()`, which holds no file handle, and `Database.close()` is async while the
@@ -350,15 +471,13 @@ The same seam carries the two other places a platform decision changed what was
takes nullable `isAndroid`/`isMacOS` that fall back to the host. Every test names the platform it means, because
`debugDefaultTargetPlatformOverride` does not move `system` and a suite that leaves it to the host asserts the macOS
shape on a developer machine and the Linux one on CI. `WindowHeaderContainer` builds the caption buttons on every
non-macOS host, so a test mounting it needs `TestApp` for `AppLocalizations` and a `window_manager` channel mock that
non-macOS host, so a test mounting it needs `TestApp` for `AppLocalizations` and a `window` channel mock that
answers `isMaximized`/`isAlwaysOnTop` with a bool.
Auto-dispose providers need a container-level hold before a test reads them back. `proxyGroupProvider`, `ruleProvider`,
`itemsProvider` and friends mix in `AutoDisposeNotifierMixin`, so a `container.read` that no widget is currently watching
rebuilds the provider from its override and silently discards whatever the code under test wrote. Add
`container.listen(theProvider, (_, _) {})` in the harness, as `overwrite_stage_flow_test.dart` does. The staging flow also
re-arms its debounce when it clears the stage, so drain it (`pump` past the duration, then unmount) or the binding fails
the test on a pending timer.
`container.listen(theProvider, (_, _) {})` in the harness, as `test/providers/action_test.dart` does for `scriptsProvider`.
A field that constructs its own `ValueNotifier`, `TextEditingController`, `ScrollController`, `FocusNode`, `TabController`,
`PageController`, `AnimationController` or `StreamController` must be released in the same file.
@@ -390,13 +509,27 @@ A public top-level declaration that nothing outside its own file references is d
riverpod notifier is reached through its generated provider) and barrels as neither. Files publishing only extensions or
typedefs are skipped: those are reached through the types they attach to, never by name.
`CommonScaffold` adds the height of its docked search to `BottomInsetScope` around its body, so a page that reads
`BottomInsetScope.of(context)` with the context it builds the scaffold from gets the inset without it, and the end of
its content stays under the search field. Read it below the scaffold,
inside `body: Builder(builder: (context) => ...)`. `test/lint/scaffold_bottom_inset_test.dart` enforces this by
scanning `lib/`.
Every reorderable list builds its drag proxy with `commonProxyDecorator`, which tells `DecorationListItem` it is lifted:
the row drops its separator and rounds from its corners in the run to a card of its own as it lifts, and back as it
drops. A list without it lifts a middle row with square edges. `test/lint/reorder_proxy_decorator_test.dart` enforces
this by scanning `lib/`.
A `State.dispose()` override must not await before `super.dispose()`. `StatefulElement.unmount` calls `dispose()` and then
immediately asserts that `super.dispose()` already ran, so an `await` defers the call past the assert and every teardown
throws "`…State.dispose failed to call super.dispose.`" in debug and profile builds. Declare the override as `void
dispose()` and hand async teardown to `unawaited(...)`; `Future<void> dispose() async` compiles and is the shape that
invites the bug.
Use `ProviderContainer` directly for simple Riverpod provider tests. The generated Riverpod `update()` method takes a callback:
Use `ProviderContainer` directly for simple Riverpod provider tests, released with `addTearDown(container.dispose)`.
Assert against the provider's generated public API, not its implementation, re-read source defaults before asserting
them, and wait on provider futures or state changes rather than fixed sleeps. The generated Riverpod `update()` method
takes a callback:
```dart
notifier.update((state) => newValue);
@@ -407,10 +540,21 @@ When testing freezed models with nested objects, always round-trip through `json
For async widgets, put visual cleanup in `finally` when the action may throw. Focused widget tests should cover success,
failure, disposal, and any timer boundary that changes visible state.
## Running the App Locally
- An agent starts FlClash on the host only as a safe mode build (`--dart-define=SAFE_MODE=true`, see
`.agents/commands.md`). A normal build takes over the host network, so the user runs it, never the agent.
- Never end a FlClash process the agent did not start, and never kill by name (`pkill`, `killall`, a pattern on the
executable path): the user's own instance matches too. Record the pid of every process you launch, check with
`ps -o pid,command -p <pid>` that its executable is your own build output, and end only that pid.
- Only a macOS or Android debug build has its own data directory; every other build shares the installed app's data
directory and single-instance lock, so it cannot start while the user's instance runs. `.agents/commands.md` lists
which builds are kept apart.
## Commit Messages
Subjects follow Conventional Commits and are enforced by the `commit-msg` hook in `.pre-commit-config.yaml`, which runs
`tool/check_commit_msg.sh`:
`tool/hooks/commit_msg.sh`:
```text
<type>[(scope)][!]: <description>
@@ -431,6 +575,18 @@ Write what the change does, not that something changed: `perf(views): stop redoi
Install the hooks once with `pre-commit install --hook-type pre-commit --hook-type pre-push --hook-type commit-msg`.
### Fixup Commits
A follow-up to a change that has not shipped in a stable release goes in as `git commit --fixup=<target>`, aimed at the
commit that introduced what it amends.
- Only stable tags (`vX.Y.Z`) close a commit. A target reached only by pre-release tags (`vX.Y.Z-pre.N`) is still open
for fixups, so do not steer away from it or flag it.
- A commit a stable tag contains takes no fixup; land the follow-up as a regular commit. Check with
`git tag --contains <target> | grep -v -- '-pre'`, which must print nothing.
- Asking for a fixup means creating the `fixup!` commit only. Do not run `git rebase --autosquash` or otherwise fold it
into its target unless the user asks for the squash.
### Changelog Trailers
`tool/changelog.dart` builds the user facing changelog from the commit history, so the trailers below are the copy that
@@ -443,16 +599,36 @@ Changelog: Per-profile override scripts
```
- `Changelog:` is the English entry. `Changelog: skip` drops the commit from the changelog entirely.
- The changelog is English only. Translation trailers were removed on purpose: they pushed release copy into the commit
history, so any `Changelog-<locale>:` or `Breaking-<locale>:` now fails the hook. Translate after the fact if ever
needed, not in the commit message.
- The changelog is English only. A `Changelog-<locale>:` or `Breaking-<locale>:` trailer fails the hook, because
translated release copy does not belong in the commit history. Translate after the fact if ever needed, not in the
commit message.
- `Changelog-Type:` moves an entry into another group, for example to promote a `refactor` that users will notice. Valid
values are `breaking`, `feat`, `fix`, `perf`, `revert`.
- `BREAKING CHANGE:` is required whenever the subject carries `!`, and its text becomes the breaking entry. A `!` commit
therefore needs two lines of copy: the footer for the breaking entry and `Changelog:` for the normal one.
`feat`, `fix`, `perf`, `revert` and breaking commits are collected by default; every other type is dropped unless it
carries a `Changelog:` trailer. Commits missing a trailer reuse their subject, and the hook says so without blocking.
carries a `Changelog:` trailer. Commits missing a trailer reuse their subject; for `feat`, `fix` and `perf` the hook
says so without blocking.
## Migrations
An upgrade starts from the last release, not from the previous development build. Before writing a legacy reader, a
data move, or a test for one, check that the state it reads shipped: `git tag --contains <commit>` on the commit that
introduced it, or `git show <tag>:<path>` for the file as the release had it. A state that only ever existed between
two releases gets no migration; a development install holding it may lose that setting or fall back to its default.
A release that resets or rewrites settings, instead of reading their old shape, adds a step to `_upgrades` in
`lib/common/migration.dart` under the next `upgradeVersion`. A step takes and returns the `Config`, runs once on an
install that already had settings, and never on a new one. Leave `currentVersion` for changes to the stored format:
backups carry it, and an older app refuses a backup stamped newer than its own, so bumping it for a reset would strand
backups between devices on different releases.
The database takes one migration step per release. The newest step in `lib/database/database.dart` has not shipped
while `schemaVersion` at the last release tag is below its number; a schema change then goes into that step, raising
`schemaVersion` and the step's condition together so a development install runs the step again, instead of adding a
step of its own. Every operation in it checks the shape it finds, a missing table or column or a column under its old
name, so it can run again on a database that went through part of it. Its tests start from the released shape.
## Generated Code
@@ -462,6 +638,7 @@ Do not manually edit generated files under:
- `lib/models/generated/`
- `lib/providers/generated/`
- `lib/database/generated/`
- `lib/ui/generated/`
- `lib/l10n/intl/`
After schema, model, or provider changes, run build generation and include focused tests when behavior changes.
@@ -475,13 +652,12 @@ l10n paths for that reason. It is harmless because the file only needs `Locale`,
human writes takes Material from `material_ui`; `cupertino_ui` is banned outright and
survives only as a transitive dependency of `material_ui`.
Strings live in `arb/intl_{en,zh_CN,ja,ru}.arb` — flat JSON, no `@` metadata. Add a key to all four, then regenerate with
Strings live in `arb/intl_{en,zh_CN,ja,ru,fr,es,ko}.arb` — flat JSON, no `@` metadata. Add a key to all seven, then regenerate with
`dart run intl_utils:generate`, which rewrites `lib/l10n/`. A key present in only some locales silently falls back to
English at runtime, so add the translation rather than leaving it out.
Some labels are not reached through the generated `AppLocalizations` getters at all. `Intl.message(<runtime string>)`
builds the key from an enum name or a stored string — `action_${HotAction.name}`, `${DynamicSchemeVariant.name}Scheme`,
`NavigationItem.description`. The analyzer sees nothing, and a failed lookup returns the key itself, so a stale key ships
as `routeMode_config` in the UI rather than throwing. `test/lint/dynamic_message_key_test.dart` expands those families
from the real enums and fails when a derived key is missing from any locale; every `Intl.message` site in `lib` must be
registered there, so a new dynamic key cannot be added without also declaring what builds it.
Labels for enum values go through the exhaustive switches in `lib/common/l10n_labels.dart`, never
`Intl.message(<runtime string>)`. A key built from an enum name or a stored string is invisible to the analyzer, and a
failed lookup returns the key itself, so a stale key ships as `routeMode_config` in the UI rather than throwing.
`test/lint/dynamic_message_key_test.dart` fails on any `Intl.message(` in `lib` outside the generated code, and checks
that every locale carries the same keys and has a display name.
+147
View File
@@ -0,0 +1,147 @@
# Setting Changes
A changed setting takes one of three paths. The path depends on who reads the setting and when it reads it, not on
which page edits it. Choose the path when you add a setting or move one.
| Path | Who reads the setting | Watched provider | Acts | Effect |
|---------|---------------------------------------------|----------------------------------------|-------------------------------|--------------------------------------------|
| Update | Core, and Go can change it in place | `updateParamsProvider` | `CoreManager`, automatically | Patches the running config, no reload |
| Apply | Core, only when it parses a config | `setupPatchProvider` | `CoreManager`, automatically | Rebuilds the profile and reloads the Core |
| Restart | Android service, when it starts | `vpnOptionsProvider` | `VpnManager` asks the user | Stop and start of the service |
Update and apply run on their own, debounced. A restart never does: rebuilding the tunnel cuts every connection
through it, so `VpnManager` only shows a notifier and the user taps its restart action. A setting that only Flutter
reads takes no path.
## Update
`CoreManager` hands each change of `updateParamsProvider` to `SetupAction.updateConfigDebounce`. `updateConfig` then sends
`PatchClashConfig.toUpdateParams` to Go's `updateConfig` in `core/config.go`. That function writes the fields into the
running config and calls the runtime setter for each one, or `updateListeners` for the inbounds. Nothing is re-parsed.
The update path carries these fields:
- From `PatchClashConfig`: `mode`, `log-level`, `mixed-port`, `allow-lan`, `ipv6`, `find-process-mode`,
`tcp-concurrent`, `unified-delay`, `external-controller`, `secret`, `geo-auto-update`, `geo-update-interval`,
`geox-url`, and the `tun` keys of `Tun.coreJson`: `enable`, `device`, `stack`, and `auto-route`, always on.
- From other settings: `NetworkProps.authentication`, and `AppSettingProps.checkCertificate`, sent as
`skip-cert-verify`.
On desktop, `updateListeners` rebuilds the TUN from the patched `tun`. Enabling TUN may need authorization first. When
`requestAdmin` gets it newly granted, `updateConfig` restarts the Core instead of patching it. On Android,
`updateListeners` skips TUN, so a `tun` update reaches the VPN only through a restart.
To move a setting onto this path:
1. Add it to Dart `UpdateParams` and `toUpdateParams`.
2. Add it to Go `UpdateParams` in `core/protocol.go` and apply it in `updateConfig`.
3. Reset it in `PatchClashConfig.setupOnly`. If you skip this, every change also reloads the profile. The
`PatchClashConfig.setupOnly` test in `test/models/config_test.dart` fails until its fixture changes the field and
`setupOnly` resets it.
4. Keep writing it in `_makeRealProfileTask`, so a full apply hands the Core the same value.
Prefer update whenever Go has a setter for the setting, because an apply reloads the whole config.
## Apply
`CoreManager` answers each change of `setupPatchProvider` with `applyProfileDebounce(silence: true)`. That call
rebuilds the profile YAML and hands it to Go's `applyConfig` through `setupConfig`. The provider holds three values:
- `PatchClashConfig.setupOnly`: the config with every field that update carries reset to its default.
- `NetworkProps.appendSystemDns`.
- `NetworkProps.bypassPrivateRoute` on desktop, where the task adds the private ranges to the TUN's final
`route-exclude-address`.
A change to anything else in `PatchClashConfig` therefore reaches it. Today that is the DNS and TUN overrides (`dns`,
`dnsOverrideKeys`, `tunOverride`, `tunOverrideKeys`), `hosts`, the extra inbound ports (`port`, `socks-port`,
`redir-port`, `tproxy-port`), the dialer options (`keep-alive-*`, `disable-keep-alive`, `interface-name`, its mode,
`routing-mark`), `geodata-loader` and `global-ua`. A new `PatchClashConfig` field written by `_makeRealProfileTask` lands here without further work. A value
the profile build reads from another settings object has to be added to `setupPatchProvider`.
An apply without `force` sends nothing when the md5 of the generated YAML has not changed. A value that only
`SetupParams` carries never changes the YAML, so this path does not deliver it. `testUrl` is that case: Go's default
health-check URL follows it only when some other change or a `force` apply reaches the Core.
Profile content does not go through `setupPatchProvider`. `setupStateProvider` reads rules, groups and scripts from the
database once instead of watching them. Content is applied explicitly by whatever changes it:
- Profile editors call `autoApplyProfile` when they close.
- Subscription updates, scripts and app-level providers call `applyProfileDebounce`.
- A profile switch runs `fullSetup`.
- A start applies again with `force` unless the last apply saw the same inputs (`_inputsApplied`), the `testUrl` and
`skipCertVerify` that only `SetupParams` carries included.
## Restart
`VpnService.handleStart` builds the tunnel once from `VpnOptions` and never reads them again.
`ServiceController.start` uses `enable` to choose between `VpnService` and `ProxyService`.
`vpnOptionsProvider` derives the options the service would start with now. `runningVpnOptionsProvider` records what
the last listener start handed it. The route list is subtracted in an isolate by `vpnRouteAddressProvider`, which
reruns only when the route lists or `bypassPrivateRoute` change, so `vpnOptionsProvider` is async: a start awaits it
before recording, the Android Core awaits it before its first `syncState`, and the shared state keeps the last options
while new routes compute rather than handing the service an empty list. `SetupAction` owns every start and stop: it
sets the record on a start and clears it on a stop, on-demand SSID suspension included.
`VpnManager` shows the notifier whenever a change leaves `VpnOptions.effective` different from the recorded options.
Repeated changes refresh the same notifier instead of stacking new ones. Changing a setting back to the running value
asks for nothing. The tap runs `SetupAction.restartVpn`: a stop, then a start, and only while a run is up.
| `VpnOptions` field | Taken from | The service reads it |
|------------------------|---------------------------------------------------------------|----------------------------------------|
| `enable` | `VpnProps.enable`, off in safe mode | always |
| `stack` | `Tun.stack` | with the VPN |
| `ipv6`, `dnsHijacking` | `VpnProps` | with the VPN |
| `allowBypass` | `VpnProps` | with the VPN |
| `accessControlProps` | `VpnProps.accessControlProps` | `enable`, `mode` and that mode's list |
| `systemProxy` | `VpnProps.systemProxy`, off under local auth or safe mode | with the VPN |
| `port` | `PatchClashConfig.mixedPort` | with `systemProxy` |
| `bypassDomain` | `NetworkProps.bypassDomain` | with `systemProxy` |
| `routeAddress` | `Tun.routeAddress` and `bypassPrivateRoute` | with the VPN |
| `mtu` | `Tun` | with the VPN |
| `congestionController` | `Tun` | with the VPN |
The service never reads the access list's sort and filter fields. `effective` resets them, along with every field
the table does not mark as read, so changing them does not ask for a restart.
To add a VPN option:
1. Add it to Dart and Kotlin `VpnOptions` and to `vpnOptionsProvider`.
2. If the service reads it only under a condition, put that condition in `VpnOptions.effective`, which mirrors
`ServiceController.start` and `VpnService.handleStart`.
The notifier stays off while nothing runs, during on-demand suspension, in safe mode, and on desktop, where
`VpnManager` is not mounted. When Flutter reattaches to a service that is already running, it records the current
options, because the service does not report the options it was started with.
## One Setting, Several Paths
Each path watches its own provider, so a single setting can take more than one of them:
- `mixedPort`: update moves the Core's listener; under the VPN system proxy, the restart notice follows.
- TUN switch and stack: update rebuilds the desktop TUN. On Android the update reaches no TUN, and the restart notice
carries the stack.
- MTU, congestion control and routes: Android's rows take the restart path; on desktop they are TUN overrides, applied
with the profile.
- `bypassPrivateRoute`: apply on desktop, restart on Android.
- Local authentication: update applies it to the inbounds. It also switches the VPN system proxy, which needs a
restart.
## Choosing The Path
For a new or moved setting, answer each question. More than one answer can be yes.
1. Does the Android service read it when it starts? Then it belongs in `VpnOptions` and takes the restart path.
2. Does the Core read it, and can Go change it in place? Then it belongs in `UpdateParams` and takes the update path.
3. Does the Core read it only when it parses a config? Then write it into the generated config, and it takes the
apply path.
4. Does only Flutter read it? Then it takes no path.
Tests that hold these rules:
- `test/models/config_test.dart` (`PatchClashConfig.setupOnly`).
- `test/models/core_test.dart` (`VpnOptions.effective`).
- `test/providers/state_derived_test.dart` (the setup patch).
- `test/manager/core_manager_test.dart` (update against apply).
- `test/manager/vpn_manager_test.dart` (the restart notice).
- `test/providers/setup_action_test.dart` (running options, suspension, `restartVpn`).
+9 -3
View File
@@ -1,18 +1,24 @@
# 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.
Repo skills live under `.agents/skills/*/SKILL.md`, where Codex discovers them directly. Claude Code discovers them
through the symlinks in `.claude/skills/`, one per skill, pointing back at the `.agents/skills/` directory. Both tools
match a task against the skill `name` and `description`, then load the full instructions only when it 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, async feedback, and user-facing interactions.
- `core-platform`: Core lifecycle/process ownership, Android services, Go event delivery, desktop IPC, platform managers,
VPN/TUN, and Windows Helper flow.
- `pre-commit-quality-review`: lightweight triage before commits, with scoped structural review and user decisions on concrete regressions.
## Authoring Notes
- Add new repeatable workflows as `.agents/skills/<skill-name>/SKILL.md`.
- Add new repeatable workflows as `.agents/skills/<skill-name>/SKILL.md`, and add the matching
`.claude/skills/<skill-name>` symlink so Claude Code sees it too; remove both when retiring a skill.
- Git stores the symlinks as links only where the checkout supports them. A Windows checkout needs
`core.symlinks=true` and Developer Mode, otherwise the links become plain files and Claude Code loads no repo
skill there; Codex is unaffected because it reads `.agents/skills/` directly.
- 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.
+50 -58
View File
@@ -1,73 +1,65 @@
---
name: core-platform
description: Use when changing FlClash Core integration, lifecycle/process ownership, Go event delivery, Android services, desktop IPC, platform managers, VPN/TUN, or Windows Helper flow.
description: Use when changing FlClash Core integration, lifecycle/process ownership, Go event delivery, Android services, desktop IPC, platform managers, build hooks, system proxy, tray, VPN/TUN, or the Windows or Linux Helper.
---
# Core And Platform
## When To Use
Use this for changes touching `lib/core/`, `lib/manager/`, `core/`, `services/helper/`, Android app/service modules, build
hooks, system proxy, tray, VPN, TUN, or platform-specific desktop/mobile behavior.
Use this for changes touching `lib/core/`, `lib/manager/`, `core/`, `services/helper/`, Android app/service modules,
build hooks, system proxy, tray, VPN, TUN, or platform-specific desktop/mobile behavior.
## Workflow
## Read First
1. Identify the authoritative owner before changing behavior:
- Shared facade/protocol: `lib/core/controller.dart`, `lib/core/interface.dart`, and `lib/core/method.dart`.
- Android Core connection: `lib/core/lib.dart`, `lib/plugins/service.dart`, and Android `ServicePlugin`.
- Android start/stop intent: `ServiceState`; binding/process-time bookkeeping: `ServiceController`.
- Desktop composition: `lib/core/service.dart`; lifecycle/process ownership: `lib/core/desktop/lifecycle.dart`.
- Desktop IPC/RPC: `lib/core/desktop/transport.dart` and `lib/core/desktop/rpc_client.dart`.
- Desktop launch ownership: `lib/core/desktop/launcher.dart`; Windows Helper HTTP contract:
`lib/core/desktop/helper_client.dart` and `services/helper/`.
- Flutter orchestration: `lib/providers/actions/core.dart` and `system.dart`; UI/event observation: `lib/manager/`.
2. Trace every entry path into that owner, including UI/provider calls, Quick Settings, notification actions, Always-on VPN,
revoke callbacks, application exit, and crash/disconnect recovery. Lifecycle callbacks are not implicit user intent.
3. Preserve latest-intent semantics:
- Desktop revisions converge to running/restarted/stopped/closed and report applied/coalesced/superseded outcomes.
- Android Flutter calls stay optimistic; `ServiceState` identity-checks the latest native `RunRequest`.
4. Route feature calls through `CoreController` and `CoreHandlerInterface`. Do not bypass desktop process leases or create a
second Android service binding owner.
5. Keep JSON envelopes and event shapes identical across Dart, Go, JNI, and desktop IPC. If event traffic changes, preserve
the separate priority and bulk queues in `core/message.go`.
6. Keep shutdown single-owned and terminal. `SystemExitCoordinator` sequences resource cleanup, window close, Core close,
and process exit; widget/manager disposal must not race it.
7. Add or update focused tests at the narrowest layer, then run the matching commands from `.agents/commands.md`:
- Desktop lifecycle/transport/RPC: `test/core/desktop/` plus `test/core/service_test.dart`.
- Cross-language envelopes/events: `test/core/protocol_contract_test.dart` and `CGO_ENABLED=0 go test .`.
- Provider/exit convergence: `test/providers/action_test.dart` and `test/providers/system_action_test.dart`.
- Android Kotlin: compile each touched Gradle module with JDK 17.
- Windows Helper: Cargo format/tests; run the `windows-service` feature on Windows.
8. Explicitly state host gaps. Always-on VPN, VPN permission, system revoke, named-pipe peer identity, and Windows Service
Control Manager behavior need their real platform even when portable tests pass.
- `.agents/rules.md`: Core API Safety and Lifecycle Rules are binding. They carry the mihomo-fork patch budget, the
JSON envelope and `core/message.go` queue rules, gomobile callback rules, `BootGuard`, tray, and SSID rules, and every
lifecycle invariant (latest intent wins, desktop ownership through a `CoreProcessLease`, terminal `close()`,
optimistic Android commands arbitrated by `ServiceState`, callbacks are not user intent, `goAsync()` finishes once).
- `.agents/setting-changes.md`: a setting the Core or the Android VPN reads is patched live, re-applied with the
profile, or left to a restart the user confirms; it says which, and what adding a setting to each path takes.
- `.agents/architecture.md`: Core Integration, Lifecycle Ownership And Convergence, Core Protocol And Event Delivery,
Safe Mode, Build System, and Rust Helper Service describe the current mechanisms, including the Helper protocol,
the applied/coalesced/superseded outcomes, and the unconfirmed-exit lease.
## Reference Files
## Owners
Read `.agents/architecture.md` for the current core modes, manager stack, build hooks, local plugins, and Windows helper notes.
Every behavior here has one authoritative owner, and a change lands there:
- Shared facade/protocol: `lib/core/controller.dart`, `lib/core/interface.dart`, and `lib/core/method.dart`.
- Android Core connection: `lib/core/lib.dart`, `lib/plugins/service.dart`, and Android `ServicePlugin`.
- Android start/stop intent: `ServiceState`; binding/process-time bookkeeping: `ServiceController`.
- Desktop composition: `lib/core/service.dart`; lifecycle/process ownership: `lib/core/desktop/lifecycle.dart`.
- Desktop IPC/RPC: `lib/core/desktop/transport.dart` and `lib/core/desktop/rpc_client.dart`.
- Desktop launch ownership: `lib/core/desktop/launcher.dart`; Windows and Linux Helper HTTP contract:
`lib/core/desktop/helper_client.dart` and `services/helper/`.
- Flutter orchestration: `lib/providers/actions/core.dart` and `system.dart`; application exit:
`SystemExitCoordinator`; UI/event observation: `lib/manager/`.
An owner has more entry paths than the UI: provider calls, Quick Settings, notification actions, Always-on VPN, revoke
callbacks, application exit, and crash/disconnect recovery all reach it. Always-on startup is explicit through
`VPN_START_REQUESTED` and revoke through `VPN_REVOKED`; service creation or destruction is never read as intent.
## Verification
Focused tests live at the narrowest layer, and the matching commands are in `.agents/commands.md`:
- Desktop lifecycle/transport/RPC: `test/core/desktop/` plus `test/core/service_test.dart`.
- Cross-language envelopes/events: `test/core/protocol_contract_test.dart` and `CGO_ENABLED=0 go test .`.
- Provider/exit convergence: `test/providers/action_test.dart` and `test/providers/system_action_test.dart`.
- Android Kotlin: compile each touched Gradle module with JDK 17.
- Helper: `cargo fmt --check` and `cargo test`; the `windows-service` feature only builds on Windows.
State host gaps explicitly. Always-on VPN, VPN permission, system revoke, named-pipe peer identity, polkit and systemd
on Linux, and Windows Service Control Manager behavior need their real platform even when portable tests pass.
## Pitfalls
- Keep the Windows Helper protocol and Core SHA256 validation identical across
Flutter build modes; the Helper owns executable integrity checks.
- Protocol version 6 uses a 32-character lowercase-hex session ID. `/start` must return the submitted session and PID;
`/stop` must never terminate a different session; Dart must verify the connected named-pipe peer PID.
- `/start` must release the previously managed Core before it verifies, so no `/start` outcome leaves a Helper-managed
Core behind for the caller's direct-launch fallback to race.
- The Helper owns a managed Core until its exit is confirmed. A `200` from `/stop` means the Core is gone; when
termination cannot be confirmed the Helper keeps the child and answers `coreStopFailed`, and `/start` reports the same
code instead of spawning a replacement. Keep that code out of the Dart pre-spawn fallback set in
`helper_client.dart`, or the direct launch will race a Core the Helper still owns.
- TUN is not a required run condition. Degrading to the unelevated direct Core — and silently losing TUN — is the
expected outcome whenever the Helper path fails; do not fail the launch instead.
- A desktop process lease with unconfirmed exit must remain owned until cleanup succeeds. Do not discard it and start a
replacement Core.
- `CoreController.close()` is terminal. Do not call it from a reusable manager lifecycle or recover by starting it again.
- `ServiceBroadcastReceiver.goAsync()` must finish once even on timeout; its watchdog releases the broadcast only and must
not become a service timeout.
- Do not interpret service creation/destruction as start/stop intent. Always-on startup is explicit through
`VPN_START_REQUESTED`; revoke is explicit through `VPN_REVOKED`.
- Keep log/request floods from evicting state-bearing Core events. Each queue may evict only its own oldest item.
- Do not expose direct filesystem deletion APIs through Core or helper IPC; use
a scope-specific cleanup API instead.
- `plugins/setup/` is a build harness, not a Dart API plugin.
- Build hooks can trigger Go or Rust compilation indirectly through Flutter platform builds.
- A `200` from Helper `/stop` means the Core is gone. When termination cannot be confirmed the Helper keeps the child
and answers `coreStopFailed`, and `/start` reports the same code instead of spawning a replacement. Keep that code
out of the Dart pre-spawn fallback set in `helper_client.dart`, or the direct launch races a Core the Helper still
owns.
- The Helper owns executable integrity: Core SHA256 validation and the protocol stay identical across Flutter build
modes, so a debug build is not exempt.
- `plugins/setup/` is a build harness, not a Dart API plugin, and its hooks can trigger Go or Rust compilation
indirectly through Flutter platform builds; a missing toolchain fails `flutter test`, not only `flutter build`.
+15 -11
View File
@@ -13,33 +13,37 @@ Do not use this for README translation sync or manual edits to generated localiz
## 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:
1. Scan user-facing Dart text before opening many files. `flutter_intl` reads `arb/` and writes `lib/l10n/`:
```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:
2. Inspect the smallest relevant call sites and nearby ARB keys.
3. 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:
- `arb/intl_fr.arb`
- `arb/intl_es.arb`
- `arb/intl_ko.arb`
4. Replace inline strings with existing project accessors:
- Widgets with `BuildContext`: `context.appLocalizations.key` from `lib/common/context.dart`.
- Controllers/providers/non-widget code: `currentAppLocalizations.key` from `lib/common/app_localizations.dart`.
5. 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.
6. Run `flutter analyze --no-fatal-infos` and `flutter test test/l10n test/lint/dynamic_message_key_test.dart`. The
lint test checks that every locale carries the same keys and that no `Intl.message` builds a key at runtime; labels
for enum values go through the switches in `lib/common/l10n_labels.dart`, as the ARB section of `.agents/rules.md`
explains.
7. 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.
@@ -0,0 +1,105 @@
---
name: pre-commit-quality-review
description: Use when the user asks to commit, amend, or squash code, or requests a pre-commit structural quality review.
---
# Pre-commit Quality Review
Check that iterative edits converged into a clear, cohesive, maintainable implementation before they become a commit,
so later code review is not spent on basic structure. `AGENTS.md` and `.agents/rules.md` apply throughout. This skill
grants no authority: commit, push, and history rewrites stay under the user's existing authorization, and silence is
not consent.
## Triage
Most commits stop here. Decide from the change summary, the diff, and the local context it needs; never start with a
repository-wide scan.
- **Skip** formatting, mechanical renames, generated output, copy changes, and simple local fixes. They go through the
normal commit flow without extra confirmation. Say the review was skipped, never that it passed.
- **Review** when a core implementation is replaced; when responsibilities, dependency direction, or public abstractions
change materially; when accumulated patches added branches, duplicate paths, or indirection; or when new code draws
responsibility boundaries. A small change can move a critical responsibility, so line count is not a trigger.
- An explicit structural review request bypasses triage.
The snapshot under review is the intended commit compared with its baseline:
| Operation | Baseline |
|---|---|
| Regular commit | `HEAD` |
| Amend | Parent of the commit being replaced |
| Squash | Version preceding the agreed range |
| Initial commit | Empty tree |
Ask when the range or parent is materially ambiguous instead of assuming the whole branch. Read staged versions of
partially staged files; an unstaged fix is not part of the pending commit. Preserve unrelated changes and partial
staging.
## Review the Final Structure
Scope the review to the responsibility boundaries this change touched. Read the complete final implementation of the
affected units and compare before and after, not only the last patch. Report issues this change introduced or
worsened; untouched historical debt is not a pre-commit requirement. Follow a call or dependency path only as far as a
concrete concern needs, and disclose missing essential context rather than claiming a complete pass.
Focus on:
- **Responsibilities and cohesion:** units mixing responsibilities that change independently; orchestration tangled
with implementation detail; responsibilities in layers that should not own them.
- **Dependencies and abstractions:** reversed or circular dependencies, forwarding layers, abstractions that isolate
nothing; similar names or interfaces hiding different responsibilities.
- **Complexity and readability:** nested conditions, repeated fallbacks, boolean parameter combinations, mutually
dependent helpers; names and decomposition that do not reduce reading effort.
- **Duplication and consistency:** reimplemented existing capabilities, parallel implementations or duplicate state,
scattered ownership of error handling, cleanup, or state updates.
- **Remnants:** obsolete branches, compatibility layers, unused parameters, commented-out code, comments the rewrite
invalidated; coupling that makes existing test boundaries hard to use.
Not findings: length, a missing design pattern, naming taste, or anything without code evidence and a real
maintenance cost. Do not add cross-layer coupling to remove a few similar lines, rewrite stable code for uniformity,
or demand abstractions for speculative needs.
Business logic, UI behavior, performance, security, platform compatibility, and full functional review are out of
scope. Report an incidentally found serious defect separately through the normal defect workflow, without hiding it or
widening this review.
## Findings and the User's Decision
A finding qualifies only when all three hold:
- Before-and-after code and the necessary call relationships show this change introduced or worsened it.
- The maintenance cost is concrete, such as updating one rule in two places or touching unrelated layers for one
responsibility. Clarity or elegance alone is not enough.
- A minimal proposal fits existing constraints, with a benefit not offset by new coupling, indirection, or scope.
With no qualifying finding, state the reviewed scope and conclusion in a sentence or two and continue under the
existing commit authorization. Do not ask for confirmation because a review happened, and do not collect optional
improvements into a suggestion list.
With qualifying findings, finish the read-only review and independent checks first, merge findings that share a cause,
and give for each: file, symbol, and location with how this change introduced it; the concrete maintenance cost; the
smallest proposal with its scope and any behavioral impact. Then **pause before creating the commit and ask whether to
optimize first; do not edit or commit until the user answers.** Attribute the pause to this skill by file path so the
user recognizes their configured workflow, for example: "The structural issues above support consolidating the
duplicate state-update paths. Should I make that change before committing?"
Existing explicit authorization for these findings is enough to act without asking again. The user may accept all,
some, or none. Keep accepted findings and tradeoffs in the task context only, and do not re-ask because files were
restaged, a hash changed, or unrelated edits landed; reassess only when relevant code changes invalidate the tradeoff.
## Resolve and Finish
Once authorized, make the smallest adjustments that resolve the accepted findings while preserving behavior and task
scope; explain separately if either must change. Verify only the accepted findings and the direct effects of the
adjustments, without another search for improvements. Raise a new finding only with evidence that meets the threshold
and exceeds the existing authorization.
Before committing, recheck status and the actual staged diff so the pending snapshot matches what was reviewed, and
review only the delta of later edits. Run the checks `AGENTS.md` requires for the change: `flutter pub get`,
`flutter analyze --no-fatal-infos`, and the covering `flutter test` suites listed in `.agents/commands.md`. Reuse
results that are still valid, rerun those a later edit invalidated, and never present results from an earlier snapshot
as verification of the staged one. The `comment-density` and `commit-msg` hooks in `.agents/rules.md` still gate the
commit.
Report the reviewed scope, completed improvements or accepted tradeoffs, check results, and any verification limits.
Claim the commit is done only after it exists.
-37
View File
@@ -1,37 +0,0 @@
---
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.
+110 -75
View File
@@ -1,109 +1,144 @@
---
name: ui-work
description: Use when changing FlClash Flutter UI, widgets, screens, Material You styling, navigation surfaces, async feedback, or user-facing interactions.
description: Use when changing FlClash Flutter UI, widgets, screens, Material You styling, icons or glyphs, shapes and corner radii, navigation surfaces, async feedback, 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.
Use this for user-facing Flutter UI changes in `lib/`, including widgets, screens, navigation surfaces, settings rows,
dialogs, and interaction behavior.
## Workflow
## Conventions
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. For asynchronous controls, define separately:
- authoritative provider/domain state;
- display-only state such as a minimum progress duration;
- tap policy while work or display holds are active;
- failure/disposal cleanup, normally in `finally` for animations and timers.
9. Run targeted verification:
```bash
flutter analyze
flutter test test/widgets/
```
- Reuse the patterns of nearby widgets, providers, notifiers, and helpers before adding an abstraction, and keep to the
look the rest of the app has: Material You from `material_ui` as the base, with Apple's interaction conventions
(continuous corners, sheets that snap between detents, a floating tab bar) built on Material widgets rather than
Cupertino ones. Do not introduce a visual system for one screen or rewrite a layout the request does not need.
- `.agents/rules.md` is binding here: the lint set (`child:` last, `const`, final locals), the superellipse corner API,
and the widget-test facts under Testing Rules (`pumpAndSettle` never returns on `EditorView`, `dispose()` must not
await, auto-dispose providers need a container-level hold, platform decisions are passed in as parameters).
- A component that reads no provider, model or app string goes in `lib/ui/`; one built for an app feature goes in
`lib/widgets/`. The UI Layer section of `.agents/architecture.md` says how a `lib/ui` component gets app state.
- Settings screens are built from the `ConfigToggleItem` family in `lib/widgets/config_item.dart`; the Settings Rows
section of `.agents/architecture.md` says when a file-local helper or a named widget is warranted.
- Every icon lives in `lib/ui/icons/`, never the Material `Icons` font: reuse or add a glyph in `AppGlyphs` and show it
with `GlyphIcon`, or with `AnimatedGlyph` where it fills on selection. Glyphs are outlined, except inside a filled
button, where they take `fill: 1`. The Icons section of `.agents/architecture.md` covers shape roles, the stroke, the
fill animation, and when to fill.
- Text sizes and weights come from `AppTextStyle` in `lib/ui/typography.dart`, Apple's iOS Dynamic Type scale, never a
Material `TextTheme` slot, a bare `fontSize`, or a `fontWeight`; the Typography section of `.agents/rules.md` has the
rule.
- Modals dim what they cover with the Material 3 scrim (`ColorScheme.modalScrim`, `scrim` at 32%) and never blur it.
Every modal brings its own scrim, including one opened from inside another, so do not make a barrier transparent to
keep scrims from stacking.
- A sheet's size belongs to its route, never to its content or call site: a bottom sheet rests at
`SheetProps.detents`, a side sheet is as tall as the window and as wide as `ModalSideSheetRoute` makes it from
the window width (`lib/ui/side_sheet.dart`, pinned by `test/widgets/side_sheet_width_test.dart`), and a form sheet
is a card `FormSheetRoute` caps in width and height (`lib/ui/form_sheet.dart`). Do not wrap a sheet's content in a
fixed width or height to resize one sheet.
- A form with more than one value opens with `showFormSheet` and lays its values out in `FormRow`s, never as labeled
fields in a `CommonDialog`; a prompt for one value stays an alert. Forms in `.agents/architecture.md` has the rule.
- A modal scrolls only once the window and keyboard leave it no room, and then through one scroll view: `CommonDialog`
caps its body at nothing but the space it is given, and a multi-line field in a dialog or sheet grows with its text
(`maxLines: null`) rather than scrolling inside a line count. With the keyboard up, a bottom sheet keeps what it
shows above the keyboard, rising past its tallest detent as far as a peek below the top (`_top` in
`lib/ui/snap_sheet.dart`).
- The keyboard animates `MediaQuery.viewInsets` every frame. Read MediaQuery by aspect (`paddingOf`, `viewInsetsOf`)
rather than `MediaQuery.of`, and wrap the page of a new route type in `KeyboardInsetHold`; Keyboard Insets in
`.agents/architecture.md` has the reasons.
- A page loads what it shows as it mounts, never once its route has settled; what waits out a transition is work
that repeats, such as a live feed's refreshes (`RouteMotionHoldMixin`), and the rows past the viewport
(`arrivalScrollCacheExtent`). Page Arrival in `.agents/architecture.md` has the rule.
- A page that waits on its data shows nothing until all of it is ready (`NullStatusSwitcher.isLoading`), and a load
that can run long adds its illustration as `loadingStatus`, never a skeleton screen; Page Arrival in
`.agents/architecture.md` has the rule.
- App bar buttons reach `CommonScaffold` as data (`primaryAction`, `iconActions`, `menuItems`) so it can fold them into
its overflow menu; App Bar Actions in `.agents/architecture.md` has the rule.
- A row's secondary actions open from a context menu, never a trailing more button; a list whose rows have one
reorders in a sort mode, any other by a long press on the row. Row Actions And Sort Mode in `.agents/architecture.md`
has the rule.
- Quick edit belongs only on a page that edits a whole list or section, or one isolated item; never on one that edits a
part of something other settings also write. Quick Edit in `.agents/architecture.md` has the rule.
- A page or sheet opens on the navigator of the context it comes from, never on the root navigator unless Where A
Route Opens in `.agents/architecture.md` allows it.
- User-facing text goes through ARB; use `localization` when text changes are non-trivial.
- An asynchronous control keeps four things apart: authoritative provider/domain state; display-only state such as a
minimum progress duration; tap policy while work or a display hold is active; and failure/disposal cleanup, in
`finally` for animations and timers. `CoreStatusButton` and the proxies delay test action are the reference implementations,
described under User-Facing Core And Delay Feedback in `.agents/architecture.md`.
## Corner Radii
All corner radii come from `lib/common/shape.dart`. Never write a radius literal in a widget.
All corner radii come from `lib/ui/shape.dart`: a token, a derived value, or a named off-scale value passed to its
builders. Never write a bare radius literal at a call site.
The scale is picked by the component's **shortest side**, not by what looks good in isolation. A radius that reads as
a soft card at 64 logical pixels tall reads as a pill at 24 and as a square at 400, so a single radius everywhere is
the wrong kind of consistency.
The scale is picked by the component's **shortest side**. A radius that reads as a soft card at 64 dp tall
reads as a pill at 24 and as a square at 400, so one radius everywhere is the wrong kind of consistency.
| Token | Value | Shortest side | Use |
| --- | --- | --- | --- |
| `none` | 0 | - | square edges, and the flat side of a grouped run |
| `xs` | 4 | inset blocks | tiles clipped inside an already rounded surface |
| `sm` | 8 | up to 48 | chips, thumbs, swatches, small square tiles |
| `md` | 16 | up to 64 | interactive chrome: inputs, menus, buttons, popups, FAB |
| `md` | 16 | up to 64 | interactive chrome: inputs, menus, buttons, popups |
| `lg` | 20 | 64 to 180 | mid-size cards: dashboard tiles, proxy node cards |
| `xl` | 24 | full-bleed | anything spanning the whole width: list rows, grouped runs, group headers |
| `xxl` | 28 | over 200 | sheets, dialogs, full-screen containers |
| `full` | 1000 | - | pills and circles: tracks, indicators, progress, avatars |
| `xxl` | 28 | over 200 | sheets, full-screen containers |
| `full` | 1000 | - | pills and circles: buttons, tracks, indicators, progress, avatars |
`AppCorner.fit(shortestSide)` applies that table at runtime and is the right call whenever the size comes from a
`LayoutBuilder` or scales with text size. It snaps to the largest token that stays at or under one third of the
shortest side, which is the rule the table encodes.
- `AppCorner` holds the scale as `double`; `AppRadius` mirrors it as `BorderRadius` with `all`, `top`, and `vertical`
builders; `AppShape` mirrors it as `RoundedSuperellipseBorder`, `full` included, plus `circle`, `input`, and the
`all`/`top`/`vertical`/`of` builders. Use `AppShape.full` for pills, never `StadiumBorder`; the Corner Radius
section of `.agents/rules.md` explains why and lists the superellipse API per layer.
- `AppCorner.fit(shortestSide)` snaps to the largest token at or under one third of the shortest side. Use it whenever
the size comes from a `LayoutBuilder` or scales with text.
- `ThemeData.withAppShapes` in `lib/ui/shape.dart` already applies the scale to card, dialog, bottom sheet, snack
bar, chip, menu, input, FAB, navigation indicator, progress, and the filled, text, outlined, elevated, and icon
button themes. Do not restate those shapes at call sites, and
leave `InputDecoration.border` unset so inputs inherit `AppShape.input`.
- Colors of surfaces follow `.agents/surfaces.md`: fills by `SurfaceLevel`, no structural borders, and
`primaryContainer` only on a selected item.
- `AppCorner` holds the scale as `double`, for `radius:` on `CommonCard` and for arithmetic.
- `AppRadius` mirrors it as `BorderRadius`, plus `all`, `top`, and `vertical` builders.
- `AppShape` mirrors it as `RoundedSuperellipseBorder`, plus `full` (stadium), `circle`, `input`, and the
`all`/`top`/`vertical`/`of` builders.
- `ThemeData.withAppShapes` in `lib/application.dart` applies the scale to card, dialog, bottom sheet, snack bar,
chip, menu, input, FAB, navigation indicator, and progress themes. Do not restate those shapes at call sites; in
particular, leave `InputDecoration.border` unset so inputs inherit `AppShape.input`.
`xl` and `lg` exist because one token cannot serve both size classes. A full-bleed surface is wide and short, so its
corner must be larger than its height alone suggests or it stops reading as a card; everything full-bleed shares `xl`
(`CommonSelectedListItem`, the profiles card, the outer corners of a grouped run in `DecorationListItem`, the proxy
group card in `lib/views/proxies/list.dart`) and does not use `fit()`. This is the one deliberate overshoot of the
one-third rule; the value stays under half a standard row so nothing clamps and every full-bleed surface renders the
same radius. `lg` is for mid-size cards that are not full-bleed: a proxy node card in the tab view at
`ProxyCardType.compact` is only about 64 tall, which caps its radius at 21, so `xl` would break on a setting the user
can change at any time while `lg` clears both heights and the smallest dashboard tile.
The three tiers between `md` and `xxl` each answer to a different size class, and the split is what keeps one radius
from being wrong for two of them.
Outlined inputs use `AppInputBorder`, not `ShapedInputBorder` from `package:material_ui`. The package border subtracts
the floating label's notch as a flat band along the top edge, which takes the corner curve with it at any radius from 8
up. `AppInputBorder` clips the notch region and paints the full superellipse through it, as Flutter's own
`OutlineInputBorder` does by shortening the arc. It needs no `contentPadding` compensation.
`xl` is for a surface that spans the full width. A wide, short strip needs a larger corner than its height alone
suggests, or it stops reading as a card. Everything full-bleed shares this token and must stay on it:
`CommonSelectedListItem`, the profiles card, the outer corners of a grouped run in `DecorationListItem`
(`generateSectionV3`), and the proxy group header in `lib/views/proxies/list.dart`. The value is deliberately under
half the height of a standard row, so nothing gets clamped and every one of them renders at the identical radius no
matter which is taller. This is the one place the one-third rule is knowingly overshot; the width is what carries
it, and `fit()` is not used here.
Nested radii are derived, never tokens: concentric corners need `outer = inner + inset`, so name the inset and compute
the outer value, as `_cardRadius` in `lib/ui/popup.dart` does. Adding a token for one of these is how a scale grows without
bound. A card that also stands on its own elsewhere keeps its token when nested: the proxy node cards inside a proxies
list group stay `lg` like those in the tab view, since deriving them from the group card's `xl` left them at 14, smaller
than every other card in the app.
`lg` is for the mid-size card that is not full-bleed: dashboard tiles and proxy node cards. It exists because `xl`
does not fit all of them. A proxy node card's height follows the user's `ProxyCardType` setting, and at `min` it is
only about 64 tall, which caps its radius at 21 — so `xl` would break on a setting the user can change at any time,
while `lg` clears every one of the three heights. The same value has room to spare on the smallest dashboard tile,
about 177x80, so both surfaces can hold one token instead of branching per size.
A component the scale does not fit keeps its own value instead of growing the scale: name it once in the file and pass
it to the `all`/`top`/`vertical` builders, as `_itemShape` in `lib/ui/sidebar.dart` does with 10, between the
chip-sized `sm` and the near-pill `md` on a 40 tall row, and `_dialogCorner` in `lib/ui/shape.dart` does with 34, the
corner of an iOS alert. Reach for this only once the table and `fit()` are ruled out.
Outlined inputs use `AppInputBorder`, not `ShapedInputBorder` from `package:material_ui`. That one subtracts the
floating label's notch from the outline as a two-pixel band along the top edge, which assumes the label sits over a
flat run of border. Any radius from 8 up puts the corner curve under the notch instead, the subtraction takes the
corner with it, and the top edge is left drawn a pixel low. `AppInputBorder` clips the notch region away and paints
the full superellipse through it, so the corner arc is truncated exactly where the notch begins. That is what
Flutter's own `OutlineInputBorder` does by shortening the corner arc's sweep, which is why the framework border
survives large radii and the package one does not. The border needs no `contentPadding` compensation; leave Material's
defaults alone.
## Verification
Nested radii are derived, never tokens. Concentric corners need `outer = inner + inset`, so name the inset and
compute the outer value: `_cardRadius` in `lib/widgets/popup.dart`, `_kCornerRadius` in `lib/widgets/tab.dart`, and
the selection ring in `lib/widgets/palette.dart` all do this. Adding an intermediate token to spell one of these out
is what makes a scale grow without bound.
- A behavior change gets a focused widget test covering rendering states, taps, scrolling, and empty/error states,
including the exception path of any async action. Run `flutter analyze` and `flutter test test/widgets/` plus the
tests of the touched view.
- The scans in `test/lint/` fail on the common UI mistakes and run as part of `flutter test`: an `IconButton` whose
icon is an icon needs a `tooltip`; a field that constructs its own controller, notifier, or focus node is released in
the same file; a public declaration nothing references is dead. `.agents/rules.md` explains each rule and its
allowlist.
## 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.
- Do not mutate provider/domain state merely to smooth a transition. Keep presentation holds local and let real errors
bypass them immediately.
- Do not leave loading animations active when callbacks throw. Test the exception path, not only the successful tap.
## Current Interaction Examples
- `CoreStatusButton` watches `coreStatusProvider` but keeps its 600-millisecond connecting hold locally. Taps are ignored
during the hold or genuine connecting state; disconnected cancels the hold immediately.
- Proxy delay testing writes `0` while pending, the measured delay on success, and `-1` on failure. `DelayTestButton` resets
its animation in `finally`.
- Do not leave loading animations active when callbacks throw.
- Do not manually edit generated localization or provider files.
+187
View File
@@ -0,0 +1,187 @@
# Surfaces
FlClash draws structure with fills and keeps color for state. A surface stands out from what it sits on by a step in
lightness, never by a border. Color appears where something is selected, on, failing, or the one action a screen is
for. The dashboard set this direction; every other screen follows it, and Material components keep their behavior
but give up their tonal container fills.
## Levels
`SurfaceLevel` in `lib/ui/surface.dart` names four depths. Each surface takes the level one deeper than the one
it sits on, which `SurfaceScope` passes down the tree.
| Level | Light | Dark | Holds |
|---------|---------------------------|---------------------------|---------------------------------------------------------|
| `page` | `surfaceContainer` | `surface` | scaffold body, sheets |
| `card` | `surfaceBright` | `surfaceContainer` | cards, grouped runs of rows, light-mode floats |
| `block` | `surfaceContainer` | `surfaceContainerHigh` | unselected segments, tiles and wells in a card |
| `inset` | `surfaceContainerHighest` | `surfaceContainerHighest` | small marks inside a block: glyph wells, badges, tracks |
Light mode alternates gray and paper: a near-white card on a gray page, a gray block in the card. The card is tone 98,
not pure white, and the page tone 94: pure white beside a tinted gray made the white glare and the gray look soiled. Dark mode only
lightens, because a darker step inside a lighter surface reads as a hole rather than a layer. `inset` is the floor;
nothing nests below it, so a design that needs a fifth level needs fewer containers instead.
- A widget never picks its level at the call site. `CommonCard` and the other shared surfaces read
`context.raisedSurfaceLevel` and wrap their child in `SurfaceScope`, so the same row is white on a page and gray
inside a card. A branch on `isInBottomSheet` or on where a widget is placed to choose its fill is the bug this
replaces.
- Sheets are `page` level, like a page, so a section looks the same in a sheet and in a page. In dark mode a sheet
presents a level up, as iOS presents a sheet on its elevated backgrounds: `ColorScheme.elevated` lifts every
surface role by the step from `page` to `card`, so the sheet's page takes the tone of the cards it covers and every
level inside it follows. Under pure black the sheet takes the plain dark scheme instead, which is the same step up
and keeps the seed's tint that a lift from black would lose. `ElevatedColorScheme` carries the result and the sheet
routes apply it through `ElevatedSurfaceTheme`; a sheet opened from a sheet keeps the same lift. A scrim barely
darkens a dark page, so a dark sheet at the page's own tone read as more of the page rather than a layer over it,
a covered sheet's peek vanished, and on black the edge and corners went with it. Light mode keeps its levels,
because the scrim already sets a light sheet apart.
- What floats over content, dialogs, menus, popups, the mobile dock, toasts, takes `SurfaceLevel.floatingOf`: `card`
in light, `block` in dark. A shadow does not show on a dark page, so a float has to come out lighter than the cards
it covers, and its content scopes to that level so a block inside still steps away from it.
- Shadows mark what floats, plus the dashboard cards. Cards in a scrolling list have none; a grid of proxies with a
blurred shadow each costs raster time for no extra layering. A render object that paints a shadow itself draws it
with `drawRSuperellipse`, as `ShapeDecoration` does: Impeller blurs a superellipse in one analytic pass, but a path
through an offscreen Gaussian blur on every frame, which made the shadows the largest raster cost of an opening menu.
## The Seeded Scheme
`seededColorScheme` in `lib/ui/color.dart` builds every app scheme: Material's scheme for the seed and the
`ColorStyle` picked in the theme settings, with three groups of roles replaced. Everything else stays Material,
including `toPureBlack` applied after it.
The four styles each change something a reader sees at once. Material's nine variants mostly moved secondary and
tertiary, which the app barely shows, so most of them looked alike once surfaces stayed gray.
| Style | Material variant | Primary | Surfaces |
|--------------|------------------|-------------------|---------------------------------|
| `vivid` | content | the seed | near gray |
| `tinted` | content | the seed | the seed's hue, up to chroma 10 |
| `soft` | tonal spot | Material's, muted | near gray |
| `monochrome` | monochrome | black or white | gray |
- **Surfaces stay near gray.** Each `surface*` role keeps Material's tone but its chroma is capped at 3, the light
`surface` and `surfaceBright` at 1.5, and `outlineVariant` at 4. Material tints neutrals by the seed (content
divides the seed's chroma by 8), so a yellow, green, or rose seed tinted the whole page. `tinted` is that wash
chosen on purpose and made even: every surface takes the seed's hue at a sixth of its chroma, capped at 10, so a
gray seed stays gray and any vivid one tints about as much.
- **`primaryContainer` comes from the primary palette.** It is primary's hue at tone 90 in light and 30 in dark,
with `onPrimaryContainer` at 30 and 90, and the pair's chroma is capped at 48. Content and fidelity otherwise put it
at the seed's own tone: a yellow seed filled it with pure yellow, a red seed with something that read as an error,
and dark mode with a light block under light text. The cap matters for light, chromatic hues: yellow at tone 90 is
still close to full strength.
- **`vivid` and `tinted` keep the seed as `primary`**, as an Apple accent color does: the color picked in the theme
settings is the color of every check, switch, and filled button. Light holds the seed's tone between 35 and 60, so
white text on it keeps about 3:1 and a near-black seed still reads as a color; dark lifts it 3 tones, as iOS lifts
its system colors, within 50 to 75, and puts dark text on it above 60. Material sets primary at tone 40 and 80 for
any seed, which turned iOS blue into navy in light and a pastel in dark. `soft` and `monochrome` keep Material's
primary, since moving away from the seed is what they are for.
## State
Color marks the one item that differs, never the structure around it.
- **Selected**, one of several rows, cards, or tiles: the item keeps its level's fill and a primary mark says which,
as iOS checks the option picked from a list. A row in a grouped run takes `SelectionCheck` at its edge; a card that
stands on its own, a profile or a proxy, and a tile take the filled `SelectionBadge`, which holds its own beside a
40 dp icon well where the bare check read as small and thin (`lib/ui/selection_mark.dart`). `CommonCard.isSelected` only reports the state to
accessibility. Fills were tried and rejected: a wash of primary landed on a gray that did not read as a choice, and
`primaryContainer` read as a warning under rose seeds in light and as a heavy slab in dark, muddied the translucent
marks on it, and cut a grouped run in two.
- **Multi-select** in edit mode: the circle checkbox is the mark; the row stays at its level.
- **Proxy groups** have two states in URLTest and Fallback: the check badge marks the proxy traffic goes through, in
every group type, and a pin badge (`SelectionBadge.preferred`) marks the preferred proxy (mihomo's `fixed`): filled
like the check while the group uses it, tonal while the group has fallen back to another proxy, which carries the
check.
- **Chosen segment** in a connected button group, a mode switch, or a run of compact choice chips
(`CommonCard.fillsSelection`): solid `primary` with `onPrimary`, as the dashboard outbound mode does. Only one
segment per group.
- **Error** on a row or card: `errorContainer` with `onErrorContainer`, no border.
- **Status** values (delay, availability, success, warning): the color goes on text, a glyph, or a small chip, never
on a whole card. `success` and `warning` come from fixed green and orange palettes that ignore the seed, like
Material's `error`; harmonizing pulled orange toward red under rose seeds until warning and error matched. Text takes
tone 40 in light and 80 in dark, a chip takes `successContainer`/`warningContainer` (90/30) with the `on` role
(30/90). A delay under 600 ms is success, above it warning, a failure error: `ColorScheme.delayColor`.
- **Destructive** action in an alert: `ColorScheme.destructive`, iOS 27's systemRed in both modes, not `error`. A
menu's `danger` item still takes `error`.
- **Data series** on one chart: the first is primary, the second primary at 45% over the card
(`DashboardColors.series2`), so they differ by lightness under any seed. Tertiary is not a series color.
- **Text**: titles and values `onSurface`, labels and secondary lines `onSurfaceVariant`. `outline` is not a text
color, and a value is never tinted primary.
- **Marks** that only identify, a country badge, a group or service icon, a source avatar, sit on a neutral well with
`onSurfaceVariant`.
- `primaryContainer`, `secondaryContainer`, and `tertiaryContainer` are neither surface nor selection fills. Keep
them out of cards, rows, marks, and navigation indicators.
## Borders That Stay
- **Focus**: a 2 dp primary stroke while a surface has keyboard or remote focus. TV navigation depends on it, and a
fill change alone is too faint to follow from a couch. A card, a grouped row above all, rings only focus a key moved
there (`FocusMovedByKey`), or any focus while a remote drives the app: a tap or a click puts focus where the reader
is already looking, a field's caret already marks it, and the ring then read as a selection box around the row.
- **Inputs**: a form's field is a bare `FormRowTextField` at the end of a grouped `FormRow` whose title labels it, as
iOS and macOS set one, with no stroke or fill of its own. A field among filled controls takes their fill, one level
above its surface, through `FilledInputTheme`. An alert's fields, because they sit over its pill buttons, as
UIAlertController sets them: as wide as the buttons, in their fill, with their text in line with the title and the
label as the placeholder (`CommonDialog.hasFields`); a field under a filled block of text in an alert takes the
block's width and corner instead. The custom color's hex field, because its card holds only fills, the color
previews, where an outline was the one stroked thing. Search fields are filled pills. A sheet's docked search,
because the list scrolls under it: it floats like the dock, with the floating fill and the dock's shadow, or the
dock's glass. The editor's find bar, because its fields sit in a row of filled pill buttons: they take the buttons'
fill, one level above the bar. All of these stroke only while focused. Any other field, one standing alone on a page,
is outlined, with the floating label notched into the stroke: `ThemeData.withAppShapes` gives every input
`AppShape.input`, so call sites leave `border` unset. A lone field is never filled: here a fill means structure, so
one read as one more card or row, and in light mode the hover overlay brought it down to the page's own tone. No
field counts its characters, as none does on iOS or macOS; a length limit holds through its formatter. Material
strokes a filled field's whole outline in `onSurfaceVariant`, which ringed the pill brighter than anything else in a
dark sheet.
- **Window chrome**: the desktop sidebar's inner edge and the title bar's bottom edge draw a divider where they meet
the content. The chrome fill alone left the edge to a four-tone step, which read as a smudge rather than a boundary.
Nothing else draws an outline. Rows in a grouped run are separated by the hairline from `lib/ui/list_row_separator.dart`, which divides
one surface and does not outline it. Record lists, requests, connections, logs, and DNS queries, are not grouped: they
run to thousands of tall rows, and one card that long reads as a slab rather than a group. A stacked row of three or
four lines is already its own block, so each is a card of its own, `RecordListItem`: the raised fill at `xl`, 16 dp
from the edges and 8 dp from the next, as the profile cards sit. Every card takes the same fill, so the warning and
error tints on a label still stand out, where a fill on every other row competed with them; rows on the page split by
a full-width divider were the one structural line left on screens otherwise drawn with fills. Laid out as a table,
where a row is one line read across many columns, they drop the gap and give every other row the raised fill, inset
and rounded like the column header, so the eye keeps its row; the dashboard's table does the same.
Every divider takes its color from `ThemeData.withAppSurfaces`: `onSurface` at 14%, never `outlineVariant`. A
translucent line keeps the same contrast on the page, a card, or the chrome, in both modes; `outlineVariant` stood out
twice as much on the dark page as on the light one and carried the seed's tint.
## Navigation
- The desktop sidebar and title bar are window chrome, `ColorScheme.chromeSurface`: `surfaceContainerHighest` in
light and `surfaceContainerLow` in dark, four tones from the page either way, with a divider along the edge that
meets the content. At `page` level the sidebar merged with the content. In dark the chrome stays below `card`, so
cards still stand out from it. The pane the sidebar opens over the content takes the same fill and edge, plus a
shadow.
The selected destination keeps its neutral fill and primary bar, which already marks it with color.
- The mobile dock and the docked navigation bar are floats; their indicator is a neutral platter one level above
them, and the selected destination's content takes primary, as iOS's tab bar marks its selection.
- With Liquid Glass on (Theme settings), the dock, the buttons of a page's bar, a sheet's docked search, popup
menus, and alerts are glass from `lib/ui/liquid_glass.dart` instead of a fill, and the dock's platter takes the
secondary hue and chroma at tone 90, or at tone 4 in dark, which sinks it below the bar as iOS 27 does.
- The proxies page's group tabs are compact choice chips, the current group in the chosen segment's solid `primary`.
Once they overflow, a tonal button at their end opens every group as a popup menu, the current one checked and
scrolled into view. A profile can carry dozens of groups, so the menu stops at seven rows and half of the next.
## Where It Lives
- `lib/ui/surface.dart`: `SurfaceLevel`, `SurfaceScope`, `context.surfaceLevel`, `context.raisedSurfaceLevel`,
`ColorScheme.chromeSurface`, and `ThemeData.withAppSurfaces`, applied next to `withAppShapes` wherever the app
builds its theme.
- `lib/ui/color.dart`: `seededColorScheme`, the status palettes, and `delayColor`.
- `lib/views/dashboard/style.dart`: `DashboardColors` maps `page`, `card`, `tone`, and `tone2` onto the levels and
adds the dashboard's switch, series, and status colors.
## Checking A Change
Render the touched screens in light and dark, on a phone width and a desktop width, before and after. A throwaway
widget test does it: pump `HomePage` or the view inside a `RepaintBoundary`, load Roboto with `FontLoader` so text
gets real glyphs, and write `RenderRepaintBoundary.toImage` to a PNG outside the repository. Look for a surface that
matches the one it sits on, which means it skipped `SurfaceScope`, and for a tonal container fill that survived on a
card or row.
+1 -1
View File
@@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/tool/check_comment_density.sh\"",
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/tool/hooks/comment_density.sh\"",
"timeout": 15,
"statusMessage": "Checking comment density"
}
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/core-platform
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/localization
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/pre-commit-quality-review
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/ui-work
+1 -1
View File
@@ -3,6 +3,6 @@ matcher = "apply_patch|Bash"
[[hooks.PostToolUse.hooks]]
type = "command"
command = "bash tool/check_comment_density.sh"
command = "bash tool/hooks/comment_density.sh"
timeout = 15
statusMessage = "Checking comment density"
+2
View File
@@ -0,0 +1,2 @@
# freezed emits lines of trailing spaces, and generated code is never edited by hand.
lib/**/generated/** -whitespace
+5 -3
View File
@@ -23,9 +23,11 @@ cask "flclash" do
app "FlClash.app"
postflight do
system_command "xattr",
args: ["-rd", "com.apple.quarantine", "#{appdir}/FlClash.app"]
postflight_steps do
run "/usr/bin/xattr",
args: ["-rd", "com.apple.quarantine", "{{appdir}}/FlClash.app"],
writable_paths: ["FlClash.app"],
writable_base: :appdir
end
uninstall quit: "com.follow.clash"
+147 -43
View File
@@ -6,6 +6,7 @@ on:
- '**'
tags:
- 'v*'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -13,32 +14,42 @@ concurrency:
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
FLUTTER_VERSION: '3.47.1'
GO_VERSION: '1.26.4'
# See CI Caches in .agents/commands.md.
BRANCH_CACHE: ${{ !startsWith(github.ref, 'refs/tags/') }}
FLUTTER_VERSION: '3.47.6'
GO_VERSION: '1.27.1'
NDK_VERSION: r28c
jobs:
dart:
name: Dart
# A private copy pays for its Actions minutes, so it runs the checks only when
# started by hand.
if: &checks github.event_name == 'workflow_dispatch' || !github.event.repository.private
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Test commit message hook
run: bash tool/check_commit_msg_test.sh
- name: Test git hooks
run: bash tool/hooks/test.sh
- name: Test comment density hook
run: bash tool/check_comment_density_test.sh
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: plugins/rust_api/rust
cache-directories: .dart_tool/hooks_runner
save-if: ${{ env.BRANCH_CACHE }}
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache: ${{ env.BRANCH_CACHE }}
- name: Install dependencies
run: flutter pub get
@@ -49,22 +60,23 @@ jobs:
- name: Analyze
run: flutter analyze --no-fatal-infos
- name: Skip native build hooks
- name: Skip the Go core build hook
run: |
yq -i '.hooks.user_defines.setup.build_assets = false | .hooks.user_defines.rust_api.build_assets = false' pubspec.yaml
yq -i '.hooks.user_defines.setup.build_assets = false' pubspec.yaml
- name: Verify changelog
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
run: dart run tool/changelog.dart verify
- name: Run tests
run: flutter test --reporter expanded --coverage
run: dart run tool/test.dart -- --reporter expanded --coverage
- name: Check coverage
run: dart run tool/check_coverage.dart coverage/lcov.info 75
plugins:
name: Plugins
if: *checks
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -75,7 +87,7 @@ jobs:
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache: ${{ env.BRANCH_CACHE }}
- name: Validate local Flutter packages
run: bash tool/check_plugins.sh
@@ -87,8 +99,77 @@ jobs:
dart analyze
dart test
desktop:
name: Desktop ${{ matrix.platform }}
if: (github.event_name == 'workflow_dispatch' || !github.event.repository.private) && !(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux
os: ubuntu-latest
- platform: windows
os: windows-2022
- platform: macos
os: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
core/go.sum
plugins/setup/setup_hooks/lib/src/go_builder.dart
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
key: desktop-${{ matrix.platform }}
workspaces: |
services/helper
plugins/rust_api/rust
cache-directories: .dart_tool/hooks_runner
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Linux build dependencies
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev libsecret-1-dev
- name: Setup Xcode
if: 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: Enable git long paths (Windows)
if: matrix.platform == 'windows'
run: git config --global core.longpaths true
- name: Install dependencies
run: flutter pub get
# Compiles the desktop runners and the native plugin code that only the
# tagged release matrix built before; the bundle itself is discarded.
- name: Build debug bundle
run: flutter build ${{ matrix.platform }} --debug
go:
name: Go core
if: *checks
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -100,6 +181,7 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: ${{ env.BRANCH_CACHE }}
cache-dependency-path: core/go.sum
- name: Validate Go core wrapper
@@ -111,46 +193,48 @@ jobs:
echo 'No Go tests found in core/; the test step would pass vacuously.' >&2
exit 1
fi
test -z "$(gofmt -l ./*.go ./platform ./tun)" || {
gofmt -l ./*.go ./platform ./tun >&2
test -z "$(gofmt -l ./*.go ./cmd ./mobile ./platform ./tun)" || {
gofmt -l ./*.go ./cmd ./mobile ./platform ./tun >&2
exit 1
}
go test -count=1 .
go vet .
go vet . ./cmd/...
GOOS=linux go vet ./platform
- name: Setup NDK
uses: nttld/setup-ndk@v1
id: ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
- name: Validate the Android core library
working-directory: core
env:
ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
CGO_ENABLED: 0
run: |
# Nothing else compiles the `android && cgo` files before a `v*` tag.
CC="$(echo "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/aarch64-linux-android21-clang)"
test -x "$CC" || { echo "NDK clang not found at $CC" >&2; exit 1; }
tags="$(sed -n 's/^tags: *//p' ../build_config.yaml)"
test -n "$tags" || { echo 'Could not read tags from build_config.yaml' >&2; exit 1; }
export CGO_ENABLED=1 GOOS=android GOARCH=arm64 CC
go vet "-tags=$tags" . ./tun ./platform
GOOS=android GOARCH=arm64 go vet "-tags=$tags" . ./mobile ./tun ./platform
android:
name: Android unit tests
if: *checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: ${{ env.BRANCH_CACHE }}
cache-dependency-path: |
core/go.sum
android/core/build.gradle.kts
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache: ${{ env.BRANCH_CACHE }}
- name: Install dependencies
run: flutter pub get
@@ -160,6 +244,7 @@ jobs:
id: ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
link-to-sdk: true
- name: Setup Java
uses: actions/setup-java@v4
@@ -169,6 +254,8 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
- name: Run Android unit tests
working-directory: android
@@ -183,13 +270,15 @@ jobs:
test -d "$wrapper" || flutter precache
cp -R "$wrapper/." .
chmod +x gradlew
# `:core` only calls System.loadLibrary; the excluded Flutter tasks would run
# the native build hooks for every debug ABI, which no JVM unit test consumes.
# No JVM unit test loads the Core or the Flutter build, but `:core` compiles against
# the gomobile AAR, so one ABI of it is built.
./gradlew :common:testDebugUnitTest :service:testDebugUnitTest :app:testDebugUnitTest \
-Ptarget-platform=android-arm64 \
-x :app:compileFlutterBuildDebug -x :app:copyFlutterAssetsDebug
rust:
name: Rust
if: *checks
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -201,6 +290,7 @@ jobs:
workspaces: |
services/helper
plugins/rust_api/rust
save-if: ${{ env.BRANCH_CACHE }}
- name: Check format
run: |
@@ -214,6 +304,7 @@ jobs:
windows-helper-test:
name: Windows helper test
if: *checks
runs-on: windows-2022
steps:
- name: Checkout
@@ -223,6 +314,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: services/helper
save-if: ${{ env.BRANCH_CACHE }}
- name: Check format
run: cargo fmt --manifest-path services/helper/Cargo.toml -- --check
@@ -231,8 +323,8 @@ jobs:
run: cargo test --manifest-path services/helper/Cargo.toml --features windows-service
build:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: [ dart, plugins, go, android, rust, windows-helper-test ]
# Releases publish where the release links point: the public repository.
if: github.repository == 'chen08209/FlClash' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -254,9 +346,11 @@ jobs:
- platform: windows
os: windows-11-arm
arch: arm64
flutter-channel: master
- platform: linux
os: ubuntu-24.04-arm
arch: arm64
flutter-channel: master
steps:
- name: Setup rust
@@ -277,7 +371,6 @@ jobs:
if: startsWith(matrix.platform,'android')
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 }}"
echo "storePassword=${{ secrets.STORE_PASSWORD }}"
@@ -288,15 +381,9 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: ${{ env.BRANCH_CACHE }}
cache-dependency-path: core/go.sum
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.platform }}-${{ matrix.arch || 'all' }}
workspaces: services/helper
cache-directories: .dart_tool/hooks_runner
- name: Setup Gradle
if: startsWith(matrix.platform,'android')
uses: gradle/actions/setup-gradle@v6
@@ -310,9 +397,9 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: master
channel: ${{ matrix.flutter-channel || 'stable' }}
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache: ${{ env.BRANCH_CACHE }}
- name: Setup Xcode
if: startsWith(matrix.platform,'macos')
@@ -334,6 +421,23 @@ jobs:
flutter --version
flutter pub get
# See .agents/flutter-upgrade.md before bumping FLUTTER_VERSION.
- name: Disable Impeller on Intel macOS
if: matrix.platform == 'macos' && matrix.arch == 'amd64'
run: plutil -replace FLTEnableImpeller -bool NO macos/Runner/Info.plist
# The core embeds this bundle and FlClashHttpOverrides.trustCoreRoots loads it for Dart.
# Pinned by hash, so a new bundle from https://curl.se/docs/caextract.html is a reviewed bump.
- name: Fetch CA certificates
shell: bash
env:
CA_BUNDLE: cacert-2026-09-25.pem
CA_BUNDLE_SHA256: a41b5d356aea97a529fe27e0f7316d2f9d946d75927476cf9cf1b90637d00505
run: |
bundle=core/Clash.Meta/component/ca/ca-certificates.crt
curl -fsSL --retry 3 -o "$bundle" "https://curl.se/ca/$CA_BUNDLE"
echo "$CA_BUNDLE_SHA256 $bundle" | sha256sum -c -
- name: Setup
env:
ANDROID_NDK_HOME: ${{ steps.build-ndk.outputs.ndk-path }}
@@ -349,7 +453,7 @@ jobs:
upload:
permissions:
contents: write
needs: [ build ]
needs: [ build, dart, plugins, go, android, rust, windows-helper-test ]
runs-on: ubuntu-latest
services:
telegram-bot-api:
@@ -377,7 +481,7 @@ jobs:
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache: ${{ env.BRANCH_CACHE }}
- name: Install dependencies
run: flutter pub get
+81
View File
@@ -0,0 +1,81 @@
name: pages
on:
push:
branches:
- main
paths:
- 'site/**'
- 'tool/build_site.sh'
- 'tool/render_site.mjs'
- '.github/workflows/pages.yaml'
workflow_run:
workflows:
- build
types:
- completed
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
tag:
# head_branch also names a branch push's branch, so only a tag at the built commit counts as a release.
if: >-
github.repository == 'chen08209/FlClash' &&
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_branch, 'v') &&
!contains(github.event.workflow_run.head_branch, '-')
runs-on: ubuntu-latest
outputs:
stable: ${{ steps.lookup.outputs.stable }}
steps:
- name: Look up the tag
id: lookup
env:
NAME: ${{ github.event.workflow_run.head_branch }}
SHA: ${{ github.event.workflow_run.head_sha }}
REMOTE: ${{ github.server_url }}/${{ github.repository }}
run: |
if git ls-remote --tags "$REMOTE" "refs/tags/$NAME" "refs/tags/$NAME^{}" | grep -q "^$SHA"; then
echo 'stable=true' >> "$GITHUB_OUTPUT"
fi
deploy:
needs: tag
if: >-
github.repository == 'chen08209/FlClash' &&
!cancelled() &&
(github.event_name != 'workflow_run' || needs.tag.outputs.stable == 'true')
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Assemble site
env:
GH_TOKEN: ${{ github.token }}
run: bash tool/build_site.sh _site
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload
uses: actions/upload-pages-artifact@v3
with:
path: _site
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
+1
View File
@@ -57,6 +57,7 @@ docs/
/android/**/build
/android/common/**/.**/
/android/common/local.*
# Left in place by builds from before the gomobile Core.
/android/core/**/includes/
/android/core/**/cmake-build-*/
/android/core/**/jniLibs/
+2 -2
View File
@@ -9,7 +9,7 @@ repos:
stages: [pre-commit]
- id: comment-density
name: comment density
entry: tool/check_comment_density.sh
entry: tool/hooks/comment_density.sh
language: system
files: \.(dart|kt|kts|swift|go|rs|java|cpp|cc|h|hpp|gradle|ya?ml)$
stages: [pre-commit]
@@ -21,6 +21,6 @@ repos:
stages: [pre-push]
- id: commit-msg-format
name: conventional commit message
entry: tool/check_commit_msg.sh
entry: tool/hooks/commit_msg.sh
language: system
stages: [commit-msg]
+7
View File
@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart (safe mode)" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="additionalArgs" value="--dart-define-from-file env.json --dart-define=SAFE_MODE=true" />
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>
+24
View File
@@ -0,0 +1,24 @@
[
{
"label": "Flutter App (With Env)",
"adapter": "Dart",
"type": "flutter",
"request": "launch",
"program": "lib/main.dart",
"cwd": "$ZED_WORKTREE_ROOT",
"toolArgs": ["--dart-define-from-file", "env.json"],
},
{
"label": "Flutter App (Safe Mode)",
"adapter": "Dart",
"type": "flutter",
"request": "launch",
"program": "lib/main.dart",
"cwd": "$ZED_WORKTREE_ROOT",
"toolArgs": [
"--dart-define-from-file",
"env.json",
"--dart-define=SAFE_MODE=true",
],
},
]
+21
View File
@@ -0,0 +1,21 @@
[
{
"label": "flutter run (With Env)",
"command": "flutter",
"args": ["run", "--dart-define-from-file", "env.json"],
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": true,
},
{
"label": "flutter run (Safe Mode)",
"command": "flutter",
"args": [
"run",
"--dart-define-from-file",
"env.json",
"--dart-define=SAFE_MODE=true",
],
"cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": true,
},
]
+26 -5
View File
@@ -15,9 +15,15 @@ Read these only when the task touches their area:
- [.agents/architecture.md](.agents/architecture.md): core integration, providers, database, managers, build system, and
local plugins.
- [.agents/setting-changes.md](.agents/setting-changes.md): whether a changed setting is patched live, re-applies the
profile, or asks for an Android VPN restart.
- [.agents/surfaces.md](.agents/surfaces.md): fill levels, selection and status colors, and the few borders that
remain, for any change to how a surface is colored.
- [.agents/agent-config.md](.agents/agent-config.md): how to choose between `AGENTS.md`, `.agents`, skills, Codex config,
command rules, and hooks.
- [.agents/worktrees.md](.agents/worktrees.md): worktree hygiene across Claude Code, Codex, and Gemini.
- [.agents/flutter-upgrade.md](.agents/flutter-upgrade.md): SDK-pinned workarounds to recheck before changing the
Flutter version.
- [.agents/skills.md](.agents/skills.md): index of repo-scoped skills in `.agents/skills/`.
## Highest Priority Rules
@@ -25,15 +31,21 @@ Read these only when the task touches their area:
- When the user explicitly requests a scoped, low-risk change, inspect the relevant context and implement it directly.
Do not require brainstorming, design documents, implementation plans, multiple-option proposals, or repeated confirmation.
Ask only when material ambiguity, destructive impact, additional authority, or scope expansion could change the result.
- The request sets the scope. A pre-existing bug, cleanup, or refactor the task did not ask for is a follow-up in the
summary, not part of the change. Edit a file surgically when a rewrite would give the same result, keep scratch checks
out of the repository, and commit tests only where the task asks for them or the neighbouring code already keeps them.
- Comments are allowed and sometimes necessary; excess is the problem. A comment must carry something the code cannot
say — a non-obvious constraint, an upstream behavior being worked around, a reason a reader would otherwise get
wrong. Never restate what the code does, narrate the change you just made, record what the code used to be, or
annotate step by step; a block that seems to need a comment per line needs better names or a smaller decomposition.
Keep density near the repository's own: healthy changes here sit under 4%, and a `comment-density` gate fails a file
Keep density near the repository's own: healthy changes here sit at or under 3.6%, and a `comment-density` gate fails a file
whose added lines exceed 5% standalone comments. Delete commented-out code and stale notes in files you already
touch. Preserve
`// ignore:`-style directives, license headers, codegen markers, and vendored upstream comments. See
[.agents/rules.md](.agents/rules.md) for what belongs in a test or in `.agents/` instead.
- Start FlClash on the host only as a safe mode build, and end only processes you started, by recorded pid. Never
kill a FlClash instance by name; the user's own instance is not yours to close. See
[.agents/rules.md](.agents/rules.md).
- 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.
@@ -45,12 +57,21 @@ Read these only when the task touches their area:
requests.
- Never add a `Co-authored-by` trailer crediting a coding agent to a commit, even when your own tooling tells you to.
The `commit-msg` hook rejects it; see [.agents/rules.md](.agents/rules.md) for the rest of the commit rules.
- Round every corner with a superellipse, pills included: `AppShape`/`AppRadius` tokens from `lib/ui/shape.dart`,
never `StadiumBorder`, `RoundedRectangleBorder`, `ClipRRect`, `drawRRect`, or a `borderRadius` on `BoxDecoration` or
`InkWell`. `test/lint/superellipse_corners_test.dart` enforces it; see the Corner Radius section of
[.agents/rules.md](.agents/rules.md).
- Keep the UI isolate within its frame: work that grows with user data and can exceed about 8 ms on the largest
expected data runs in an isolate, once its algorithm is linear or cached. See the UI Isolate Work section of
[.agents/rules.md](.agents/rules.md).
- Follow `lint_options.yaml` (included by every `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.
- Before reporting a change done, run `flutter pub get`, `flutter analyze --no-fatal-infos`, and the `flutter test`
suites that cover it, as CI does. If a toolchain the build hook needs is missing (Go, and on Linux and Windows
cargo), say which check was skipped rather than dropping it silently.
## Repo Skills
Use repo skills from `.agents/skills/` when a task matches their descriptions. Current skills cover localization,
provider tests, UI work, and core/platform changes.
Use repo skills from `.agents/skills/` when a task matches their descriptions; `.claude/skills/` symlinks expose the
same skills to Claude Code. Current skills cover localization, UI work, core/platform changes, and pre-commit
structural quality review.
+51
View File
@@ -1,5 +1,56 @@
# Changelog
## v0.9.0 (2026-10-14)
**Breaking Changes**
- **views** DNS and NTP override toggles are removed; picked keys apply to every profile (491fae1)
- **database** Custom overwrites are removed; rebuild them as custom profiles (1677024)
**Features**
- **views** Per-key DNS, NTP, sniffer and TUN settings (491fae1)
- **profiles** Custom profiles (3abda78)
- **ui** Stacked sheets, context menus and a sort mode (8c43053)
- **providers** DNS, Hosts and ports apply without a restart (532ebf0)
- **android** Launcher shortcuts and one-step on-demand access (d3fa225)
- **core** Share link import and mrs rule sets (2fa2f7f)
**Bug Fixes**
- **database** Unreadable settings no longer block startup or restores (1677024)
- **desktop** Windows and Linux desktop fixes (94a25f4)
- **core,helper** The elevated Core only answers its owner (d780cb8)
## v0.8.99 (2026-10-03)
**Features**
- **editor** Editor word wrap and snippets (e131675)
- **dashboard** More dashboard cards (e62afb7)
- **views** Hide timed-out nodes and search every list (8378c70)
- **ui** Sidebar, floating dock and drag to go back (841d603)
- **ui** New app glyphs, empty states and theme page (ee6a497)
- **overwrite** Invalid override warnings and rule presets (46fb7fe)
- **config** DNS query log and per-key DNS/NTP overrides (a390a42)
- **core** Proxied service checks and outbound IP risk (8068e68)
- **platform** Tray hotkeys, node delays and a delay test (1e97716)
**Bug Fixes**
- **core** A changed resource link is now used by the next sync and by auto update (ff23f30)
- **core** DNS lookups no longer fail on networks that report no DNS servers (e55f6d1)
- **proxies** Smaller icons in icon-only group headers (786a6ab)
- **core,android** Push notifications reconnect in Doze (1e9955d)
- **backup,database** Restores never apply halfway (bee8f24)
## v0.8.98 (2026-09-14)
**Bug Fixes**
- **resources** Refresh the geo file size and time after an update finishes (c5bf5bd)
- **core** Keep the core running while Windows sleeps with the app suspended (60f371a)
## v0.8.97 (2026-09-10)
**Features**
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+100 -100
View File
@@ -1,138 +1,138 @@
<div>
<div align="center">
[**简体中文**](README_zh_CN.md)
<img src="assets/images/icon.png" alt="FlClash" width="88">
# FlClash
A multi-platform proxy client based on ClashMeta. Simple to use, open source and ad-free.
**English** · [简体中文](README_zh_CN.md)
[![Release](https://img.shields.io/github/v/release/chen08209/FlClash?style=flat-square&label=release)](https://github.com/chen08209/FlClash/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases)
[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Telegram](https://img.shields.io/badge/Telegram-channel-26A5E4?style=flat-square&logo=telegram&logoColor=white)](https://t.me/FlClash)
[Website](https://chen08209.github.io/FlClash) · [Download](#download) · [Changelog](CHANGELOG.md) · [Build from source](#build-from-source)
</div>
## FlClash
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
on Desktop:
<p style="text-align: center;">
<img alt="desktop" src="snapshots/desktop.gif">
</p>
on Mobile:
<p style="text-align: center;">
<img alt="mobile" src="snapshots/mobile.gif">
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="snapshots/preview-dark.png">
<img alt="FlClash dashboard on a MacBook and a phone" src="snapshots/preview.png" width="92%">
</picture>
</p>
## Features
✈️ Multi-platform: Android, Windows, macOS and Linux
💻 Adaptive multiple screen sizes, Multiple color themes available
💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI
☁️ Supports data sync via WebDAV
✨ Support subscription link, Dark mode
## Use
### Linux
⚠️ Make sure to install the following dependencies before using them
```bash
sudo apt-get install libayatana-appindicator3-dev
```
### Android
Support the following actions
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.TOGGLE
```
- **One app for Android, Windows, macOS and Linux**, with x64 and ARM64 builds on the desktop.
- **mihomo (Clash.Meta) core** with rule routing, proxy groups, latency tests, system proxy and TUN mode.
- **Profiles** from a subscription link or a file, with a built-in editor, override scripts, and custom rules,
proxies and proxy groups.
- **Live views** of connections, requests, DNS queries and logs.
- **Material You design with Apple-style interaction**: dynamic color, light and dark themes, continuous corners,
sheets that snap between heights, and layouts that adapt from phones to desktops.
- **Backup and restore** through WebDAV or a local file.
- **Platform touches**: a Quick Settings tile, per-app proxy and Android TV support on Android; a tray menu and
global hotkeys on the desktop.
- **Open source and ad-free**, licensed under GPL-3.0.
## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
Get the latest build from [GitHub Releases](https://github.com/chen08209/FlClash/releases/latest), or open the
[website](https://chen08209.github.io/FlClash#download), which picks the right file for your device.
### Homebrew
| Platform | Packages | Notes |
| --- | --- | --- |
| Android | APK for `arm64-v8a`, `armeabi-v7a` and `x86_64` | Most phones use `arm64-v8a`. Also on the F-Droid repository below. |
| Windows 10 and later | Installer (`.exe`) or portable `.zip`, for x64 and ARM64 | Pick ARM64 on Snapdragon and other ARM laptops. |
| macOS 12 and later | DMG for Apple Silicon and Intel | Also on Homebrew. |
| Linux | `.deb`, `.rpm` and AppImage, for x64 and ARM64 | See the tray note below. |
<p>
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" height="56"></a>
<a href="https://github.com/chen08209/FlClash/releases/latest"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" height="56"></a>
</p>
**Homebrew**
```bash
brew tap chen08209/tap
brew install --cask flclash
```
## Build
**Linux tray icon**
1. Update submodules
```bash
git submodule update --init --recursive
```
The `.deb` package installs its own dependencies. With the AppImage or the `.rpm`, install the AyatanaAppIndicator
library so the tray icon can show:
2. Install `Flutter` and `Golang` environment
```bash
sudo apt-get install libayatana-appindicator3-1 # Debian and Ubuntu
sudo dnf install libayatana-appindicator-gtk3 # Fedora
```
3. Build Application
## Usage
- android
**Import a profile from a link.** Opening a link in this form imports the subscription into FlClash. The `clashmeta://`
and `flclash://` schemes work the same way.
1. Install `Android SDK`, `Android NDK`
```text
clash://install-config?url=<URL-encoded subscription link>
```
2. Set `ANDROID_NDK` environment variable
**Automate on Android.** Tasker, MacroDroid and similar apps can start, stop or toggle the proxy by starting an
activity with one of these actions:
3. Run build script
```text
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.TOGGLE
```
```bash
dart setup.dart android
```
From a computer, the same works over adb:
- windows
```bash
adb shell am start -a com.follow.clash.action.TOGGLE
```
1. Requires a Windows client
## Build from source
2. Install `GCC`, `Inno Setup`
You need [Flutter](https://docs.flutter.dev/get-started/install) 3.47 (release builds use 3.47.6),
[Go](https://go.dev/dl/) 1.27 and [Rust](https://rustup.rs/) installed through rustup. Each platform builds on its own
host, except Android, which builds anywhere.
3. Run build script
```bash
git clone --recursive https://github.com/chen08209/FlClash.git
cd FlClash
flutter pub get
dart setup.dart android # or windows, macos, linux
```
```bash
dart setup.dart windows
```
Packages land in `dist/`. The Go core and the Rust libraries are compiled as part of the Flutter build.
- linux
| Platform | Also needed |
| --- | --- |
| Android | Android SDK with the NDK. Add `--arch arm64` to build a single ABI. |
| Windows | GCC (MinGW-w64) for the core and [Inno Setup](https://jrsoftware.org/isinfo.php) 6 for the installer. |
| macOS | Xcode and Node.js. The script installs `appdmg` through npm. |
| Linux | Debian or Ubuntu. The script installs the build packages with apt and downloads `appimagetool`. |
1. Requires a Linux client
Run `dart setup.dart --help` for the remaining options, such as `--targets` to build only some package formats.
2. Dependencies are auto-installed by setup script, or manually:
```bash
sudo apt-get install -y libayatana-appindicator3-dev
```
## Support
3. Run build script
Starring the repository is the easiest way to support the project. Questions and announcements go to the
[Telegram channel](https://t.me/FlClash); bugs and feature requests go to
[GitHub Issues](https://github.com/chen08209/FlClash/issues).
```bash
dart setup.dart linux
```
<a href="https://star-history.com/#chen08209/FlClash&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=chen08209/FlClash&type=Date&theme=dark">
<img alt="Star history" src="https://api.star-history.com/svg?repos=chen08209/FlClash&type=Date" width="640">
</picture>
</a>
- macOS
## License
1. Requires a macOS client
2. Run build script
```bash
dart setup.dart macos
```
## Star
The easiest way to support developers is to click on the star (⭐) at the top of the page.
<p style="text-align: center;">
<a href="https://api.star-history.com/svg?repos=chen08209/FlClash&Date">
<img alt="start" width=50% src="https://api.star-history.com/svg?repos=chen08209/FlClash&Date"/>
</a>
</p>
FlClash is released under the [GPL-3.0 license](LICENSE).
+96 -104
View File
@@ -1,138 +1,130 @@
<div>
<div align="center">
[**English**](README.md)
<img src="assets/images/icon.png" alt="FlClash" width="88">
# FlClash
基于 ClashMeta 的多平台代理客户端,简单易用,开源无广告。
[English](README.md) · **简体中文**
[![Release](https://img.shields.io/github/v/release/chen08209/FlClash?style=flat-square&label=release)](https://github.com/chen08209/FlClash/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases)
[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Telegram](https://img.shields.io/badge/Telegram-channel-26A5E4?style=flat-square&logo=telegram&logoColor=white)](https://t.me/FlClash)
[官网](https://chen08209.github.io/FlClash/zh) · [下载](#下载) · [更新日志](CHANGELOG.md) · [从源码构建](#从源码构建)
</div>
## FlClash
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
on Desktop:
<p style="text-align: center;">
<img alt="desktop" src="snapshots/desktop.gif">
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="snapshots/preview-dark.png">
<img alt="FlClash 在 MacBook 与手机上的仪表盘" src="snapshots/preview.png" width="92%">
</picture>
</p>
on Mobile:
<p style="text-align: center;">
<img alt="mobile" src="snapshots/mobile.gif">
## 功能
- **一个应用覆盖 Android、Windows、macOS 和 Linux**,桌面端同时提供 x64 与 ARM64 版本。
- **mihomo(Clash.Meta)内核**:规则分流、代理组、延迟测试、系统代理与 TUN 模式。
- **配置管理**:通过订阅链接或文件导入配置,内置编辑器,支持覆写脚本以及自定义规则、代理和代理组。
- **实时查看**连接、请求、DNS 查询和日志。
- **Material You 设计,融合 Apple 式交互**:支持动态取色、浅色与深色主题,连续曲率圆角,弹出面板可在几档高度间吸附,布局随屏幕从手机到桌面自适应。
- **备份与恢复**:通过 WebDAV 或本地文件。
- **平台细节**:Android 上有快捷设置磁贴、分应用代理和 Android TV 支持;桌面端有托盘菜单和全局快捷键。
- **开源无广告**,以 GPL-3.0 协议发布。
## 下载
从 [GitHub Releases](https://github.com/chen08209/FlClash/releases/latest) 获取最新版本,或打开
[官网](https://chen08209.github.io/FlClash/zh#download),它会自动为你的设备选好安装包。
| 平台 | 安装包 | 说明 |
| --- | --- | --- |
| Android | `arm64-v8a`、`armeabi-v7a`、`x86_64` 三种 APK | 绝大多数手机选 `arm64-v8a`。也可以通过下方的 F-Droid 仓库安装。 |
| Windows 10 及以上 | 安装版(`.exe`)或便携版(`.zip`),分 x64 与 ARM64 | 骁龙等 ARM 架构笔记本选 ARM64。 |
| macOS 12 及以上 | Apple Silicon 与 Intel 两种 DMG | 也可以通过 Homebrew 安装。 |
| Linux | `.deb`、`.rpm` 和 AppImage,分 x64 与 ARM64 | 托盘依赖见下方说明。 |
<p>
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" height="56"></a>
<a href="https://github.com/chen08209/FlClash/releases/latest"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" height="56"></a>
</p>
## Features
✈️ 多平台: Android, Windows, macOS and Linux
💻 自适应多个屏幕尺寸,多种颜色主题可供选择
💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面
☁️ 支持通过WebDAV同步数据
✨ 支持一键导入订阅, 深色模式
## Use
### Linux
⚠️ 使用前请确保安装以下依赖
```bash
sudo apt-get install libayatana-appindicator3-dev
```
### Android
支持下列操作
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.TOGGLE
```
## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
### Homebrew
**Homebrew**
```bash
brew tap chen08209/tap
brew install --cask flclash
```
## Build
**Linux 托盘图标**
1. 更新 submodules
```bash
git submodule update --init --recursive
```
`.deb` 安装包会自动安装所需依赖。使用 AppImage 或 `.rpm` 时,需要先安装 AyatanaAppIndicator 库,托盘图标才能显示:
2. 安装 `Flutter` 以及 `Golang` 环境
```bash
sudo apt-get install libayatana-appindicator3-1 # Debian 与 Ubuntu
sudo dnf install libayatana-appindicator-gtk3 # Fedora
```
3. 构建应用
## 使用
- android
**通过链接导入配置。** 打开下面格式的链接,即可把订阅导入 FlClash。`clashmeta://` 和 `flclash://` 开头的链接同样可用。
1. 安装 `Android SDK` , `Android NDK`
```text
clash://install-config?url=<经过 URL 编码的订阅链接>
```
2. 设置 `ANDROID_NDK` 环境变量
**Android 自动化。** Tasker、MacroDroid 等应用可以用下面的 Action 启动 Activity,从而启动、停止或切换代理:
3. 运行构建脚本
```text
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.TOGGLE
```
```bash
dart setup.dart android
```
在电脑上也可以通过 adb 触发:
- windows
```bash
adb shell am start -a com.follow.clash.action.TOGGLE
```
1. 你需要一个windows客户端
## 从源码构建
2. 安装 `GCC`,`Inno Setup`
需要 [Flutter](https://docs.flutter.dev/get-started/install) 3.47(正式版构建使用 3.47.6)、[Go](https://go.dev/dl/) 1.27,
以及通过 rustup 安装的 [Rust](https://rustup.rs/)。桌面端需要在对应系统上构建,Android 在任意系统上都能构建。
3. 运行构建脚本
```bash
git clone --recursive https://github.com/chen08209/FlClash.git
cd FlClash
flutter pub get
dart setup.dart android # 或 windows、macos、linux
```
```bash
dart setup.dart windows
```
安装包输出到 `dist/`。Go 内核和 Rust 库会在 Flutter 构建过程中一并编译。
- linux
| 平台 | 额外依赖 |
| --- | --- |
| Android | 带 NDK 的 Android SDK。加上 `--arch arm64` 可以只构建单个 ABI。 |
| Windows | 编译内核用的 GCC(MinGW-w64),以及打包安装程序用的 [Inno Setup](https://jrsoftware.org/isinfo.php) 6。 |
| macOS | Xcode 与 Node.js。脚本会通过 npm 安装 `appdmg`。 |
| Linux | Debian 或 Ubuntu。脚本会用 apt 安装构建所需的软件包,并下载 `appimagetool`。 |
1. 你需要一个linux客户端
其余选项可以运行 `dart setup.dart --help` 查看,例如用 `--targets` 只构建部分安装包格式。
2. 依赖会由 setup 脚本自动安装,也可以手动安装:
```bash
sudo apt-get install -y libayatana-appindicator3-dev
```
## 支持
3. 运行构建脚本
给仓库点一个 Star 是支持项目最简单的方式。问题讨论和更新通知请关注 [Telegram 频道](https://t.me/FlClash),
Bug 与功能建议请提交到 [GitHub Issues](https://github.com/chen08209/FlClash/issues)。
```bash
dart setup.dart linux
```
<a href="https://star-history.com/#chen08209/FlClash&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=chen08209/FlClash&type=Date&theme=dark">
<img alt="Star history" src="https://api.star-history.com/svg?repos=chen08209/FlClash&type=Date" width="640">
</picture>
</a>
- macOS
## 许可证
1. 你需要一个macOS客户端
2. 运行构建脚本
```bash
dart setup.dart macos
```
## Star
支持开发者的最简单方式是点击页面顶部的星标(⭐)。
<p style="text-align: center;">
<a href="https://api.star-history.com/svg?repos=chen08209/FlClash&Date">
<img alt="start" width=50% src="https://api.star-history.com/svg?repos=chen08209/FlClash&Date"/>
</a>
</p>
FlClash 以 [GPL-3.0 协议](LICENSE)发布。
-5
View File
@@ -4,8 +4,6 @@ import java.util.Properties
plugins {
id("com.android.application")
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
val localProperties = Properties().apply {
@@ -107,9 +105,6 @@ dependencies {
implementation(libs.smali.dexlib2) {
exclude(group = "com.google.guava", module = "guava")
}
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics.ndk)
implementation(libs.firebase.analytics)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
}
-65
View File
@@ -1,65 +0,0 @@
{
"project_info": {
"project_number": "000000000000",
"project_id": "dev"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash.debug"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash.dev"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
]
}
+5
View File
@@ -27,6 +27,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<application
android:name=".FlClashApplication"
@@ -45,6 +46,10 @@
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!-- install-config links go through app_links; Flutter would also push them as routes. -->
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -1,6 +1,7 @@
package com.follow.clash
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.BatteryPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity
@@ -10,6 +11,7 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(BatteryPlugin())
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin())
ServiceState.attachFlutterEngine(flutterEngine)
@@ -1,28 +1,37 @@
package com.follow.clash
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.core.content.pm.ShortcutManagerCompat
import com.follow.clash.common.GlobalState
import com.follow.clash.common.QuickAction
import com.follow.clash.common.action
import com.follow.clash.common.shortcutId
import kotlinx.coroutines.launch
class QuickActionActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
QuickAction.START.action -> GlobalState.launch { ServiceState.handleStartAction() }
QuickAction.STOP.action -> GlobalState.launch { ServiceState.handleStopAction() }
QuickAction.TOGGLE.action -> {
ShortcutManagerCompat.reportShortcutUsed(this, SHORTCUT_ID)
GlobalState.launch { ServiceState.handleToggleAction() }
}
}
handle(intent)
finish()
}
private companion object {
const val SHORTCUT_ID = "toggle"
// A caller that repeats an action before this instance is gone reaches it here, not onCreate.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handle(intent)
}
private fun handle(intent: Intent) {
val action = QuickAction.entries.firstOrNull { it.action == intent.action } ?: return
ShortcutManagerCompat.reportShortcutUsed(this, action.shortcutId)
GlobalState.launch {
when (action) {
QuickAction.START -> ServiceState.handleStartAction()
QuickAction.STOP -> ServiceState.handleStopAction()
QuickAction.TOGGLE -> ServiceState.handleToggleAction()
}
}
}
}
@@ -34,15 +34,9 @@ class ServiceBroadcastReceiver : BroadcastReceiver() {
private suspend fun handleAction(action: String) {
when (action) {
BroadcastAction.VPN_START_REQUESTED.action -> {
GlobalState.log("System requested VPN service start")
ServiceState.handleStartAction()
}
BroadcastAction.VPN_START_REQUESTED.action -> ServiceState.handleStartAction()
BroadcastAction.VPN_REVOKED.action -> {
GlobalState.log("VPN permission revoked")
ServiceState.handleVpnRevokeAction()
}
BroadcastAction.VPN_REVOKED.action -> ServiceState.handleVpnRevokeAction()
}
}
@@ -30,21 +30,26 @@ import kotlin.coroutines.resume
object ServiceController {
private val lock = Mutex()
private var binding: ManagedServiceBinding? = null
@Volatile
private var runTimeMillis = 0L
suspend fun unbind() = lock.withLock {
clearBinding()
}
@Volatile
internal var runtime: ServiceRuntime? = null
private set
private fun clearBinding() {
binding?.unbind()
binding = null
}
fun invokeMethod(data: String, callback: (String) -> Unit): Result<Unit> = runCatching {
// The Core listens exactly while a run exists, so every path that ends one closes it.
private fun release() {
clearBinding()
runtime = null
Core.stopListener()
}
fun invokeMethod(data: String, callback: (ByteArray) -> Unit): Result<Unit> = runCatching {
Core.invokeMethod(data) { result ->
callback(result.orEmpty())
callback(result ?: ByteArray(0))
}
}
@@ -59,11 +64,11 @@ object ServiceController {
}
}
fun setEventListener(callback: ((String?) -> Unit)?): Result<Unit> = runCatching {
fun setEventListener(callback: ((ByteArray) -> Unit)?): Result<Unit> = runCatching {
Core.updateEventListener(callback)
}
suspend fun start(options: VpnOptions): Long = lock.withLock {
suspend fun start(options: VpnOptions): Boolean = lock.withLock {
ServiceConfig.updateVpnOptions(options)
val nextIntent = if (options.enable) {
VpnService::class.intent
@@ -82,13 +87,12 @@ object ServiceController {
binding = nextBinding
nextBinding.bind().onFailure { error ->
GlobalState.log("Unable to bind background service: $error")
clearBinding()
runTimeMillis = 0L
return@withLock runTimeMillis
release()
return@withLock false
}
}
val currentBinding = binding ?: return@withLock 0L
val currentBinding = binding ?: return@withLock false
val result = currentBinding.useService { service -> service.start() }
if (result.isFailure) {
GlobalState.log("Unable to start background service: ${result.exceptionOrNull()}")
@@ -96,20 +100,21 @@ object ServiceController {
.onFailure { error ->
GlobalState.log("Unable to clean up failed background service start: $error")
}
clearBinding()
runTimeMillis = 0L
return@withLock runTimeMillis
release()
return@withLock false
}
if (runTimeMillis == 0L) {
runTimeMillis = System.currentTimeMillis()
}
runTimeMillis
Core.startListener()
runtime = ServiceRuntime(
vpn = options.enable,
startedAtMillis = runtime?.startedAtMillis ?: System.currentTimeMillis(),
)
true
}
suspend fun stop() = lock.withLock {
tearDownServices()
runTimeMillis = 0L
release()
}
private suspend fun tearDownServices() {
@@ -133,12 +138,6 @@ object ServiceController {
}
}
suspend fun isVpnServiceActive(): Boolean = lock.withLock {
runTimeMillis != 0L && binding?.component == VpnService::class.intent.component
}
fun getRunTimeMillis(): Long = runTimeMillis
private fun handleServiceDisconnected(
disconnectedBinding: ManagedServiceBinding,
message: String,
@@ -150,8 +149,7 @@ object ServiceController {
return@withLock false
}
GlobalState.log("Background service disconnected: $message")
clearBinding()
runTimeMillis = 0L
release()
true
}
if (wasCurrent) {
@@ -9,6 +9,9 @@ object ServiceState {
val runState = machine.runState
val isRunRequested: Boolean
get() = machine.isRunRequested
fun attachFlutterEngine(engine: FlutterEngine) =
AndroidServiceStateHost.attachFlutterEngine(engine)
@@ -23,7 +26,7 @@ object ServiceState {
suspend fun handleVpnRevokeAction() = machine.handleVpnRevokeAction()
suspend fun refresh(): Long = machine.refresh()
suspend fun awaitRunTime(): Long = machine.awaitRunTime()
fun requestStart(): Deferred<Boolean> = machine.requestStart()
@@ -1,9 +1,14 @@
package com.follow.clash
import android.Manifest
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import androidx.core.content.ContextCompat
import com.follow.clash.common.GlobalState
import com.follow.clash.models.SharedState
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.service.ServiceConfig
import com.follow.clash.service.models.NotificationParams
@@ -11,13 +16,18 @@ import com.follow.clash.service.models.VpnOptions
import io.flutter.embedding.engine.FlutterEngine
import kotlinx.coroutines.CoroutineScope
internal data class ServiceRuntime(
val vpn: Boolean,
val startedAtMillis: Long,
)
/**
* The machine owns the arbitration and the transitions; this is only the part that cannot run on a
* plain JVM, so unit tests can drive the state machine with an in-memory implementation.
*/
internal interface ServiceStateHost {
val scope: CoroutineScope
val runTimeMillis: Long
val runtime: ServiceRuntime?
val homeDirPath: String
val sdkInt: Int
@@ -25,36 +35,36 @@ internal interface ServiceStateHost {
fun showToast(message: String)
fun setCrashlytics(enabled: Boolean)
fun updateNotificationParams(params: NotificationParams)
fun loadSharedState(): SharedState
fun isVpnPermissionGranted(): Boolean
fun isLocalNetworkPermissionGranted(): Boolean
fun tile(): TileGateway?
fun app(): AppGateway?
fun notifyStopped()
suspend fun quickSetup(initParams: String, setupParams: String): Result<String>
suspend fun startService(options: VpnOptions): Long
suspend fun startService(options: VpnOptions): Boolean
suspend fun stopService()
suspend fun isVpnServiceActive(): Boolean
}
internal interface TileGateway {
internal fun interface TileGateway {
fun handleStart()
fun handleStop()
}
internal interface AppGateway {
fun requestNotificationPermission(callback: (Boolean) -> Unit)
fun requestLocalNetworkPermission(callback: (Boolean) -> Unit)
fun prepareVpn(enable: Boolean, callback: (Boolean) -> Unit)
fun cancelVpnPreparation(callback: (Boolean) -> Unit)
@@ -67,8 +77,8 @@ internal object AndroidServiceStateHost : ServiceStateHost {
override val scope: CoroutineScope
get() = GlobalState
override val runTimeMillis: Long
get() = ServiceController.getRunTimeMillis()
override val runtime: ServiceRuntime?
get() = ServiceController.runtime
override val homeDirPath: String
get() = GlobalState.application.filesDir.path
@@ -90,8 +100,6 @@ internal object AndroidServiceStateHost : ServiceStateHost {
override fun showToast(message: String) = GlobalState.application.showToast(message)
override fun setCrashlytics(enabled: Boolean) = GlobalState.setCrashlytics(enabled)
override fun updateNotificationParams(params: NotificationParams) =
ServiceConfig.updateNotificationParams(params)
@@ -100,12 +108,15 @@ internal object AndroidServiceStateHost : ServiceStateHost {
override fun isVpnPermissionGranted(): Boolean =
VpnService.prepare(GlobalState.application) == null
override fun tile(): TileGateway? = flutterEngine?.plugin<TilePlugin>()?.let { plugin ->
object : TileGateway {
override fun handleStart() = plugin.handleStart()
override fun isLocalNetworkPermissionGranted(): Boolean =
sdkInt < Build.VERSION_CODES.CINNAMON_BUN ||
ContextCompat.checkSelfPermission(
GlobalState.application,
Manifest.permission.ACCESS_LOCAL_NETWORK,
) == PackageManager.PERMISSION_GRANTED
override fun handleStop() = plugin.handleStop()
}
override fun tile(): TileGateway? = flutterEngine?.plugin<TilePlugin>()?.let { plugin ->
TileGateway(plugin::handleStart)
}
override fun app(): AppGateway? = flutterEngine?.plugin<AppPlugin>()?.let { plugin ->
@@ -113,6 +124,9 @@ internal object AndroidServiceStateHost : ServiceStateHost {
override fun requestNotificationPermission(callback: (Boolean) -> Unit) =
plugin.requestNotificationPermission(callback)
override fun requestLocalNetworkPermission(callback: (Boolean) -> Unit) =
plugin.requestLocalNetworkPermission(callback)
override fun prepareVpn(enable: Boolean, callback: (Boolean) -> Unit) =
plugin.prepareVpn(enable, callback)
@@ -121,13 +135,15 @@ internal object AndroidServiceStateHost : ServiceStateHost {
}
}
override fun notifyStopped() {
flutterEngine?.plugin<ServicePlugin>()?.notifyStopped()
}
override suspend fun quickSetup(initParams: String, setupParams: String): Result<String> =
ServiceController.quickSetup(initParams, setupParams)
override suspend fun startService(options: VpnOptions): Long =
override suspend fun startService(options: VpnOptions): Boolean =
ServiceController.start(options)
override suspend fun stopService() = ServiceController.stop()
override suspend fun isVpnServiceActive(): Boolean = ServiceController.isVpnServiceActive()
}
@@ -5,8 +5,10 @@ import com.follow.clash.models.SharedState
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@@ -29,10 +31,16 @@ internal const val INVALID_CONFIG_MESSAGE = "Invalid configuration."
internal const val VPN_PERMISSION_MESSAGE = "VPN permission required."
internal const val START_FAILED_MESSAGE = "Failed to start service."
private enum class StartOutcome {
STARTED,
FAILED,
SUPERSEDED,
}
/**
* Callers request a transition; the newest request always wins. Every step that outlives its own
* suspension point re-checks [isCurrent] before it publishes anything, so a start that was overtaken
* by a stop cannot report itself as started.
* Callers request a transition; the newest request always wins. No step assigns [runState]:
* [publish] derives it from the latest intent, the transition in flight and what the host really
* runs, so no path can leave it claiming a service that is not there.
*/
internal class ServiceStateMachine(private val host: ServiceStateHost) {
private val transitionLock = Mutex()
@@ -46,23 +54,24 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
@Volatile
private var pendingVpnPreparation: (() -> Unit)? = null
@Volatile
private var transition: RunState? = null
val runState = mutableRunState.asStateFlow()
private val runTimeMillis: Long
get() = host.runTimeMillis
val isRunRequested: Boolean
get() = arbiter.isRunningRequested
suspend fun handleToggleAction() {
if (isRunningRequested()) {
if (isRunRequested) {
handleStopAction()
} else {
handleStartAction()
}
}
suspend fun refresh(): Long = transitionLock.withLock {
val current = runTimeMillis
mutableRunState.value = if (current == 0L) RunState.STOPPED else RunState.STARTED
current
suspend fun awaitRunTime(): Long = transitionLock.withLock {
host.runtime?.startedAtMillis?.takeIf { isRunRequested } ?: 0L
}
fun captureRequestToken(): RunRequest = arbiter.current()
@@ -71,18 +80,20 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
* Settles the state after the bound service was lost. [token] is the request that was current
* when the loss was observed, so a start that raced ahead of this callback keeps its intent.
*/
suspend fun handleServiceLost(token: RunRequest) = transitionLock.withLock {
if (runTimeMillis != 0L) {
return@withLock
suspend fun handleServiceLost(token: RunRequest) {
val dropped = transitionLock.withLock {
if (host.runtime != null) {
return@withLock false
}
arbiter.resetToStopped(token).also { publish() }
}
if (!arbiter.resetToStopped(token)) {
return@withLock
if (dropped) {
host.notifyStopped()
}
mutableRunState.value = RunState.STOPPED
}
suspend fun handleStartAction() {
if (isRunningRequested()) {
if (isRunRequested) {
return
}
val tile = host.tile()
@@ -90,72 +101,36 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
tile.handleStart()
return
}
loadPreferencesAndStart()
startFromPreferences()
}
suspend fun handleStopAction() {
if (!isRunningRequested()) {
return
}
val tile = host.tile()
if (tile != null) {
tile.handleStop()
if (!isRunRequested) {
return
}
host.showToast(sharedState.stopTip)
requestStop().await()
host.notifyStopped()
}
suspend fun handleVpnRevokeAction() {
if (!host.isVpnServiceActive()) {
if (host.runtime?.vpn != true) {
return
}
handleStopAction()
}
fun requestStart(): Deferred<Boolean> {
val request = createRequest(running = true)
val result = CompletableDeferred<Boolean>()
val launchRequest: (Boolean) -> Unit = { shouldStart ->
if (!shouldStart) {
fail(request)
result.complete(false)
} else {
host.scope.launch {
result.complete(
runCatching { start(request) }
.onFailure { error ->
host.log("Unable to process service start request: $error")
fail(request)
reconcileStopped()
}
.getOrDefault(false),
)
}
}
if (isStartedAs(sharedState.vpnOptions)) {
return CompletableDeferred(true)
}
val app = host.app()
if (app != null) {
app.requestNotificationPermission(launchRequest)
} else {
launchRequest(true)
}
return result
val outcome = launchStart(createRequest(running = true))
return host.scope.async { outcome.await() == StartOutcome.STARTED }
}
fun requestStop(): Deferred<Boolean> {
val request = createRequest(running = false)
val result = CompletableDeferred<Boolean>()
host.scope.launch {
result.complete(
runCatching { stop(request) }
.onFailure { error ->
host.log("Unable to process service stop request: $error")
}
.getOrDefault(false),
)
}
return result
return host.scope.async { stop(request) }
}
fun syncSharedState(state: SharedState) {
@@ -163,21 +138,50 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
applySharedState()
}
private suspend fun loadPreferencesAndStart() {
// No new request for a settled run: one refused at a prompt would take the service down.
private fun isStartedAs(options: VpnOptions?): Boolean =
runState.value == RunState.STARTED && host.runtime?.vpn == options?.enable
// The intent is claimed before the Core is set up, so a second action that lands meanwhile
// toggles this start off instead of racing a second setup against it.
private suspend fun startFromPreferences() {
val request = createRequest(running = true)
sharedState = host.loadSharedState()
if (sharedState.setupParams == null || sharedState.vpnOptions == null) {
host.showToast(MISSING_CONFIG_MESSAGE)
abandon(request)
return
}
if (setupCore()) {
if (!requestStart().await()) {
host.showToast(START_FAILED_MESSAGE)
}
if (!setupCore()) {
abandon(request)
} else if (launchStart(request).await() == StartOutcome.FAILED) {
host.showToast(START_FAILED_MESSAGE)
}
}
private fun launchStart(request: RunRequest): Deferred<StartOutcome> {
val result = CompletableDeferred<StartOutcome>()
val launch: (Boolean) -> Unit = { permitted ->
host.scope.launch {
result.complete(if (permitted) start(request) else abandon(request))
}
}
val app = host.app()
if (app != null) {
app.requestNotificationPermission { permitted ->
if (permitted) {
app.requestLocalNetworkPermission { launch(true) }
} else {
launch(false)
}
}
} else {
launch(true)
}
return result
}
private fun applySharedState() {
host.setCrashlytics(sharedState.crashlytics)
host.updateNotificationParams(notificationParams(sharedState))
}
@@ -209,64 +213,72 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
host.showToast(message?.takeIf { it.isNotBlank() } ?: INVALID_CONFIG_MESSAGE)
}
private suspend fun start(request: RunRequest): Boolean = startPreparationLock.withLock {
val started = runStart(request)
if (!started) {
reconcileStopped()
private suspend fun start(request: RunRequest): StartOutcome {
val started = try {
startPreparationLock.withLock { runStart(request) }
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
host.log("Unable to process service start request: $error")
false
}
started
return if (started) StartOutcome.STARTED else abandon(request)
}
private suspend fun abandon(request: RunRequest): StartOutcome {
val dropped = arbiter.resetToStopped(request)
reconcileStopped()
if (!dropped) {
return StartOutcome.SUPERSEDED
}
host.notifyStopped()
return StartOutcome.FAILED
}
private suspend fun runStart(request: RunRequest): Boolean {
if (!isCurrent(request)) {
return false
}
val options = sharedState.vpnOptions
if (options == null) {
fail(request)
return false
}
val options = sharedState.vpnOptions ?: return false
if (!prepareVpn(options)) {
if (host.app() == null && isCurrent(request)) {
host.showToast(VPN_PERMISSION_MESSAGE)
}
fail(request)
return false
}
if (!isCurrent(request)) {
return false
}
return transitionLock.withLock transition@{
if (!isCurrent(request)) {
return@transition false
return transitionLock.withLock {
when {
!isCurrent(request) -> false
host.runtime?.vpn == options.enable -> true
else -> transitioning(RunState.STARTING) {
host.startService(withLocalNetworkFallback(options)) && isCurrent(request)
}
}
if (runTimeMillis != 0L && host.isVpnServiceActive() == options.enable) {
mutableRunState.value = RunState.STARTED
return@transition true
}
mutableRunState.value = RunState.STARTING
val startedAtMillis = host.startService(options)
if (startedAtMillis == 0L) {
mutableRunState.value = RunState.STOPPED
fail(request)
return@transition false
}
if (!isCurrent(request)) {
return@transition false
}
mutableRunState.value = RunState.STARTED
true
}
}
private suspend fun reconcileStopped() = transitionLock.withLock {
if (isRunningRequested() || runTimeMillis == 0L) {
return@withLock
// system/mixed hand TCP to the kernel via the tun subnet, which Android 17 gates as local network.
private fun withLocalNetworkFallback(options: VpnOptions): VpnOptions {
if (!options.enable || options.stack !in KERNEL_TCP_STACKS ||
host.isLocalNetworkPermissionGranted()
) {
return options
}
host.showToast(sharedState.localNetworkTip)
return options.copy(stack = FALLBACK_STACK)
}
// Flutter's startListener and the native quickSetup both open the Core's listeners before any
// service exists to answer for them, and Flutter may be gone before it hears of the failure.
private suspend fun reconcileStopped() = transitionLock.withLock {
when {
isRunRequested -> publish()
host.runtime != null -> stopLocked()
else -> {
stopService()
publish()
}
}
mutableRunState.value = RunState.STOPPING
host.stopService()
mutableRunState.value = RunState.STOPPED
}
private suspend fun stop(request: RunRequest): Boolean = transitionLock.withLock {
@@ -274,15 +286,44 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
return@withLock false
}
abandonVpnPreparation()
if (runState.value == RunState.STOPPED && runTimeMillis == 0L) {
return@withLock true
if (host.runtime != null) {
stopLocked()
}
mutableRunState.value = RunState.STOPPING
host.stopService()
mutableRunState.value = RunState.STOPPED
isCurrent(request)
}
private suspend fun stopLocked() = transitioning(RunState.STOPPING) { stopService() }
private suspend fun stopService() {
try {
host.stopService()
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
host.log("Unable to stop service: $error")
}
}
private inline fun <T> transitioning(state: RunState, block: () -> T): T {
transition = state
publish()
try {
return block()
} finally {
transition = null
publish()
}
}
@Synchronized
private fun publish() {
mutableRunState.value = transition ?: when {
host.runtime == null -> RunState.STOPPED
isRunRequested -> RunState.STARTED
else -> RunState.STOPPING
}
}
private suspend fun prepareVpn(options: VpnOptions): Boolean {
val app = host.app()
?: return !options.enable || host.isVpnPermissionGranted()
@@ -311,17 +352,15 @@ internal class ServiceStateMachine(private val host: ServiceStateHost) {
abandon()
}
private fun createRequest(running: Boolean): RunRequest = arbiter.request(running)
private fun isRunningRequested(): Boolean = arbiter.isRunningRequested
private fun createRequest(running: Boolean): RunRequest =
arbiter.request(running).also { publish() }
private fun isCurrent(request: RunRequest): Boolean = arbiter.isCurrent(request)
private fun fail(request: RunRequest) {
arbiter.resetToStopped(request)
}
internal companion object {
val KERNEL_TCP_STACKS = setOf("system", "mixed")
const val FALLBACK_STACK = "gvisor"
/**
* The Core init payload. The key spelling is a cross-language contract with the Go wrapper,
* not an implementation detail.
@@ -20,7 +20,6 @@ class TileService : android.service.quicksettings.TileService() {
scope?.cancel()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).also { scope ->
scope.launch {
ServiceState.refresh()
ServiceState.runState.collect(::updateTile)
}
}
@@ -6,7 +6,8 @@ import com.google.gson.annotations.SerializedName
data class SharedState(
val startTip: String = "Starting VPN...",
val stopTip: String = "Stopping VPN...",
val crashlytics: Boolean = true,
val localNetworkTip: String =
"Local network permission denied: using the gvisor stack, LAN is unreachable.",
val currentProfileName: String = "FlClash",
val stopText: String = "Stop",
val onlyStatisticsProxy: Boolean = false,
@@ -20,4 +21,6 @@ data class SetupParams(
val testUrl: String,
@SerializedName("selected-map")
val selectedMap: Map<String, String>,
@SerializedName("skip-cert-verify")
val skipCertVerify: Boolean = false,
)
@@ -1,7 +1,6 @@
package com.follow.clash.plugins
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.content.BroadcastReceiver
@@ -13,7 +12,6 @@ import android.net.VpnService
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@@ -22,6 +20,9 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.follow.clash.R
import com.follow.clash.common.Components
import com.follow.clash.common.GlobalState
@@ -29,6 +30,7 @@ import com.follow.clash.common.PendingCallback
import com.follow.clash.common.QuickAction
import com.follow.clash.common.quickIntent
import com.follow.clash.common.registerReceiverCompat
import com.follow.clash.common.shortcutId
import com.follow.clash.getPackageIconPath
import com.follow.clash.packages.PackageResolver
import com.follow.clash.showToast
@@ -68,8 +70,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private val requestInstalledAppsCallback = PendingCallback<Boolean>()
private val requestLocalNetworkCallback = PendingCallback<Boolean>()
private var isRequestingNotificationPermission = false
private var isRequestingLocalNetworkPermission = false
private var skipLocalNetworkPermissionRequest = false
private val gson = Gson()
private val packageResolver by lazy {
@@ -98,6 +106,22 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var skipNotificationPermissionRequest = false
@Suppress("DEPRECATION")
private val isTelevision by lazy {
val packageManager = GlobalState.application.packageManager
packageManager.hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
private var floatingIme = false
// A TV keyboard floats over the app and reports a zero-height IME inset.
private val imeInsetsListener = OnApplyWindowInsetsListener { view, insets ->
val ime = WindowInsetsCompat.Type.ime()
reportFloatingIme(insets.isVisible(ime) && insets.getInsets(ime).bottom == 0)
ViewCompat.onApplyWindowInsets(view, insets)
}
private val mainHandler = Handler(Looper.getMainLooper())
/**
@@ -118,8 +142,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
override fun onMethodCall(call: MethodCall, rawResult: Result) {
val result = MainThreadResult(rawResult)
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"moveTaskToBack" -> {
activity?.moveTaskToBack(true)
@@ -133,11 +156,15 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
"initShortcuts" -> {
val label = call.arguments as? String
if (label == null) {
result.error("INVALID_ARGUMENT", "Shortcut label must be a string", null)
val labels = shortcutLabels(call.arguments)
if (labels == null) {
result.error(
"INVALID_ARGUMENT",
"Shortcut labels must map every quick action to a string",
null,
)
} else {
initShortcuts(label)
initShortcuts(labels)
result.success(true)
}
}
@@ -168,22 +195,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(true)
}
"isBatteryOptimizationDisabled" -> {
result.success(isBatteryOptimizationDisabled())
}
"openBatteryOptimizationSettings" -> {
result.success(openBatteryOptimizationSettings())
}
"openAppSettings" -> {
result.success(openAppSettings())
}
"didCrashOnPreviousExecution" -> reply(result) {
GlobalState.didCrashOnPreviousExecution()
}
"getLastExitInfo" -> reply(result) {
GlobalState.lastExitInfo()
}
@@ -212,45 +227,35 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
private fun initShortcuts(label: String) {
val shortcut = with(ShortcutInfoCompat.Builder(GlobalState.application, "toggle")) {
setShortLabel(label)
setIcon(
IconCompat.createWithResource(
GlobalState.application,
R.mipmap.ic_launcher_round,
),
)
setIntent(QuickAction.TOGGLE.quickIntent)
build()
private fun shortcutLabels(arguments: Any?): Map<QuickAction, String>? {
val map = arguments as? Map<*, *> ?: return null
return QuickAction.entries.associateWith { action ->
map[action.shortcutId] as? String ?: return null
}
ShortcutManagerCompat.setDynamicShortcuts(
GlobalState.application,
listOf(shortcut),
)
}
private fun isBatteryOptimizationDisabled(): Boolean {
val powerManager = getSystemService(GlobalState.application, PowerManager::class.java)
return powerManager?.isIgnoringBatteryOptimizations(GlobalState.application.packageName)
?: false
}
@SuppressLint("BatteryLife")
private fun openBatteryOptimizationSettings(): Boolean {
// VPN continuity is the user-requested core function, so the direct exemption is intentional.
val activity = activity ?: return false
return try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = "package:${GlobalState.application.packageName}".toUri()
private fun initShortcuts(labels: Map<QuickAction, String>) {
val shortcuts = SHORTCUT_ORDER.mapIndexed { rank, action ->
with(ShortcutInfoCompat.Builder(GlobalState.application, action.shortcutId)) {
setShortLabel(labels.getValue(action))
setIcon(IconCompat.createWithResource(GlobalState.application, action.shortcutIcon))
setIntent(action.quickIntent)
setRank(rank)
build()
}
activity.startActivity(intent)
true
} catch (_: Exception) {
false
}
ShortcutManagerCompat.setDynamicShortcuts(GlobalState.application, shortcuts)
}
// Plain circles, not adaptive icons: an adaptive shortcut icon takes the
// launcher's own mask, which is a square on many devices.
private val QuickAction.shortcutIcon: Int
get() = when (this) {
QuickAction.START -> R.drawable.ic_shortcut_start
QuickAction.STOP -> R.drawable.ic_shortcut_stop
QuickAction.TOGGLE -> R.drawable.ic_shortcut_toggle
}
private fun openAppSettings(): Boolean {
val activity = activity ?: return false
return try {
@@ -266,12 +271,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
@Suppress("DEPRECATION")
private fun updateExcludeFromRecents(value: Boolean?) {
val taskId = activity?.taskId ?: return
val am = getSystemService(GlobalState.application, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId
it.taskInfo?.taskId == taskId
} else {
it.taskInfo.id == activity?.taskId
it.taskInfo?.id == taskId
}
}
task?.setExcludeFromRecents(value ?: false)
@@ -309,6 +315,42 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
requestNotificationCallback.resolve(shouldStart)
}
fun requestLocalNetworkPermission(callback: (Boolean) -> Unit) = onMainThread {
requestLocalNetworkCallback.replace(callback, supersededValue = false)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.CINNAMON_BUN || hasLocalNetworkPermission()) {
invokeRequestLocalNetworkCallback(true)
return@onMainThread
}
if (skipLocalNetworkPermissionRequest) {
invokeRequestLocalNetworkCallback(false)
return@onMainThread
}
if (isRequestingLocalNetworkPermission) {
return@onMainThread
}
val activity = activity
if (activity == null) {
invokeRequestLocalNetworkCallback(false)
return@onMainThread
}
isRequestingLocalNetworkPermission = true
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.ACCESS_LOCAL_NETWORK),
LOCAL_NETWORK_PERMISSION_REQUEST_CODE,
)
}
private fun hasLocalNetworkPermission(): Boolean = ContextCompat.checkSelfPermission(
GlobalState.application,
Manifest.permission.ACCESS_LOCAL_NETWORK,
) == PackageManager.PERMISSION_GRANTED
private fun invokeRequestLocalNetworkCallback(granted: Boolean) {
isRequestingLocalNetworkPermission = false
requestLocalNetworkCallback.resolve(granted)
}
private fun requestInstalledAppsPermission(callback: (Boolean) -> Unit) = onMainThread {
requestInstalledAppsCallback.replace(callback, supersededValue = false)
val activity = activity
@@ -390,6 +432,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
scope.cancel()
invokeVpnPrepareCallback(false)
invokeRequestNotificationCallback(false)
invokeRequestLocalNetworkCallback(false)
invokeRequestInstalledAppsCallback(false)
}
@@ -403,9 +446,21 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
activity = binding.activity
binding.addActivityResultListener(activityResultListener)
binding.addRequestPermissionsResultListener(permissionsResultListener)
if (isTelevision) {
ViewCompat.setOnApplyWindowInsetsListener(
binding.activity.window.decorView,
imeInsetsListener,
)
}
}
private fun detachFromActivity() {
if (isTelevision) {
activity?.window?.decorView?.let {
ViewCompat.setOnApplyWindowInsetsListener(it, null)
}
reportFloatingIme(false)
}
activity = null
val binding = activityBinding ?: return
activityBinding = null
@@ -413,6 +468,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
binding.removeRequestPermissionsResultListener(permissionsResultListener)
}
private fun reportFloatingIme(floating: Boolean) {
if (floating == floatingIme) {
return
}
floatingIme = floating
channel.invokeMethod("floatingImeChanged", floating)
}
override fun onDetachedFromActivityForConfigChanges() {
detachFromActivity()
}
@@ -426,6 +489,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
detachFromActivity()
invokeVpnPrepareCallback(false)
invokeRequestNotificationCallback(false)
invokeRequestLocalNetworkCallback(false)
invokeRequestInstalledAppsCallback(false)
}
@@ -456,12 +520,23 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
true
}
LOCAL_NETWORK_PERMISSION_REQUEST_CODE -> {
skipLocalNetworkPermissionRequest = true
invokeRequestLocalNetworkCallback(
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED,
)
true
}
else -> false
}
private companion object {
val SHORTCUT_ORDER = listOf(QuickAction.START, QuickAction.STOP, QuickAction.TOGGLE)
const val VPN_PERMISSION_REQUEST_CODE = 1001
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
const val INSTALLED_APPS_PERMISSION_REQUEST_CODE = 1003
const val LOCAL_NETWORK_PERMISSION_REQUEST_CODE = 1004
}
}
@@ -0,0 +1,241 @@
package com.follow.clash.plugins
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.ContentObserver
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import androidx.core.net.toUri
import com.follow.clash.common.Components
import com.follow.clash.common.PendingCallback
import com.follow.clash.common.registerReceiverCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
class BatteryPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
EventChannel.StreamHandler {
private lateinit var context: Context
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private var activityBinding: ActivityPluginBinding? = null
private val pendingRequest = PendingCallback<Unit>()
private var events: EventChannel.EventSink? = null
private var lastPublished: Boolean? = null
private val activityResultListener =
PluginRegistry.ActivityResultListener { requestCode, _, _ ->
if (requestCode != REQUEST_CODE) {
return@ActivityResultListener false
}
resolve()
true
}
private val allowlistReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
publish()
}
}
private val saverObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
publish()
}
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
methodChannel = MethodChannel(binding.binaryMessenger, "${Components.PACKAGE_NAME}/battery")
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(
binding.binaryMessenger,
"${Components.PACKAGE_NAME}/battery/optimization",
)
eventChannel.setStreamHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
onCancel(null)
resolve()
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"requestIgnoreOptimizations" -> requestIgnoreOptimizations { result.success(null) }
else -> result.notImplemented()
}
}
override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
val ignoring = isIgnoringOptimizations()
sink.success(ignoring)
context.registerReceiverCompat(
allowlistReceiver,
IntentFilter(ACTION_POWER_SAVE_WHITELIST_CHANGED),
)
context.contentResolver.registerContentObserver(
Settings.System.getUriFor(MILLET_NO_RESTRICT_APP),
false,
saverObserver,
)
events = sink
lastPublished = ignoring
}
override fun onCancel(arguments: Any?) {
events ?: return
events = null
context.unregisterReceiver(allowlistReceiver)
context.contentResolver.unregisterContentObserver(saverObserver)
}
private fun publish() {
val sink = events ?: return
val ignoring = isIgnoringOptimizations()
if (ignoring == lastPublished) {
return
}
lastPublished = ignoring
sink.success(ignoring)
}
private fun resolve() {
publish()
pendingRequest.resolve(Unit)
}
private fun requestIgnoreOptimizations(callback: (Unit) -> Unit) {
val activity = activityBinding?.activity
if (activity == null || isIgnoringOptimizations()) {
callback(Unit)
return
}
pendingRequest.replace(callback, supersededValue = Unit)
val pages = if (isXiaomi) saverPages() else optimizationPages()
if (pages.none { launch(activity, it) }) {
resolve()
}
}
@SuppressLint("BatteryLife")
private fun optimizationPages(): List<Intent> {
// VPN continuity is the user-requested core function, so the direct exemption is intentional.
val prompt = Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
"package:${context.packageName}".toUri(),
)
// Some ROMs drop the direct prompt; the allowlist page still lets the user exempt the app.
val allowlist = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
return listOf(prompt, allowlist)
}
private fun saverPages(): List<Intent> {
val policy = Intent()
.setComponent(ComponentName(POWERKEEPER_PACKAGE, POWERKEEPER_POLICY_ACTIVITY))
.putExtra("package_name", context.packageName)
.putExtra(
"package_label",
context.applicationInfo.loadLabel(context.packageManager).toString(),
)
val details = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
"package:${context.packageName}".toUri(),
)
return listOf(policy, details)
}
private fun launch(activity: Activity, page: Intent): Boolean = try {
@Suppress("DEPRECATION")
activity.startActivityForResult(page, REQUEST_CODE)
true
} catch (_: ActivityNotFoundException) {
false
} catch (_: SecurityException) {
false
}
// On Xiaomi the battery saver is what counts: "No restrictions" also allowlists the
// app, the allowlist alone does not stop PowerKeeper freezing it, and PowerKeeper
// drops a running VPN from the allowlist while the list follows the user's policy.
private fun isIgnoringOptimizations(): Boolean {
if (isXiaomi) {
return context.packageName in noRestrictionApps()
}
return context.getSystemService(PowerManager::class.java)
?.isIgnoringBatteryOptimizations(context.packageName) ?: false
}
// PowerKeeper's comma-separated "No restrictions" packages, kept for user 0 only.
private fun noRestrictionApps(): List<String> {
val packages = try {
Settings.System.getString(context.contentResolver, MILLET_NO_RESTRICT_APP)
} catch (_: SecurityException) {
null
}
return packages?.split(',')?.map { it.trim() }.orEmpty()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(activityResultListener)
}
override fun onDetachedFromActivityForConfigChanges() {
detachFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
detachFromActivity()
resolve()
}
private fun detachFromActivity() {
activityBinding?.removeActivityResultListener(activityResultListener)
activityBinding = null
}
private companion object {
val isXiaomi = Build.MANUFACTURER.equals("xiaomi", ignoreCase = true)
// Every plugin on the activity sees every result; AppPlugin holds 1001-1004.
const val REQUEST_CODE = 1005
// PowerManager keeps this action @hide, yet the system broadcasts it to
// registered receivers of the system user whenever the allowlist changes.
const val ACTION_POWER_SAVE_WHITELIST_CHANGED =
"android.os.action.POWER_SAVE_WHITELIST_CHANGED"
const val MILLET_NO_RESTRICT_APP = "MILLET_NO_RESTRICT_APP"
const val POWERKEEPER_PACKAGE = "com.miui.powerkeeper"
const val POWERKEEPER_POLICY_ACTIVITY = "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"
}
}
@@ -1,37 +0,0 @@
package com.follow.clash.plugins
import android.os.Handler
import android.os.Looper
import io.flutter.plugin.common.MethodChannel
/**
* Delivers a [MethodChannel.Result] on the main thread, which is where Flutter
* requires it, from handlers that finish on a background dispatcher.
*
* The hop goes through the main looper rather than the plugin's coroutine scope:
* that scope is cancelled when the engine detaches, and a reply dropped by a
* cancelled scope leaves the Dart future waiting forever. A reply posted after
* the engine is gone is the messenger's problem to ignore, and it does.
*/
internal class MainThreadResult(
private val delegate: MethodChannel.Result,
) : MethodChannel.Result {
override fun success(result: Any?) = post { delegate.success(result) }
override fun error(code: String, message: String?, details: Any?) =
post { delegate.error(code, message, details) }
override fun notImplemented() = post { delegate.notImplemented() }
private fun post(block: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
block()
} else {
mainHandler.post(block)
}
}
private companion object {
val mainHandler = Handler(Looper.getMainLooper())
}
}
@@ -6,8 +6,10 @@ import com.follow.clash.common.Components
import com.follow.clash.models.SharedState
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.StandardMethodCodec
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -15,35 +17,50 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var messenger: BinaryMessenger
private lateinit var channel: MethodChannel
private lateinit var coreChannel: MethodChannel
private lateinit var scope: CoroutineScope
private val gson = Gson()
private var commandRevision = 0
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
channel = MethodChannel(binding.binaryMessenger, "${Components.PACKAGE_NAME}/service")
messenger = binding.binaryMessenger
channel = MethodChannel(messenger, SERVICE_CHANNEL)
channel.setMethodCallHandler(this)
// Serial: watchRoute keeps the latest intent by the order calls reach the Core.
coreChannel = MethodChannel(
messenger,
CORE_CHANNEL,
StandardMethodCodec.INSTANCE,
messenger.makeBackgroundTaskQueue(),
)
coreChannel.setMethodCallHandler(::onCoreMethodCall)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
coreChannel.setMethodCallHandler(null)
scope.cancel()
ServiceController.setEventListener(null)
}
override fun onMethodCall(call: MethodCall, rawResult: MethodChannel.Result) {
// Most handlers below reply from a scope worker on Dispatchers.Default,
// but a MethodChannel.Result has to be answered on the platform thread.
// Wrapping once here covers every branch, including notImplemented.
val result = MainThreadResult(rawResult)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"init" -> initialize(result)
"shutdown" -> shutdown(result)
"invokeMethod" -> invokeMethod(call, result)
"getRunTime" -> getRunTime(result)
"syncState" -> syncState(call, result)
"start" -> start(result)
"stop" -> stop(result)
"start" -> start(call, result)
"stop" -> stop(call, result)
else -> result.notImplemented()
}
}
private fun onCoreMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"invokeMethod" -> invokeMethod(call, result)
else -> result.notImplemented()
}
}
@@ -55,10 +72,8 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun shutdown(result: MethodChannel.Result) {
scope.launch {
ServiceController.unbind()
result.success(true)
}
ServiceController.setEventListener(null)
result.success(true)
}
private fun invokeMethod(call: MethodCall, result: MethodChannel.Result) {
@@ -67,18 +82,16 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.error("INVALID_ARGUMENT", "Method call payload must be a string", null)
return
}
scope.launch {
ServiceController.invokeMethod(data) { response ->
result.success(response)
}.onFailure { error ->
result.error("CORE_ERROR", error.message, null)
}
ServiceController.invokeMethod(data) { response ->
result.success(response)
}.onFailure { error ->
result.error("CORE_ERROR", error.message, null)
}
}
private fun getRunTime(result: MethodChannel.Result) {
scope.launch {
result.success(ServiceState.refresh())
result.success(ServiceState.awaitRunTime())
}
}
@@ -97,19 +110,36 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}
private fun start(result: MethodChannel.Result) {
private fun start(call: MethodCall, result: MethodChannel.Result) {
commandRevision = call.arguments as? Int ?: commandRevision
ServiceState.requestStart()
result.success(true)
}
private fun stop(result: MethodChannel.Result) {
private fun stop(call: MethodCall, result: MethodChannel.Result) {
commandRevision = call.arguments as? Int ?: commandRevision
ServiceState.requestStop()
result.success(true)
}
private fun sendEvent(value: String?) {
// Read on the platform thread, where start and stop arrive: revision and intent are one instant.
fun notifyStopped() {
scope.launch(Dispatchers.Main) {
channel.invokeMethod("event", value)
if (!ServiceState.isRunRequested) {
channel.invokeMethod("stopped", commandRevision)
}
}
}
private fun sendEvent(value: ByteArray) {
val message = StandardMethodCodec.INSTANCE.encodeMethodCall(MethodCall("event", value))
scope.launch(Dispatchers.Main) {
messenger.send(SERVICE_CHANNEL, message)
}
}
private companion object {
val SERVICE_CHANNEL = "${Components.PACKAGE_NAME}/service"
val CORE_CHANNEL = "${Components.PACKAGE_NAME}/core"
}
}
@@ -23,10 +23,6 @@ class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
channel.invokeMethodOnMainThread("start")
}
fun handleStop() {
channel.invokeMethodOnMainThread("stop")
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
result.notImplemented()
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@color/shortcut_background"
android:pathData="M24,2 A22,22 0 1 1 24,46 A22,22 0 1 1 24,2 Z" />
<group
android:scaleX="1.35"
android:scaleY="1.35"
android:translateX="7.8"
android:translateY="7.8">
<path
android:fillColor="#FFFFFF"
android:pathData="M8.30,7.74 A1.2,1.2 0 0 1 10.12,6.71 L17.12,10.98 A1.2,1.2 0 0 1 17.12,13.02 L10.12,17.29 A1.2,1.2 0 0 1 8.30,16.26 Z"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="1.8" />
</group>
</vector>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@color/shortcut_background"
android:pathData="M24,2 A22,22 0 1 1 24,46 A22,22 0 1 1 24,2 Z" />
<group
android:scaleX="1.35"
android:scaleY="1.35"
android:translateX="7.8"
android:translateY="7.8">
<path
android:fillColor="#FFFFFF"
android:pathData="M14.90,7.00 A1.1,1.1 0 0 1 16.00,5.90 L16.00,5.90 A1.1,1.1 0 0 1 17.10,7.00 L17.10,17.00 A1.1,1.1 0 0 1 16.00,18.10 L16.00,18.10 A1.1,1.1 0 0 1 14.90,17.00 Z M6.90,7.00 A1.1,1.1 0 0 1 8.00,5.90 L8.00,5.90 A1.1,1.1 0 0 1 9.10,7.00 L9.10,17.00 A1.1,1.1 0 0 1 8.00,18.10 L8.00,18.10 A1.1,1.1 0 0 1 6.90,17.00 Z"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="1.8" />
</group>
</vector>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@color/shortcut_background"
android:pathData="M24,2 A22,22 0 1 1 24,46 A22,22 0 1 1 24,2 Z" />
<group
android:scaleX="1.35"
android:scaleY="1.35"
android:translateX="7.8"
android:translateY="7.8">
<path
android:fillColor="#FFFFFF"
android:pathData="M2.90,12.00 A5.5,5.5 0 0 1 8.40,6.50 L15.60,6.50 A5.5,5.5 0 0 1 21.10,12.00 L21.10,12.00 A5.5,5.5 0 0 1 15.60,17.50 L8.40,17.50 A5.5,5.5 0 0 1 2.90,12.00 Z"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="1.8" />
<path
android:fillColor="@color/shortcut_background"
android:pathData="M8.4,9.2 A2.8,2.8 0 1 1 8.4,14.8 A2.8,2.8 0 1 1 8.4,9.2 Z" />
</group>
</vector>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="shortcut_background">#6666FB</color>
</resources>
-3
View File
@@ -32,8 +32,5 @@ kotlin {
dependencies {
implementation(libs.androidx.core)
implementation(libs.gson)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics.ndk)
implementation(libs.firebase.analytics)
testImplementation(libs.junit)
}
@@ -9,6 +9,7 @@ import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Context.RECEIVER_EXPORTED
import android.content.Context.RECEIVER_NOT_EXPORTED
import android.content.Intent
import android.content.IntentFilter
@@ -30,6 +31,9 @@ val ComponentName.intent: Intent
val QuickAction.action: String
get() = "${GlobalState.application.packageName}.action.${this.name}"
val QuickAction.shortcutId: String
get() = name.lowercase()
val QuickAction.quickIntent: Intent
get() = Components.quickActionActivity.intent.apply {
action = this@quickIntent.action
@@ -44,7 +48,6 @@ fun BroadcastAction.sendBroadcast() {
val intent = Intent(broadcastAction).apply {
component = Components.serviceBroadcastReceiver
}
GlobalState.log("Send broadcast: $broadcastAction")
GlobalState.application.sendBroadcast(
intent,
GlobalState.receiveBroadcastPermission,
@@ -59,19 +62,24 @@ val Intent.toPendingIntent: PendingIntent
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
fun Service.startForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
if (channel == null) {
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL,
getString(R.string.service_channel_name),
NotificationManager.IMPORTANCE_LOW,
)
manager?.createNotificationChannel(channel)
}
fun Context.ensureNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val manager = getSystemService(NotificationManager::class.java) ?: return
if (manager.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL) != null) {
return
}
manager.createNotificationChannel(
NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL,
getString(R.string.service_channel_name),
NotificationManager.IMPORTANCE_LOW,
),
)
}
fun Service.startForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
GlobalState.NOTIFICATION_ID,
@@ -87,13 +95,19 @@ fun Service.startForeground(notification: Notification) {
fun Context.registerReceiverCompat(
receiver: BroadcastReceiver,
filter: IntentFilter,
exported: Boolean = false,
) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
registerReceiver(
receiver,
filter,
if (exported) RECEIVER_EXPORTED else RECEIVER_NOT_EXPORTED,
)
} else {
registerReceiver(receiver, filter)
}
fun Context.receiveBroadcastFlow(
exported: Boolean = false,
configure: IntentFilter.() -> Unit,
): Flow<Intent> = callbackFlow {
val filter = IntentFilter().apply(configure)
@@ -103,6 +117,6 @@ fun Context.receiveBroadcastFlow(
trySend(intent)
}
}
registerReceiverCompat(receiver, filter)
registerReceiverCompat(receiver, filter, exported)
awaitClose { unregisterReceiver(receiver) }
}
@@ -4,8 +4,6 @@ import android.app.ActivityManager
import android.app.Application
import android.os.Build
import android.util.Log
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -36,19 +34,6 @@ object GlobalState : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatch
Log.d("FlClash", text)
}
fun setCrashlytics(enable: Boolean) {
FirebaseApp.initializeApp(application)
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = enable
if (enable) {
log("Crashlytics enabled")
}
}
fun didCrashOnPreviousExecution(): Boolean {
FirebaseApp.initializeApp(application)
return FirebaseCrashlytics.getInstance().didCrashOnPreviousExecution()
}
fun lastExitInfo(): Map<String, Any?>? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
val manager = application.getSystemService(ActivityManager::class.java) ?: return null
+241 -53
View File
@@ -1,18 +1,15 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.ByteArrayOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import javax.inject.Inject
plugins {
id("com.android.library")
}
// Must equal Target.forPlatform("android") in setup_hooks, which writes libclash.so only for the platforms being built.
val coreAbiByPlatform =
linkedMapOf("android-arm" to "armeabi-v7a", "android-arm64" to "arm64-v8a", "android-x64" to "x86_64")
val coreAbis =
(rootProject.findProperty("target-platform") as String?)
?.split(",")
?.map { coreAbiByPlatform[it.trim()] ?: throw GradleException("No Core for Flutter target platform $it") }
?: coreAbiByPlatform.values.toList()
android {
namespace = "com.follow.clash.core"
compileSdk = libs.versions.compileSdk.get().toInt()
@@ -20,24 +17,13 @@ android {
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
ndk {
abiFilters += coreAbis
}
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
kotlin {
@@ -46,42 +32,244 @@ kotlin {
}
}
dependencies {
implementation(libs.annotation.jvm)
}
val gomobileTargetByPlatform =
linkedMapOf("android-arm" to "android/arm", "android-arm64" to "android/arm64", "android-x64" to "android/amd64")
val gomobileTargets =
(rootProject.findProperty("target-platform") as String?)
?.split(",")
?.map { gomobileTargetByPlatform[it.trim()] ?: throw GradleException("No Core for Flutter target platform $it") }
?: gomobileTargetByPlatform.values.toList()
// Why prefix matching and mustRunAfter: .agents/architecture.md, "Android Native Task Ordering".
val nativeTaskPattern =
Regex("^(configureCMake|buildCMake|externalNativeBuild|merge.*(NativeLibs|JniLibFolders)|copy.*JniLibs)")
val flutterCompileTasks =
rootProject.project(":app").tasks.matching { it.name.startsWith("compileFlutterBuild") }
val isWindows = System.getProperty("os.name").startsWith("Windows")
tasks.matching { nativeTaskPattern.containsMatchIn(it.name) }.configureEach {
mustRunAfter(flutterCompileTasks)
}
// Gradle started from Android Studio sees the PATH of the GUI session, which leaves out Homebrew and the Go installer.
val goExecutablePath =
providers.provider {
val name = if (isWindows) "go.exe" else "go"
val home = System.getProperty("user.home")
val searchPath =
System.getenv("PATH").orEmpty().split(File.pathSeparator) +
listOf("/opt/homebrew/bin", "/usr/local/bin", "/usr/local/go/bin", "$home/go/bin")
searchPath
.filter { it.isNotEmpty() }
.map { File(it, name) }
.firstOrNull { it.canExecute() }
?.path
?: throw GradleException("The Android Core is built with gomobile, which needs Go on PATH")
}
gradle.taskGraph.whenReady {
val nativeTask =
allTasks.firstOrNull {
it.path.startsWith("${project.path}:") && nativeTaskPattern.containsMatchIn(it.name)
val goModule = rootProject.layout.projectDirectory.dir("../core")
// gomobile names the library gojni, both as the file it builds and in go.Seq's System.loadLibrary, and has no option to change it.
val coreLibraryName = "clash"
val buildConfig =
providers.fileContents(rootProject.layout.projectDirectory.file("../build_config.yaml")).asText.map { text ->
text.lines()
.mapNotNull { Regex("""^(\w+):\s*(.*?)\s*$""").find(it)?.destructured }
.associate { (key, value) -> key to value.trim('"', '\'') }
}
abstract class GomobileBind : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sources: ConfigurableFileCollection
@get:Internal
abstract val moduleDirectory: DirectoryProperty
@get:Internal
abstract val goExecutable: Property<String>
@get:Input
abstract val goVersion: Property<String>
@get:Input
abstract val targets: ListProperty<String>
@get:Input
abstract val androidApi: Property<Int>
@get:Input
abstract val tags: Property<String>
@get:Input
abstract val ldflags: Property<String>
@get:Internal
abstract val sdkDirectory: DirectoryProperty
@get:Internal
abstract val ndkDirectory: DirectoryProperty
@get:Input
abstract val ndkVersion: Property<String>
@get:Input
abstract val libraryName: Property<String>
@get:OutputFile
abstract val aar: RegularFileProperty
@get:OutputFile
abstract val classesJar: RegularFileProperty
@TaskAction
fun bind() {
val go = goExecutable.get()
val tools = temporaryDir.resolve("bin")
val executableSuffix = if (System.getProperty("os.name").startsWith("Windows")) ".exe" else ""
tools.mkdirs()
val path =
listOf(tools, File(System.getProperty("java.home"), "bin"), File(go).parentFile)
.joinToString(File.pathSeparator) { it.path } + File.pathSeparator + System.getenv("PATH").orEmpty()
execOperations.exec {
workingDir(moduleDirectory)
environment("PATH", path)
commandLine(go, "build", "-o", tools.path + File.separator, "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind")
}
// gomobile links the library as the main package of a module it generates, whose go line is the toolchain's,
// so the GODEBUG defaults that the go line of core/go.mod sets for the desktop Core would not reach it on their own.
val godebug = ByteArrayOutputStream()
execOperations.exec {
workingDir(moduleDirectory)
environment("PATH", path)
commandLine(go, "list", "-f", "{{.DefaultGODEBUG}}", "./cmd/core")
standardOutput = godebug
}
execOperations.exec {
workingDir(moduleDirectory)
environment("PATH", path)
environment("ANDROID_HOME", sdkDirectory.get().asFile.path)
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.path)
commandLine(
tools.resolve("gomobile$executableSuffix").path,
"bind",
"-target=${targets.get().joinToString(",")}",
"-androidapi=${androidApi.get()}",
"-javapkg=com.follow.clash.core",
"-tags=${tags.get()}",
"-ldflags=${ldflags.get()} -X=runtime.godebugDefault=${godebug.toString().trim()}",
"-o",
aar.get().asFile.path,
"./mobile",
)
}
ZipFile(aar.get().asFile).use { aarFile ->
ZipInputStream(aarFile.getInputStream(aarFile.getEntry("classes.jar"))).use { jar ->
ZipOutputStream(classesJar.get().asFile.outputStream()).use { out ->
var seqPatched = false
generateSequence { jar.nextEntry }.forEach { entry ->
var bytes = jar.readBytes()
if (entry.name == "go/Seq.class") {
bytes = bytes.replaceUtf8Constant("gojni", libraryName.get())
seqPatched = true
}
out.putNextEntry(ZipEntry(entry.name).apply { time = entry.time })
out.write(bytes)
}
if (!seqPatched) throw GradleException("gomobile's classes.jar has no go/Seq.class to load lib${libraryName.get()}.so")
}
}
}
if (nativeTask == null || allTasks.any { it.path.startsWith(":app:compileFlutterBuild") }) {
return@whenReady
}
val missing =
coreAbis
.flatMap {
listOf(file("src/main/jniLibs/$it/libclash.so"), file("src/main/cpp/includes/$it/libclash.h"))
}.filterNot { it.isFile }
if (missing.isNotEmpty()) {
throw GradleException(
"${nativeTask.name} consumes Core artifacts that the setup build hook writes into src/main, " +
"and these are absent: ${missing.joinToString { it.relativeTo(projectDir).path }}. " +
"Run `dart setup.dart android` once, or build through :app so the hook runs first.",
)
private fun ByteArray.replaceUtf8Constant(old: String, new: String): ByteArray {
fun constant(value: String) = value.toByteArray().let { byteArrayOf(1, (it.size shr 8).toByte(), it.size.toByte()) + it }
val target = constant(old)
val matches = (0..size - target.size).filter { start -> target.indices.all { this[start + it] == target[it] } }
if (matches.size != 1) throw GradleException("Expected one \"$old\" constant in go/Seq.class, found ${matches.size}")
val start = matches.single()
return copyOfRange(0, start) + constant(new) + copyOfRange(start + target.size, size)
}
logger.warn(
":core: ${nativeTask.name} is running without an :app Flutter compile task, " +
"so the Core artifacts under src/main are whatever the last hook run left behind",
)
}
abstract class UnpackJniLibs : DefaultTask() {
@get:Inject
abstract val archiveOperations: ArchiveOperations
@get:Inject
abstract val fileSystemOperations: FileSystemOperations
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val aar: RegularFileProperty
@get:Input
abstract val libraryName: Property<String>
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun unpack() {
var renamed = 0
fileSystemOperations.sync {
from(archiveOperations.zipTree(aar)) {
include("jni/**")
eachFile {
path = path.removePrefix("jni/")
if (name == "libgojni.so") {
name = "lib${libraryName.get()}.so"
renamed++
}
}
includeEmptyDirs = false
}
into(outputDirectory)
}
if (renamed == 0) throw GradleException("gomobile's AAR has no libgojni.so to rename")
}
}
val bindCore =
tasks.register<GomobileBind>("bindCore") {
sources.from(
fileTree(goModule) {
exclude("**/*_test.go", "**/testdata/**", "cmd/**", "Clash.Meta/.github/**")
},
)
moduleDirectory.set(goModule)
goExecutable.set(goExecutablePath)
goVersion.set(
goExecutablePath
.flatMap { go -> providers.exec { commandLine(go, "env", "GOVERSION") }.standardOutput.asText }
.map { it.trim() },
)
targets.set(gomobileTargets)
androidApi.set(libs.versions.minSdk.get().toInt())
tags.set(buildConfig.map { it["tags"].orEmpty() })
ldflags.set(
buildConfig.map { config ->
val flags = config["go_ldflags"].orEmpty().split(" ").filter { it.isNotEmpty() }
val version = config["core_version"].orEmpty()
(flags + listOfNotNull(version.takeIf { it.isNotEmpty() }?.let { "-X=github.com/metacubex/mihomo/constant.Version=$it" }))
.joinToString(" ")
},
)
sdkDirectory.set(androidComponents.sdkComponents.sdkDirectory)
ndkDirectory.set(androidComponents.sdkComponents.ndkDirectory)
ndkVersion.set(libs.versions.ndkVersion)
libraryName.set(coreLibraryName)
aar.set(layout.buildDirectory.file("gomobile/core.aar"))
classesJar.set(layout.buildDirectory.file("gomobile/classes.jar"))
}
// A library cannot bundle a local AAR into its own, so the gomobile AAR reaches AGP as a classes jar and generated jniLibs.
androidComponents {
onVariants { variant ->
val unpack =
tasks.register<UnpackJniLibs>("unpack${variant.name.replaceFirstChar { it.uppercase() }}CoreJniLibs") {
aar.set(bindCore.flatMap { it.aar })
libraryName.set(coreLibraryName)
}
variant.sources.jniLibs?.addGeneratedSourceDirectory(unpack, UnpackJniLibs::outputDirectory)
}
}
dependencies {
implementation(files(bindCore.flatMap { it.classesJar }))
}
+2
View File
@@ -0,0 +1,2 @@
-keep class go.** { *; }
-keep class com.follow.clash.core.mobile.** { *; }
-28
View File
@@ -1,28 +0,0 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
add_compile_options(-O3 -flto -g0 -fno-exceptions -fno-rtti)
add_link_options(-flto -Wl,--gc-sections,--strip-all)
endif ()
set(LIB_CLASH_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/../cpp/includes/${ANDROID_ABI}")
# Never make linking libclash conditional on the file existing: AGP's configure
# fingerprint tracks only CMakeLists.txt, so a configure that ran before the build
# hook would stay cached and silently drop the Core. Failing at compile or link
# time is what keeps that decision out of the configure step.
include_directories(${LIB_CLASH_INCLUDE_DIR})
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
clash)
-204
View File
@@ -1,204 +0,0 @@
#include <jni.h>
#include <cstring>
#include "jni_helper.h"
#include "libclash.h"
#include "bride.h"
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
jstring stack, jstring address, jstring dns) {
const auto interface = new_global(cb);
return startTUN(interface, fd, get_string(stack), get_string(address), get_string(dns))
? JNI_TRUE
: JNI_FALSE;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
stopTun();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
forceGC();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
updateDns(get_string(dns));
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_invokeMethod(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
const auto interface = new_global(cb);
invokeMethod(interface, get_string(data));
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_setEventListener(JNIEnv *env, jobject thiz, jobject cb) {
if (cb != nullptr) {
const auto interface = new_global(cb);
setEventListener(interface);
} else {
setEventListener(nullptr);
}
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
scoped_string traffic = getTraffic(only_statistics_proxy);
return new_string(traffic);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
scoped_string traffic = getTotalTraffic(only_statistics_proxy);
return new_string(traffic);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
suspend(suspended);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_quickSetup(JNIEnv *env, jobject thiz, jstring init_params_string,
jstring setup_params_string, jobject cb) {
const auto interface = new_global(cb);
quickSetup(interface, get_string(init_params_string), get_string(setup_params_string));
}
static jmethodID m_tun_interface_protect;
static jmethodID m_tun_interface_resolve_uid;
static jmethodID m_tun_interface_resolve_package;
static jmethodID m_invoke_interface_result;
static void release_jni_object_impl(void *obj) {
ATTACH_JNI();
del_global(static_cast<jobject>(obj));
}
static void free_string_impl(char *str) {
free(str);
}
static int call_tun_interface_protect_impl(void *tun_interface, const int fd) {
// ART aborts the process on a call through a null object, so a callback
// that was already released has to stop here rather than at the JNI call.
if (tun_interface == nullptr) {
return 0;
}
ATTACH_JNI();
const auto accepted = env->CallBooleanMethod(static_cast<jobject>(tun_interface),
m_tun_interface_protect,
fd);
if (jni_clear_exception(env)) {
return 0;
}
return accepted == JNI_TRUE ? 1 : 0;
}
static int call_tun_interface_resolve_uid_impl(void *tun_interface, const int protocol,
const char *source,
const char *target) {
if (tun_interface == nullptr) {
return -1;
}
ATTACH_JNI();
const auto source_string = new_string(source);
const auto target_string = new_string(target);
const auto uid = env->CallIntMethod(static_cast<jobject>(tun_interface),
m_tun_interface_resolve_uid,
protocol,
source_string,
target_string);
const auto failed = jni_clear_exception(env);
if (source_string != nullptr) {
env->DeleteLocalRef(source_string);
}
if (target_string != nullptr) {
env->DeleteLocalRef(target_string);
}
return failed ? -1 : uid;
}
static char *call_tun_interface_resolve_package_impl(void *tun_interface, const int uid) {
if (tun_interface == nullptr) {
return strdup("");
}
ATTACH_JNI();
const auto package_name = reinterpret_cast<jstring>(env->CallObjectMethod(
static_cast<jobject>(tun_interface),
m_tun_interface_resolve_package,
uid));
jni_clear_exception(env);
const auto result = get_string(package_name);
if (package_name != nullptr) {
env->DeleteLocalRef(package_name);
}
return result;
}
static void call_invoke_interface_result_impl(void *invoke_interface, const char *data) {
if (invoke_interface == nullptr) {
return;
}
ATTACH_JNI();
const auto value = new_string(data);
env->CallVoidMethod(static_cast<jobject>(invoke_interface),
m_invoke_interface_result,
value);
jni_clear_exception(env);
if (value != nullptr) {
env->DeleteLocalRef(value);
}
}
extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env = nullptr;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
initialize_jni(vm, env);
const auto c_tun_interface = find_class("com/follow/clash/core/TunInterface");
const auto c_invoke_interface = find_class("com/follow/clash/core/InvokeInterface");
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)Z");
m_tun_interface_resolve_uid = find_method(c_tun_interface, "resolveUid",
"(ILjava/lang/String;Ljava/lang/String;)I");
m_tun_interface_resolve_package = find_method(c_tun_interface, "resolvePackage",
"(I)Ljava/lang/String;");
m_invoke_interface_result = find_method(c_invoke_interface, "onResult",
"(Ljava/lang/String;)V");
protect_func = &call_tun_interface_protect_impl;
resolve_uid_func = &call_tun_interface_resolve_uid_impl;
resolve_package_func = &call_tun_interface_resolve_package_impl;
result_func = &call_invoke_interface_result_impl;
release_object_func = &release_jni_object_impl;
free_string_func = &free_string_impl;
return JNI_VERSION_1_6;
}
-119
View File
@@ -1,119 +0,0 @@
#include "jni_helper.h"
#include <cstdlib>
#include <cstring>
#include <pthread.h>
static JavaVM *global_vm;
static jclass c_string;
static jmethodID m_new_string;
static jmethodID m_get_bytes;
static pthread_key_t detach_key;
static bool detach_key_ready;
static void detach_current_thread(void *) {
global_vm->DetachCurrentThread();
}
void initialize_jni(JavaVM *vm, JNIEnv *env) {
global_vm = vm;
detach_key_ready = pthread_key_create(&detach_key, detach_current_thread) == 0;
c_string = reinterpret_cast<jclass>(new_global(find_class("java/lang/String")));
m_new_string = find_method(c_string, "<init>", "([B)V");
m_get_bytes = find_method(c_string, "getBytes", "()[B");
}
bool jni_clear_exception(JNIEnv *env) {
if (env->ExceptionCheck() == JNI_FALSE) {
return false;
}
env->ExceptionDescribe();
env->ExceptionClear();
return true;
}
static char *empty_string() {
return static_cast<char *>(calloc(1, 1));
}
char *jni_get_string(JNIEnv *env, jstring str) {
if (str == nullptr) {
return empty_string();
}
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
if (jni_clear_exception(env) || array == nullptr) {
return empty_string();
}
const int length = env->GetArrayLength(array);
const auto content = static_cast<char *>(malloc(length + 1));
if (content == nullptr) {
env->DeleteLocalRef(array);
return empty_string();
}
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
if (jni_clear_exception(env)) {
// The copy did not happen, so `content` still holds whatever malloc
// handed back. Returning it would pass that heap content on as the
// string the caller asked for.
free(content);
env->DeleteLocalRef(array);
return empty_string();
}
env->DeleteLocalRef(array);
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
if (str == nullptr) {
str = "";
}
const auto length = static_cast<int>(strlen(str));
const auto array = env->NewByteArray(length);
if (jni_clear_exception(env) || array == nullptr) {
return env->NewStringUTF("");
}
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
if (jni_clear_exception(env)) {
// Calling NewObject with an exception still pending is undefined, and
// the array it would read from was not filled in anyway.
env->DeleteLocalRef(array);
return env->NewStringUTF("");
}
const auto result = reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
jni_clear_exception(env);
env->DeleteLocalRef(array);
if (result == nullptr) {
return env->NewStringUTF("");
}
return result;
}
void jni_attach_thread(scoped_jni *jni) {
if (global_vm->GetEnv(reinterpret_cast<void **>(&jni->env), JNI_VERSION_1_6) == JNI_OK) {
jni->require_release = 0;
return;
}
if (global_vm->AttachCurrentThreadAsDaemon(&jni->env, nullptr) != JNI_OK) {
abort();
}
if (detach_key_ready && pthread_setspecific(detach_key, jni->env) == 0) {
jni->require_release = 0;
return;
}
jni->require_release = 1;
}
void jni_detach_thread(const scoped_jni *env) {
if (env->require_release) {
global_vm->DetachCurrentThread();
}
}
void release_string(char **value) {
free(*value);
}
-36
View File
@@ -1,36 +0,0 @@
#pragma once
#include <jni.h>
struct scoped_jni {
JNIEnv *env;
int require_release;
};
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
extern jstring jni_new_string(JNIEnv *env, const char *str);
extern char *jni_get_string(JNIEnv *env, jstring str);
extern void jni_attach_thread(scoped_jni *jni);
extern void jni_detach_thread(const scoped_jni *env);
extern void release_string(char **value);
extern bool jni_clear_exception(JNIEnv *env);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
scoped_jni _jni{}; \
jni_attach_thread(&_jni); \
JNIEnv *env = _jni.env
#define scoped_string __attribute__((cleanup(release_string))) char*
#define find_class(name) env->FindClass(name)
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
#define new_global(obj) env->NewGlobalRef(obj)
#define del_global(obj) env->DeleteGlobalRef(obj)
#define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr)
@@ -1,23 +1,20 @@
package com.follow.clash.core
import com.follow.clash.core.mobile.Mobile
import com.follow.clash.core.mobile.ResultHandler
import com.follow.clash.core.mobile.TunHandler
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.URI
object Core {
private external fun startTun(
fd: Int,
cb: TunInterface,
stack: String,
address: String,
dns: String,
): Boolean
fun forceGC() = Mobile.forceGC()
external fun forceGC()
external fun updateDNS(
fun updateDNS(
dns: String,
)
) = Mobile.updateDns(dns)
fun resetNetwork() = Mobile.resetNetwork()
private fun parseInetSocketAddress(address: String): InetSocketAddress {
val uri = URI("tcp://$address")
@@ -34,10 +31,11 @@ object Core {
stack: String,
address: String,
dns: String,
options: String,
): Boolean {
return startTun(
return Mobile.startTun(
fd,
object : TunInterface {
object : TunHandler {
override fun protect(fd: Int): Boolean = protect(fd)
override fun resolveUid(
@@ -57,48 +55,23 @@ object Core {
stack,
address,
dns,
options,
)
}
external fun suspended(
suspended: Boolean,
)
private external fun invokeMethod(
data: String,
cb: InvokeInterface,
)
fun invokeMethod(
data: String,
cb: (result: String?) -> Unit,
cb: (result: ByteArray?) -> Unit,
) {
invokeMethod(
data,
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
Mobile.invokeMethod(data, ResultHandler { result -> cb(result) })
}
private external fun setEventListener(cb: InvokeInterface?)
fun updateEventListener(
callback: ((result: String?) -> Unit)?,
callback: ((result: ByteArray) -> Unit)?,
) {
if (callback == null) {
setEventListener(null)
} else {
setEventListener(
object : InvokeInterface {
override fun onResult(result: String?) {
callback(result)
}
},
)
}
Mobile.setEventListener(
callback?.let { ResultHandler { result -> it(result ?: ByteArray(0)) } },
)
}
fun quickSetup(
@@ -106,30 +79,20 @@ object Core {
setupParamsString: String,
callback: (result: String?) -> Unit,
) {
quickSetup(
Mobile.quickSetup(
initParamsString,
setupParamsString,
object : InvokeInterface {
override fun onResult(result: String?) {
callback(result)
}
},
ResultHandler { result -> callback(result?.decodeToString()) },
)
}
private external fun quickSetup(
initParamsString: String,
setupParamsString: String,
cb: InvokeInterface,
)
fun stopTun() = Mobile.stopTun()
external fun stopTun()
fun startListener() = Mobile.startListener()
external fun getTraffic(onlyStatisticsProxy: Boolean): String
fun stopListener() = Mobile.stopListener()
external fun getTotalTraffic(onlyStatisticsProxy: Boolean): String
fun getTraffic(onlyStatisticsProxy: Boolean): String = Mobile.getTraffic(onlyStatisticsProxy)
init {
System.loadLibrary("core")
}
fun getTotalTraffic(onlyStatisticsProxy: Boolean): String = Mobile.getTotalTraffic(onlyStatisticsProxy)
}
@@ -1,8 +0,0 @@
package com.follow.clash.core
import androidx.annotation.Keep
@Keep
interface InvokeInterface {
fun onResult(result: String?)
}
@@ -1,14 +0,0 @@
package com.follow.clash.core
import androidx.annotation.Keep
@Keep
interface TunInterface {
/** Returns false when the socket could not be kept out of the tunnel. */
fun protect(fd: Int): Boolean
/** Returns the uid owning the connection, or -1 when the system will not name it. */
fun resolveUid(protocol: Int, source: String, target: String): Int
fun resolvePackage(uid: Int): String
}
+8 -14
View File
@@ -1,24 +1,18 @@
[versions]
firebaseBom = "34.15.0"
minSdk = "23"
targetSdk = "36"
compileSdk = "36"
minSdk = "24"
targetSdk = "37"
compileSdk = "37"
ndkVersion = "28.2.13676358"
coreKtx = "1.17.0"
annotationJvm = "1.9.1"
coreSplashscreen = "1.0.1"
gson = "2.13.1"
smaliDexlib2 = "3.0.9"
coreKtx = "1.19.1"
coreSplashscreen = "1.2.0"
gson = "2.14.0"
smaliDexlib2 = "3.0.10"
junit = "4.13.2"
kotlinxCoroutines = "1.10.2"
kotlinxCoroutines = "1.11.0"
[libraries]
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" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-crashlytics-ndk = { module = "com.google.firebase:firebase-crashlytics-ndk" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
+1 -1
View File
@@ -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-9.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-all.zip
@@ -51,10 +51,25 @@ class FilesProvider : DocumentsProvider() {
mode: String,
signal: CancellationSignal?,
): ParcelFileDescriptor {
val accessMode = ParcelFileDescriptor.parseMode(mode)
val accessMode = ParcelFileDescriptor.parseMode(canonicalMode(mode))
return ParcelFileDescriptor.open(resolveFile(documentId), accessMode)
}
// parseMode throws on a letter outside r, w, t and a, and before Android 10
// on any spelling but these six; callers do not all stick to them.
private fun canonicalMode(mode: String): String {
val read = 'r' in mode
val truncate = 't' in mode
return when {
'w' !in mode -> "r"
read && truncate -> "rwt"
read -> "rw"
truncate -> "wt"
'a' in mode -> "wa"
else -> "w"
}
}
private fun includeFile(result: MatrixCursor, file: File) {
result.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, file.absolutePath)
@@ -1,9 +1,11 @@
package com.follow.clash.service
import android.app.Service
import android.content.ComponentCallbacks2
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.GlobalState
import com.follow.clash.common.sendBroadcast
import com.follow.clash.core.Core
interface ManagedService {
fun start()
@@ -20,3 +22,14 @@ internal fun Service.notifyVpnRevoked() {
GlobalState.log("VPN permission revoked")
BroadcastAction.VPN_REVOKED.sendBroadcast()
}
// UI_HIDDEN reaches the services too, since they share the activity's process,
// and Android 14 stopped delivering the running levels this answers to.
@Suppress("DEPRECATION")
internal fun trimCoreMemory(level: Int) {
if (level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW ||
level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL
) {
Core.forceGC()
}
}
@@ -24,6 +24,11 @@ class ProxyService : Service(), ManagedService {
super.onLowMemory()
}
override fun onTrimMemory(level: Int) {
trimCoreMemory(level)
super.onTrimMemory(level)
}
inner class LocalBinder : Binder() {
val service: ProxyService
get() = this@ProxyService
@@ -7,7 +7,6 @@ import android.net.ProxyInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.content.getSystemService
import com.follow.clash.common.AccessControlMode
import com.follow.clash.common.GlobalState
@@ -18,6 +17,8 @@ import com.follow.clash.service.models.VpnOptions
import com.follow.clash.service.models.getIpv4RouteAddress
import com.follow.clash.service.models.getIpv6RouteAddress
import com.follow.clash.service.models.toCIDR
import com.follow.clash.service.models.tunMtu
import com.follow.clash.service.models.tunOptions
import com.follow.clash.service.modules.ServiceModules
import java.net.InetSocketAddress
import java.util.concurrent.ConcurrentHashMap
@@ -92,6 +93,11 @@ class VpnService : SystemVpnService(), ManagedService {
super.onLowMemory()
}
override fun onTrimMemory(level: Int) {
trimCoreMemory(level)
super.onTrimMemory(level)
}
inner class LocalBinder : Binder() {
val service: VpnService
get() = this@VpnService
@@ -120,7 +126,7 @@ class VpnService : SystemVpnService(), ManagedService {
val fd = with(Builder()) {
addAddressAndRoutes(options)
addDnsServers(options)
setMtu(MTU)
setMtu(options.tunMtu)
configureAccessControl(options)
setSession(getString(CommonR.string.app_name))
setBlocking(false)
@@ -160,6 +166,7 @@ class VpnService : SystemVpnService(), ManagedService {
stack = options.stack,
address = options.tunAddress,
dns = options.tunDns,
options = options.tunOptions,
),
) { "Core rejected the tun file descriptor" }
} catch (error: Exception) {
@@ -175,7 +182,7 @@ class VpnService : SystemVpnService(), ManagedService {
addRoutes(
routes = options::getIpv4RouteAddress,
fallbackAddress = NET_ANY,
logTag = "addRoute4",
routeAll = options.routeAddress.isEmpty(),
)
if (options.ipv6) {
@@ -188,24 +195,25 @@ class VpnService : SystemVpnService(), ManagedService {
addRoutes(
routes = options::getIpv6RouteAddress,
fallbackAddress = NET_ANY6,
logTag = "addRoute6",
routeAll = options.routeAddress.isEmpty(),
)
}
}
// Flutter lists every family it routes, so a family missing from a
// non-empty list is one whose routes were all excluded.
private fun Builder.addRoutes(
routes: () -> List<CIDR>,
fallbackAddress: String,
logTag: String,
routeAll: Boolean,
) {
val routeList = runCatching(routes).getOrDefault(emptyList())
if (routeList.isEmpty()) {
val routeList = runCatching(routes).getOrNull()
if (routeAll || routeList == null) {
addRoute(fallbackAddress, 0)
return
}
try {
routeList.forEach { route ->
Log.d(logTag, "address: ${route.address} prefixLength: ${route.prefixLength}")
addRoute(route.address, route.prefixLength)
}
} catch (_: Exception) {
@@ -285,13 +293,14 @@ class VpnService : SystemVpnService(), ManagedService {
}
companion object {
private const val IPV4_ADDRESS = "172.19.0.1/30"
// mihomo's own TUN default: 198.18.0.0/15 is benchmarking space no LAN
// uses, and its fake-ip pool starts at .4, past this /30.
private const val IPV4_ADDRESS = "198.18.0.1/30"
private const val IPV6_ADDRESS = "fdfe:dcba:9876::1/126"
private const val DNS = "172.19.0.2"
private const val DNS = "198.18.0.2"
private const val DNS6 = "fdfe:dcba:9876::2"
private const val NET_ANY = "0.0.0.0"
private const val NET_ANY6 = "::"
private const val LOCAL_HOST = "127.0.0.1"
private const val MTU = 9000
}
}
@@ -1,6 +1,7 @@
package com.follow.clash.service.models
import com.follow.clash.common.AccessControlMode
import com.google.gson.Gson
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
@@ -23,8 +24,29 @@ data class VpnOptions(
val bypassDomain: List<String>,
val stack: String,
val routeAddress: List<String>,
val mtu: Int = 0,
val congestionController: String? = null,
)
private const val DEFAULT_TUN_MTU = 9000
// establish() rejects an MTU above 65535, and below 1280 the kernel drops the
// IPv6 address.
private val TUN_MTU_RANGE = 1280..65535
private val gson = Gson()
val VpnOptions.tunMtu: Int
get() = if (mtu > 0) mtu.coerceIn(TUN_MTU_RANGE) else DEFAULT_TUN_MTU
val VpnOptions.tunOptions: String
get() = gson.toJson(
mapOf(
"mtu" to tunMtu,
"congestion-controller" to congestionController.orEmpty(),
),
)
data class CIDR(
val address: InetAddress,
val prefixLength: Int,
@@ -10,6 +10,7 @@ import android.net.NetworkCapabilities.TRANSPORT_USB
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import androidx.core.content.getSystemService
import com.follow.clash.core.Core
@@ -32,8 +33,9 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
private val connectivity by lazy {
service.getSystemService<ConnectivityManager>()
}
private val mainHandler = Handler(Looper.getMainLooper())
private val handler = Handler(networkObserveLooper)
private var currentDnsList = listOf<String>()
private var currentNetwork: Network? = null
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
@@ -47,15 +49,15 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
networkInfos[network] = NetworkInfo()
updateDns()
scheduleUpdateDns()
}
override fun onLosing(network: Network, maxMsToLive: Int) {
val info = networkInfos[network] ?: return
info.losingUntilMillis = System.currentTimeMillis() + maxMsToLive
updateDns()
scheduleUpdateDns()
if (maxMsToLive > 0) {
mainHandler.postDelayed({
handler.postDelayed({
if (networkInfos.containsKey(network)) {
updateDns()
}
@@ -65,15 +67,24 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
override fun onLost(network: Network) {
networkInfos.remove(network)
updateDns()
scheduleUpdateDns()
}
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
networkInfos[network]?.dnsList = linkProperties.dnsServers
updateDns()
scheduleUpdateDns()
}
}
private val updateDnsTask = Runnable(::updateDns)
// A switch arrives as a burst, and a network that just came up has no DNS
// until its link properties follow; the Core flushes its cache per list.
private fun scheduleUpdateDns() {
handler.removeCallbacks(updateDnsTask)
handler.postDelayed(updateDnsTask, DNS_UPDATE_DELAY_MILLIS)
}
override fun start() {
updateDns()
connectivity?.registerNetworkCallback(request, callback)
@@ -100,32 +111,44 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
@Synchronized
private fun updateDns() {
val dnsList = networkInfos.asSequence()
.minByOrNull(::networkPriority)
val preferred = networkInfos.entries.minByOrNull(::networkPriority)
val dnsList = preferred
?.value
?.dnsList
.orEmpty()
.map { address -> address.asSocketAddressText(DNS_PORT) }
.distinct()
if (dnsList == currentDnsList) {
return
if (dnsList != currentDnsList) {
currentDnsList = dnsList
Core.updateDNS(dnsList.joinToString(","))
}
currentDnsList = dnsList
Core.updateDNS(dnsList.joinToString(","))
val network = preferred?.key ?: return
// Sockets protected onto the network we left stay bound to it; closing
// them makes apps reconnect now instead of after a TCP timeout.
if (currentNetwork != null && currentNetwork != network) {
Core.resetNetwork()
}
currentNetwork = network
}
override fun stop() {
mainHandler.removeCallbacksAndMessages(null)
handler.removeCallbacksAndMessages(null)
try {
connectivity?.unregisterNetworkCallback(callback)
} finally {
networkInfos.clear()
updateDns()
currentNetwork = null
}
}
}
private const val DNS_PORT = 53
private const val DNS_UPDATE_DELAY_MILLIS = 200L
private val networkObserveLooper: Looper by lazy {
HandlerThread("NetworkObserve").apply { start() }.looper
}
private fun InetAddress.asSocketAddressText(port: Int): String = when (this) {
is Inet6Address -> "[$hostAddress]:$port"
@@ -11,6 +11,7 @@ import androidx.core.content.getSystemService
import com.follow.clash.common.Components
import com.follow.clash.common.GlobalState
import com.follow.clash.common.QuickAction
import com.follow.clash.common.ensureNotificationChannel
import com.follow.clash.common.quickIntent
import com.follow.clash.common.receiveBroadcastFlow
import com.follow.clash.common.startForeground
@@ -22,9 +23,9 @@ import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.getSpeedTrafficText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -50,31 +51,36 @@ internal class NotificationModule(
private val scope: CoroutineScope,
) : ServiceModule {
override fun start() {
service.ensureNotificationChannel()
update(ServiceConfig.notificationParams.value.extended)
scope.launch {
val screenFlow = service.receiveBroadcastFlow {
// Some ROMs, vivo's among them, drop updates posted behind the
// keyguard, so unlocking restarts the loop to post once more.
// SystemUI, an app, sends USER_PRESENT, which a receiver that is
// not exported never gets; all three actions are protected.
service.receiveBroadcastFlow(exported = true) {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_USER_PRESENT)
}.map { intent ->
intent.action == Intent.ACTION_SCREEN_ON
intent.action != Intent.ACTION_SCREEN_OFF
}.onStart {
emit(isScreenOn())
}.collectLatest { screenOn ->
if (!screenOn) return@collectLatest
combine(
flow {
while (true) {
delay(1_000)
emit(Unit)
}
},
ServiceConfig.notificationParams,
) { _, params ->
params.extended
}.distinctUntilChanged()
.collect(::update)
}
combine(
flow {
while (true) {
delay(1_000)
emit(Unit)
}
},
ServiceConfig.notificationParams,
screenFlow,
) { _, params, screenOn ->
params.takeIf { screenOn }?.extended
}.filterNotNull()
.distinctUntilChanged()
.collect(::update)
}
}
@@ -102,6 +108,8 @@ internal class NotificationModule(
}
}
private val stopIntent by lazy { QuickAction.STOP.quickIntent.toPendingIntent }
private fun update(params: ExtendedNotificationParams) {
service.startForeground(
with(notificationBuilder) {
@@ -109,11 +117,7 @@ internal class NotificationModule(
setContentText(params.contentText)
clearActions()
if (params.showStopAction) {
addAction(
0,
params.stopText,
QuickAction.STOP.quickIntent.toPendingIntent,
)
addAction(0, params.stopText, stopIntent)
}
build()
},
@@ -24,7 +24,6 @@ internal class ServiceModules(private val service: Service) {
val nextModules = listOf(
NotificationModule(service, nextScope),
NetworkObserveModule(service),
SuspendModule(service, nextScope),
)
val startedModules = mutableListOf<ServiceModule>()
@@ -1,47 +0,0 @@
package com.follow.clash.service.modules
import android.app.Service
import android.content.Intent
import android.os.PowerManager
import androidx.core.content.getSystemService
import com.follow.clash.common.receiveBroadcastFlow
import com.follow.clash.core.Core
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
internal class SuspendModule(
private val service: Service,
private val scope: CoroutineScope,
) : ServiceModule {
private fun isScreenOn() =
service.getSystemService<PowerManager>()?.isInteractive ?: true
private val isDeviceIdle: Boolean
get() = service.getSystemService<PowerManager>()?.isDeviceIdleMode ?: true
private fun updateSuspension(screenOn: Boolean) {
Core.suspended(!screenOn && isDeviceIdle)
}
override fun start() {
scope.launch {
val screenFlow = service.receiveBroadcastFlow {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
}.map {
isScreenOn()
}.onStart {
emit(isScreenOn())
}
screenFlow.collect(::updateSuspension)
}
}
override fun stop() {
Core.suspended(false)
}
}
+3 -3
View File
@@ -6,12 +6,12 @@
android:viewportHeight="240"
tools:ignore="VectorRaster">
<path
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:pathData="M48.1,80.89L168.44,11.41A23.16,23.16 0,0 1,191.6,51.53L71.26,121.01A23.16,23.16 0,0 1,48.1,80.89Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
android:pathData="M78.98,134.37L139.16,99.63A23.16,23.16 0,0 1,162.32,139.75L102.14,174.49A23.16,23.16 0,0 1,78.98,134.37Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:pathData="M98.28,207.92A23.16,23.16 0,1 1,144.6,207.92A23.16,23.16 0,1 1,98.28,207.92Z"
android:fillColor="#FFFFFF"/>
</vector>
@@ -6,12 +6,12 @@
android:viewportHeight="240"
tools:ignore="VectorRaster">
<path
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:pathData="M48.1,80.89L168.44,11.41A23.16,23.16 0,0 1,191.6,51.53L71.26,121.01A23.16,23.16 0,0 1,48.1,80.89Z"
android:fillColor="#6666FB"/>
<path
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
android:pathData="M78.98,134.37L139.16,99.63A23.16,23.16 0,0 1,162.32,139.75L102.14,174.49A23.16,23.16 0,0 1,78.98,134.37Z"
android:fillColor="#336AB6"/>
<path
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:pathData="M98.28,207.92A23.16,23.16 0,1 1,144.6,207.92A23.16,23.16 0,1 1,98.28,207.92Z"
android:fillColor="#5CA8E9"/>
</vector>
+2 -4
View File
@@ -19,10 +19,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
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
id("com.android.application") version "9.4.1" apply false
id("org.jetbrains.kotlin.android") version "2.4.20" apply false
}
include(":app")
+267 -44
View File
@@ -18,7 +18,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
private fun vpnOptions(enable: Boolean = true) = VpnOptions(
private fun vpnOptions(enable: Boolean = true, stack: String = "gvisor") = VpnOptions(
enable = enable,
port = 7890,
ipv6 = false,
@@ -32,41 +32,45 @@ private fun vpnOptions(enable: Boolean = true) = VpnOptions(
allowBypass = false,
systemProxy = true,
bypassDomain = emptyList(),
stack = "gvisor",
stack = stack,
routeAddress = emptyList(),
)
private fun configuredState(enable: Boolean = true) = SharedState(
vpnOptions = vpnOptions(enable),
private fun configuredState(enable: Boolean = true, stack: String = "gvisor") = SharedState(
vpnOptions = vpnOptions(enable, stack),
setupParams = SetupParams(testUrl = "https://example.com", selectedMap = emptyMap()),
)
private const val STARTED_AT = 1_700_000_000_000L
private class FakeTile : TileGateway {
var startCount = 0
var stopCount = 0
override fun handleStart() {
startCount++
}
override fun handleStop() {
stopCount++
}
}
private class FakeApp(
private val notificationGranted: Boolean = true,
private val localNetworkGranted: Boolean = true,
private val vpnGranted: Boolean = true,
private val holdVpnPreparation: Boolean = false,
) : AppGateway {
var beforeVpnPrepared: (() -> Unit)? = null
var cancelledPreparations = 0
var localNetworkRequests = 0
private var heldCallback: ((Boolean) -> Unit)? = null
override fun requestNotificationPermission(callback: (Boolean) -> Unit) =
callback(notificationGranted)
override fun requestLocalNetworkPermission(callback: (Boolean) -> Unit) {
localNetworkRequests++
callback(localNetworkGranted)
}
override fun prepareVpn(enable: Boolean, callback: (Boolean) -> Unit) {
beforeVpnPrepared?.invoke()
if (holdVpnPreparation) {
@@ -87,24 +91,27 @@ private class FakeApp(
private class FakeHost(override val scope: CoroutineScope) : ServiceStateHost {
var storedSharedState = configuredState()
var setupResult: Result<String> = Result.success("")
var startResult = 1_700_000_000_000L
var startSucceeds = true
var vpnPermissionGranted = true
var vpnServiceActive = true
var localNetworkPermissionGranted = true
var runsAsVpn: Boolean? = null
var tile: TileGateway? = null
var app: AppGateway? = null
var duringSetup: (suspend () -> Unit)? = null
var beforeStartService: (() -> Unit)? = null
var lastStartOptions: VpnOptions? = null
override var runTimeMillis = 0L
override var runtime: ServiceRuntime? = null
override val homeDirPath = "/data/user/0/com.follow.clash/files"
override val sdkInt = 34
val toasts = mutableListOf<String>()
val logs = mutableListOf<String>()
val notificationParams = mutableListOf<NotificationParams>()
val crashlytics = mutableListOf<Boolean>()
var setupCalls = 0
var startCalls = 0
var stopCalls = 0
var stoppedNotifications = 0
var lastInitParams: String? = null
var lastSetupParams: String? = null
@@ -116,10 +123,6 @@ private class FakeHost(override val scope: CoroutineScope) : ServiceStateHost {
toasts += message
}
override fun setCrashlytics(enabled: Boolean) {
crashlytics += enabled
}
override fun updateNotificationParams(params: NotificationParams) {
notificationParams += params
}
@@ -128,30 +131,39 @@ private class FakeHost(override val scope: CoroutineScope) : ServiceStateHost {
override fun isVpnPermissionGranted(): Boolean = vpnPermissionGranted
override fun isLocalNetworkPermissionGranted(): Boolean = localNetworkPermissionGranted
override fun tile(): TileGateway? = tile
override fun app(): AppGateway? = app
override fun notifyStopped() {
stoppedNotifications++
}
override suspend fun quickSetup(initParams: String, setupParams: String): Result<String> {
setupCalls++
lastInitParams = initParams
lastSetupParams = setupParams
duringSetup?.invoke()
return setupResult
}
override suspend fun startService(options: VpnOptions): Long {
override suspend fun startService(options: VpnOptions): Boolean {
startCalls++
lastStartOptions = options
beforeStartService?.invoke()
runTimeMillis = startResult
return startResult
if (!startSucceeds) {
return false
}
runtime = ServiceRuntime(vpn = runsAsVpn ?: options.enable, startedAtMillis = STARTED_AT)
return true
}
override suspend fun stopService() {
stopCalls++
runTimeMillis = 0L
runtime = null
}
override suspend fun isVpnServiceActive(): Boolean = vpnServiceActive
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -187,16 +199,33 @@ class ServiceStateMachineTest {
}
@Test
fun `refresh reports STARTED only while the service has a run time`() = runTest {
fun `the run time is reported only while a run is both requested and up`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
assertEquals(0L, machine.refresh())
assertEquals(0L, machine.awaitRunTime())
machine.requestStart().await()
assertEquals(STARTED_AT, machine.awaitRunTime())
machine.requestStop().await()
assertEquals(0L, machine.awaitRunTime())
}
@Test
fun `a stop request reads as STOPPING until the service is down`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
machine.requestStart().await()
val stop = machine.requestStop()
assertEquals(RunState.STOPPING, machine.runState.value)
assertEquals(0L, machine.awaitRunTime())
assertTrue(stop.await())
assertEquals(RunState.STOPPED, machine.runState.value)
host.runTimeMillis = 42L
assertEquals(42L, machine.refresh())
assertEquals(RunState.STARTED, machine.runState.value)
}
@Test
@@ -213,12 +242,14 @@ class ServiceStateMachineTest {
@Test
fun `a start that the service refuses settles back to STOPPED`() = runTest {
val host = FakeHost(backgroundScope)
host.startResult = 0L
host.startSucceeds = false
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
assertFalse(machine.requestStart().await())
assertEquals(RunState.STOPPED, machine.runState.value)
assertFalse(machine.isRunRequested)
assertEquals(1, host.stoppedNotifications)
}
@Test
@@ -240,6 +271,42 @@ class ServiceStateMachineTest {
assertFalse(machine.requestStart().await())
assertEquals(0, host.startCalls)
assertEquals(RunState.STOPPED, machine.runState.value)
assertEquals(1, host.stoppedNotifications)
}
@Test
fun `a start repeated on a settled run asks for nothing and keeps the service`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
machine.requestStart().await()
val token = machine.captureRequestToken()
val app = FakeApp(notificationGranted = false)
host.app = app
assertTrue(machine.requestStart().await())
assertTrue(machine.captureRequestToken() === token)
assertEquals(0, app.localNetworkRequests)
assertEquals(1, host.startCalls)
assertEquals(0, host.stopCalls)
assertEquals(RunState.STARTED, machine.runState.value)
}
@Test
fun `a start that changes the service kind is refused down to STOPPED`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState(enable = false))
machine.requestStart().await()
machine.syncSharedState(configuredState(enable = true))
host.app = FakeApp(vpnGranted = false)
assertFalse(machine.requestStart().await())
assertEquals(1, host.stopCalls)
assertEquals(RunState.STOPPED, machine.runState.value)
assertFalse(machine.isRunRequested)
}
@Test
@@ -251,8 +318,30 @@ class ServiceStateMachineTest {
assertFalse(machine.requestStart().await())
assertEquals(0, host.startCalls)
assertEquals(1, host.stoppedNotifications)
}
/** Flutter opened the Core's listeners before asking, and may be gone before it hears back. */
@Test
fun `a start refused before any service came up closes the core without passing STOPPING`() =
runTest {
val host = FakeHost(backgroundScope)
host.app = FakeApp(vpnGranted = false)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
val states = mutableListOf<RunState>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
machine.runState.toList(states)
}
assertFalse(machine.requestStart().await())
testScheduler.runCurrent()
assertEquals(1, host.stopCalls)
assertFalse(states.contains(RunState.STOPPING))
assertEquals(RunState.STOPPED, machine.runState.value)
}
/**
* Without a foreground app there is nobody to show the system consent dialog, so the machine
* has to explain the refusal itself.
@@ -279,6 +368,57 @@ class ServiceStateMachineTest {
assertEquals(1, host.startCalls)
}
/** Android 17 drops the kernel hop system/mixed rely on until the permission is granted. */
@Test
fun `a denied local network permission falls back to the gvisor stack`() = runTest {
val host = FakeHost(backgroundScope)
host.localNetworkPermissionGranted = false
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState(stack = "system"))
assertTrue(machine.requestStart().await())
assertEquals("gvisor", host.lastStartOptions?.stack)
assertTrue(host.toasts.contains(configuredState().localNetworkTip))
}
@Test
fun `a granted local network permission keeps the chosen stack`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState(stack = "mixed"))
assertTrue(machine.requestStart().await())
assertEquals("mixed", host.lastStartOptions?.stack)
assertFalse(host.toasts.contains(configuredState().localNetworkTip))
}
@Test
fun `a proxy-only start keeps its stack without the local network permission`() = runTest {
val host = FakeHost(backgroundScope)
host.localNetworkPermissionGranted = false
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState(enable = false, stack = "system"))
assertTrue(machine.requestStart().await())
assertEquals("system", host.lastStartOptions?.stack)
assertFalse(host.toasts.contains(configuredState().localNetworkTip))
}
@Test
fun `the local network prompt is asked once and never blocks the start`() = runTest {
val host = FakeHost(backgroundScope)
val app = FakeApp(localNetworkGranted = false)
host.app = app
host.localNetworkPermissionGranted = false
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState(stack = "system"))
assertTrue(machine.requestStart().await())
assertEquals(1, app.localNetworkRequests)
assertEquals(1, host.startCalls)
assertEquals("gvisor", host.lastStartOptions?.stack)
}
@Test
fun `a stop request drives the service to STOPPED`() = runTest {
val host = FakeHost(backgroundScope)
@@ -289,6 +429,7 @@ class ServiceStateMachineTest {
assertTrue(machine.requestStop().await())
assertEquals(RunState.STOPPED, machine.runState.value)
assertEquals(1, host.stopCalls)
assertEquals(0, host.stoppedNotifications)
}
@Test
@@ -322,6 +463,7 @@ class ServiceStateMachineTest {
host.beforeStartService = { machine.requestStop() }
assertFalse(machine.requestStart().await())
assertEquals(0, host.stoppedNotifications)
}
@Test
@@ -332,12 +474,15 @@ class ServiceStateMachineTest {
machine.requestStart().await()
val token = machine.captureRequestToken()
host.runTimeMillis = 0L
host.runtime = null
machine.handleServiceLost(token)
assertEquals(RunState.STOPPED, machine.runState.value)
assertFalse(machine.isRunRequested)
assertEquals(1, host.stoppedNotifications)
}
/** The newer start still owns the intent, but nothing is running until it brings it up. */
@Test
fun `handleServiceLost keeps the intent of a start that raced ahead of it`() = runTest {
val host = FakeHost(backgroundScope)
@@ -346,10 +491,12 @@ class ServiceStateMachineTest {
val staleToken = machine.captureRequestToken()
machine.requestStart().await()
host.runTimeMillis = 0L
host.runtime = null
machine.handleServiceLost(staleToken)
assertEquals(RunState.STARTED, machine.runState.value)
assertTrue(machine.isRunRequested)
assertEquals(RunState.STOPPED, machine.runState.value)
assertEquals(0, host.stoppedNotifications)
}
@Test
@@ -401,6 +548,7 @@ class ServiceStateMachineTest {
assertEquals(listOf(MISSING_CONFIG_MESSAGE), host.toasts)
assertEquals(0, host.setupCalls)
assertFalse(machine.isRunRequested)
}
@Test
@@ -447,16 +595,17 @@ class ServiceStateMachineTest {
@Test
fun `a service that refuses the start after a good setup is reported`() = runTest {
val host = FakeHost(backgroundScope)
host.startResult = 0L
host.startSucceeds = false
val machine = ServiceStateMachine(host)
machine.handleStartAction()
assertTrue(host.toasts.contains(START_FAILED_MESSAGE))
assertFalse(machine.isRunRequested)
}
@Test
fun `handleStopAction hands the stop to the tile when one is attached`() = runTest {
fun `handleStopAction stops the service itself and reports it to Flutter`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
@@ -466,8 +615,69 @@ class ServiceStateMachineTest {
machine.handleStopAction()
assertEquals(1, tile.stopCount)
assertEquals(0, tile.startCount)
assertEquals(1, host.stopCalls)
assertEquals(RunState.STOPPED, machine.runState.value)
assertEquals(1, host.stoppedNotifications)
}
@Test
fun `a toggle during the native setup cancels the start without reporting a failure`() =
runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
host.duringSetup = { machine.handleToggleAction() }
machine.handleStartAction()
assertEquals(1, host.setupCalls)
assertEquals(0, host.startCalls)
assertFalse(host.toasts.contains(START_FAILED_MESSAGE))
assertFalse(machine.isRunRequested)
assertEquals(RunState.STOPPED, machine.runState.value)
}
/** The setup already opened the Core's ports, so a start that ends there has to close them. */
@Test
fun `a native start that does not end up running tears the core setup down`() = runTest {
val cancelled = FakeHost(backgroundScope)
val cancelledMachine = ServiceStateMachine(cancelled)
cancelled.duringSetup = { cancelledMachine.handleToggleAction() }
cancelledMachine.handleStartAction()
val refused = FakeHost(backgroundScope)
refused.vpnPermissionGranted = false
ServiceStateMachine(refused).handleStartAction()
val rejected = FakeHost(backgroundScope)
rejected.setupResult = Result.success("proxy group not found")
ServiceStateMachine(rejected).handleStartAction()
assertEquals(listOf(1, 1, 1), listOf(cancelled, refused, rejected).map { it.stopCalls })
}
@Test
fun `a native start that runs leaves the core setup alone`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.handleStartAction()
assertEquals(0, host.stopCalls)
assertEquals(RunState.STARTED, machine.runState.value)
}
@Test
fun `a second start action during the native setup is ignored`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
host.duringSetup = { machine.handleStartAction() }
machine.handleStartAction()
assertEquals(1, host.setupCalls)
assertEquals(1, host.startCalls)
assertEquals(RunState.STARTED, machine.runState.value)
}
@Test
@@ -509,9 +719,8 @@ class ServiceStateMachineTest {
@Test
fun `a revoke is ignored while no vpn service is active`() = runTest {
val host = FakeHost(backgroundScope)
host.vpnServiceActive = false
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
machine.syncSharedState(configuredState(enable = false))
machine.requestStart().await()
machine.handleVpnRevokeAction()
@@ -519,6 +728,21 @@ class ServiceStateMachineTest {
assertEquals(0, host.stopCalls)
}
@Test
fun `a revoke stops the vpn service even with Flutter attached`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
machine.requestStart().await()
host.tile = FakeTile()
machine.handleVpnRevokeAction()
assertEquals(1, host.stopCalls)
assertEquals(RunState.STOPPED, machine.runState.value)
assertEquals(1, host.stoppedNotifications)
}
@Test
fun `a revoke stops the active vpn service`() = runTest {
val host = FakeHost(backgroundScope)
@@ -532,20 +756,18 @@ class ServiceStateMachineTest {
}
@Test
fun `syncSharedState pushes crashlytics and the notification straight through`() = runTest {
fun `syncSharedState pushes the notification straight through`() = runTest {
val host = FakeHost(backgroundScope)
val machine = ServiceStateMachine(host)
machine.syncSharedState(
SharedState(
crashlytics = false,
currentProfileName = "Work",
stopText = "Disconnect",
onlyStatisticsProxy = true,
),
)
assertEquals(listOf(false), host.crashlytics)
assertEquals(listOf(NotificationParams("Work", "Disconnect", true)), host.notificationParams)
}
@@ -565,7 +787,7 @@ class ServiceStateMachineTest {
assertFalse(states.contains(RunState.STARTED))
assertEquals(RunState.STOPPED, machine.runState.value)
assertEquals(0L, host.runTimeMillis)
assertEquals(null, host.runtime)
}
@Test
@@ -582,7 +804,7 @@ class ServiceStateMachineTest {
testScheduler.runCurrent()
assertEquals(1, host.stopCalls)
assertEquals(0L, host.runTimeMillis)
assertEquals(null, host.runtime)
assertEquals(RunState.STOPPED, machine.runState.value)
assertFalse(machine.captureRequestToken().running)
}
@@ -610,7 +832,7 @@ class ServiceStateMachineTest {
@Test
fun `a start rebinds when the running service is not the one the options ask for`() = runTest {
val host = FakeHost(backgroundScope)
host.vpnServiceActive = false
host.runsAsVpn = false
val machine = ServiceStateMachine(host)
machine.syncSharedState(configuredState())
host.beforeStartService = {
@@ -654,5 +876,6 @@ class ServiceStateMachineTest {
assertFalse(machine.requestStart().await())
assertTrue(host.logs.any { it.contains("binder died") })
assertFalse(machine.captureRequestToken().running)
assertEquals(RunState.STOPPED, machine.runState.value)
}
}
+3 -5
View File
@@ -17,12 +17,13 @@ class SharedStateTest {
@Test
fun `setup params use the kebab-case spelling Flutter writes`() {
val json = """{"test-url":"https://example.test/204","selected-map":{"GLOBAL":"auto"}}"""
val json = """{"test-url":"https://example.test/204","selected-map":{"GLOBAL":"auto"},"skip-cert-verify":true}"""
val params = gson.fromJson(json, SetupParams::class.java)
assertEquals("https://example.test/204", params.testUrl)
assertEquals(mapOf("GLOBAL" to "auto"), params.selectedMap)
assertTrue(params.skipCertVerify)
}
@Test
@@ -33,6 +34,7 @@ class SharedStateTest {
assertTrue(encoded.contains("\"test-url\""))
assertTrue(encoded.contains("\"selected-map\""))
assertTrue(encoded.contains("\"skip-cert-verify\""))
assertTrue(!encoded.contains("testUrl"))
assertTrue(!encoded.contains("selectedMap"))
}
@@ -43,7 +45,6 @@ class SharedStateTest {
{
"startTip": "Starting",
"stopTip": "Stopping",
"crashlytics": false,
"currentProfileName": "Work",
"stopText": "Halt",
"onlyStatisticsProxy": true,
@@ -76,7 +77,6 @@ class SharedStateTest {
assertEquals("Starting", state.startTip)
assertEquals("Work", state.currentProfileName)
assertEquals(false, state.crashlytics)
assertEquals(true, state.onlyStatisticsProxy)
assertEquals(false, state.showStopAction)
assertEquals(7890, state.vpnOptions?.port)
@@ -95,7 +95,6 @@ class SharedStateTest {
assertEquals("FlClash", defaults.currentProfileName)
assertEquals("Stop", defaults.stopText)
assertEquals(true, defaults.crashlytics)
assertEquals(false, defaults.onlyStatisticsProxy)
assertEquals(true, defaults.showStopAction)
assertNull(defaults.vpnOptions)
@@ -112,7 +111,6 @@ class SharedStateTest {
assertNotNull(state)
assertEquals("Starting VPN...", state.startTip)
assertEquals("FlClash", state.currentProfileName)
assertEquals(true, state.crashlytics)
assertNull(state.vpnOptions)
assertNull(state.setupParams)
}
+28
View File
@@ -1,6 +1,7 @@
package com.follow.clash.service.models
import com.follow.clash.common.AccessControlMode
import com.google.gson.JsonParser
import java.net.Inet4Address
import java.net.Inet6Address
import org.junit.Assert.assertEquals
@@ -142,3 +143,30 @@ class RouteAddressTest {
}
}
}
class TunOptionsTest {
@Test
fun `an unset mtu falls back to the core default`() {
assertEquals(9000, optionsWithRoutes(emptyList()).tunMtu)
assertEquals(1400, optionsWithRoutes(emptyList()).copy(mtu = 1400).tunMtu)
}
@Test
fun `hands the core the mtu the builder was given`() {
val options = optionsWithRoutes(emptyList()).copy(congestionController = "bbr")
val json = JsonParser.parseString(options.tunOptions).asJsonObject
assertEquals(9000, json["mtu"].asInt)
assertEquals("bbr", json["congestion-controller"].asString)
}
@Test
fun `a state saved before the options existed still encodes`() {
val options = optionsWithRoutes(emptyList()).copy(congestionController = null)
val json = JsonParser.parseString(options.tunOptions).asJsonObject
assertEquals("", json["congestion-controller"].asString)
}
}
+489 -167
View File
@@ -9,19 +9,31 @@
"profiles": "Profiles",
"tools": "Tools",
"logs": "Logs",
"logsDesc": "Captured log records",
"resources": "Resources",
"resourcesDesc": "Information about external resources",
"trafficUsage": "Traffic usage",
"networkSpeed": "Network speed",
"outboundMode": "Outbound mode",
"networkDetection": "Network detection",
"serviceStatus": "Service status",
"serviceRestricted": "Access restricted",
"serviceAvailable": "Available",
"serviceUnavailable": "Unavailable",
"serviceFailed": "Check failed",
"serviceDisallowedIsp": "Disallowed ISP",
"serviceBlocked": "Blocked",
"serviceUnsupportedRegion": "Region not supported",
"serviceOriginalsOnly": "Originals only",
"serviceComingSoon": "Coming soon",
"servicePending": "Not checked",
"serviceManage": "Manage services",
"serviceCheck": "Check",
"serviceCheckAll": "Check all",
"serviceCheckedAt": "Checked at {time}",
"upload": "Upload",
"download": "Download",
"usedTraffic": "Used traffic",
"totalTraffic": "Total traffic",
"expireTime": "Expiration time",
"nullProfileDesc": "No profiles yet, please add one first",
"nullProfileDesc": "Add a profile to get started",
"settings": "Settings",
"language": "Language",
"defaultText": "Default",
@@ -29,34 +41,31 @@
"other": "Other",
"about": "About",
"en": "English",
"es": "Spanish",
"fr": "French",
"ja": "Japanese",
"ko": "Korean",
"ru": "Russian",
"zhCN": "Simplified Chinese",
"theme": "Theme",
"themeDesc": "Set dark mode and adjust colors",
"override": "Override",
"extend": "Extend",
"runtimeConfig": "Runtime config",
"allowLan": "Allow LAN",
"allowLanDesc": "Allow proxy access over the LAN",
"tun": "TUN",
"tunDesc": "Only effective in administrator mode",
"tunDesc": "TUN works only in administrator mode.",
"minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Override the default system exit behavior",
"autoLaunch": "Auto launch",
"autoLaunchDesc": "Launch automatically at system startup",
"silentLaunch": "Silent launch",
"silentLaunchDesc": "Start in the background",
"silentLaunchDesc": "Silent launch starts without showing the window.",
"autoRun": "Auto run",
"autoRunDesc": "Run automatically when the app opens",
"autoRunDesc": "Auto run starts the proxy when the app opens.",
"logcat": "Logcat",
"logcatDesc": "Disabling hides the log entry point",
"logcatDesc": "Turning off Logcat hides the logs entry.",
"autoCheckUpdate": "Auto check for updates",
"autoCheckUpdateDesc": "Check for updates automatically when the app starts",
"checkCertificate": "Verify TLS certificates",
"checkCertificateDesc": "Reject untrusted certificates. Turning this off exposes subscriptions and backups to man-in-the-middle attacks",
"checkCertificateDesc": "With TLS verification off, subscriptions and backups are open to man-in-the-middle attacks.",
"accessControl": "Access control",
"accessControlDesc": "Control which apps use the proxy",
"application": "Application",
"applicationDesc": "Adjust application settings",
"accessControlDesc": "Access control picks which apps use the proxy.",
"edit": "Edit",
"confirm": "Confirm",
"update": "Update",
@@ -67,15 +76,17 @@
"qrcode": "QR code",
"qrcodeDesc": "Scan a QR code to obtain a profile",
"url": "URL",
"urlDesc": "Obtain a profile from a URL",
"link": "Link",
"linkDesc": "Obtain a profile from a subscription link or a proxy share link",
"file": "File",
"fileDesc": "Upload a profile file directly",
"name": "Name",
"profileNameNullValidationDesc": "Please enter the profile name",
"rename": "Rename",
"profileUrlNullValidationDesc": "Please enter the profile URL",
"profileUrlInvalidValidationDesc": "Please enter a valid profile URL",
"autoUpdate": "Auto update",
"autoUpdateInterval": "Auto-update interval (minutes)",
"autoUpdateInterval": "Auto-update interval",
"profileAutoUpdateIntervalNullValidationDesc": "Please enter the auto-update interval",
"profileAutoUpdateIntervalInvalidValidationDesc": "Please enter a valid interval",
"themeMode": "Theme mode",
@@ -84,7 +95,7 @@
"auto": "Auto",
"light": "Light",
"dark": "Dark",
"importFromURL": "Import from URL",
"importFromLink": "Import from link",
"submit": "Submit",
"createProfileFromUrlTip": "Do you want to create a profile from {url}?",
"create": "Create",
@@ -103,12 +114,15 @@
"show": "Show",
"exit": "Exit",
"systemProxy": "System proxy",
"safeMode": "Safe mode",
"safeModeAppTitle": "{appName} (Safe mode)",
"project": "Project",
"core": "Core",
"tabAnimation": "Tab animation",
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
"startVpn": "Starting VPN...",
"stopVpn": "Stopping VPN...",
"startVpn": "Starting VPN…",
"stopVpn": "Stopping VPN…",
"localNetworkDeniedTip": "Local network permission denied: using the gvisor stack, LAN is unreachable.",
"compatible": "Compatibility mode",
"notSelectedTip": "The current proxy group cannot be selected",
"changeProxyFailedTip": "Failed to switch proxy; the previous selection has been restored",
@@ -127,8 +141,8 @@
"addressTip": "Please enter a valid WebDAV address",
"password": "Password",
"authentication": "Authentication",
"authenticationDesc": "Require credentials on the local proxy port to keep other local apps from using it",
"authenticationSystemProxyDesc": "Not applied while authentication is enabled",
"authenticationDesc": "Authentication asks for a username and password on the local proxy port.",
"authenticationSystemProxyDesc": "The system proxy is not applied while authentication is on.",
"checkUpdate": "Check for updates",
"discoverNewVersion": "New version found",
"checkUpdateError": "The app is already up to date",
@@ -137,40 +151,37 @@
"country": "Region",
"search": "Search",
"allowBypass": "Allow apps to bypass VPN",
"allowBypassDesc": "When enabled, some apps can bypass the VPN",
"externalController": "External controller",
"externalControllerDesc": "When enabled, the Clash core can be controlled on port 9090",
"ipv6Desc": "When enabled, IPv6 traffic can be received",
"externalControllerDesc": "The external controller lets you control the core on port 9090.",
"externalControllerSecret": "External controller secret",
"ipv6Desc": "IPv6 traffic is received only while IPv6 is on.",
"app": "App",
"general": "General",
"systemProxyDesc": "Set the system proxy",
"startupAndBackground": "Startup and background",
"requestsAndUpdates": "Requests and updates",
"logsAndDiagnostics": "Logs and diagnostics",
"userAgent": "User-Agent",
"unifiedDelay": "Unified delay",
"unifiedDelayDesc": "Remove extra delays such as handshakes",
"tcpConcurrent": "TCP concurrent",
"tcpConcurrentDesc": "Allow concurrent TCP connections",
"geodataLoader": "Geo low-memory mode",
"geodataLoaderDesc": "Use the low-memory Geo loader",
"requests": "Requests",
"requestsDesc": "View recent request records",
"recentRequests": "Recent requests",
"findProcessMode": "Find process",
"init": "Init",
"infiniteTime": "Never expires",
"connections": "Connections",
"liveConnections": "Live connections",
"inbound": "Inbound",
"connectionsDesc": "View current connection data",
"intranetIP": "Intranet IP",
"view": "View",
"cut": "Cut",
"copy": "Copy",
"clipboardWriteFailed": "Couldn't copy to the clipboard. The selection may be too large",
"paste": "Paste",
"testUrl": "Test URL",
"sync": "Sync",
"exclude": "Hide from recent tasks",
"excludeDesc": "Hide the app from recent tasks while it is in the background",
"expand": "Standard",
"shrink": "Compact",
"min": "Minimal",
"compact": "Compact",
"tab": "Tab",
"list": "List",
"delay": "Delay",
@@ -182,19 +193,19 @@
"go": "Go",
"externalLink": "External link",
"otherContributors": "Other contributors",
"appIconDesign": "App icon design",
"autoCloseConnections": "Auto close connections",
"autoCloseConnectionsDesc": "Close connections automatically after switching nodes",
"autoCloseConnectionsDesc": "Auto close connections drops open connections when you switch proxies.",
"onlyStatisticsProxy": "Only count proxy traffic",
"onlyStatisticsProxyDesc": "When enabled, only proxy traffic is counted",
"showNotificationStopAction": "Stop button in notification",
"showNotificationStopActionDesc": "Show a stop button on the persistent notification. Turn it off if your system keeps the notification expanded because of it",
"pureBlackMode": "Pure black mode",
"sidebarBlur": "Sidebar blur",
"sidebarBlurDesc": "Show the blurred desktop behind the window through the sidebar",
"liquidGlass": "Liquid Glass",
"keepAliveIntervalDesc": "TCP keep-alive interval",
"entries": " entries",
"local": "Local",
"remote": "Remote",
"remoteBackupDesc": "Back up data to WebDAV",
"localBackupDesc": "Back up data locally",
"mode": "Mode",
"time": "Time",
"source": "Source",
@@ -205,94 +216,174 @@
"layout": "Layout",
"tight": "Tight",
"standard": "Standard",
"ultra": "Ultra",
"loose": "Loose",
"profilesSort": "Sort profiles",
"start": "Start",
"runTime": "Run time",
"stop": "Stop",
"dnsDesc": "Update DNS-related settings",
"key": "Key",
"value": "Value",
"hostsDesc": "Append hosts",
"vpnTip": "Changes take effect after restarting the VPN",
"vpnEnableDesc": "Route all system traffic through VpnService automatically",
"vpnEnableDesc": "VPN routes all system traffic through VpnService automatically.",
"options": "Options",
"loopback": "Loopback unlock tool",
"loopbackDesc": "Used for UWP loopback exemption",
"enabled": "Enabled",
"disabled": "Disabled",
"loopback": "UWP loopback exemption",
"providers": "External resources",
"proxyProviders": "Proxy providers",
"ruleProviders": "Rule providers",
"providerUrlTip": "Only remote providers are supported",
"ruleSetEmptyTip": "The rule set has no usable entries",
"ruleSetMixedTip": "The rule set mixes domains and IP ranges, so its type cannot be told",
"ruleSetInvalidTip": "This mrs file cannot be read",
"subscriptionInfo": "Subscription info",
"overrideDns": "Override DNS",
"overrideDnsDesc": "When enabled, the DNS options in the profile are overridden",
"ntpStatusDesc": "NTP takes the time from an NTP server instead of the system clock.",
"server": "Server",
"ntpInterval": "Sync interval (minutes)",
"dialerProxy": "Dialer proxy",
"dialerProxyDesc": "Dialer proxy is the outbound used to reach the NTP server.",
"writeToSystem": "Write to system",
"writeToSystemDesc": "Write to system also sets the system clock; Android ignores it.",
"sniffer": "Sniffer",
"snifferStatusDesc": "The sniffer reads the domain from TLS, HTTP and QUIC traffic so rules can match it.",
"overrideDestination": "Override destination",
"overrideDestinationDesc": "Override destination connects to the sniffed domain instead of the original address.",
"forceDnsMapping": "Force DNS mapping",
"forceDnsMappingDesc": "Force DNS mapping also sniffs connections whose domain came from a DNS mapping.",
"parsePureIp": "Parse pure IP",
"parsePureIpDesc": "Parse pure IP sniffs connections that carry only an IP address.",
"forceDomain": "Force domains",
"forceDomainDesc": "Force domains are sniffed too; other connections that already have a domain are left alone.",
"skipDomain": "Skip domains",
"skipDomainDesc": "A sniffed domain on the Skip domains list is not used.",
"skipSrcAddress": "Skip source addresses",
"skipDstAddress": "Skip destination addresses",
"sniffProtocols": "Protocols",
"ports": "Ports",
"overrideEntries": "Override entries",
"addOverrideEntry": "Add override entry",
"dnsOverrideDesc": "Entries added here replace the DNS of every URL and file profile; anything not added keeps the profile's own setting. Custom profiles set their DNS in their content.",
"settingEntries": "settings",
"addSettingEntry": "Add setting",
"profileSettingsDesc": "Applies to this profile only; anything not added uses its default.",
"healthCheck": "Health check",
"discardChanges": "Discard the changes?",
"quickEdit": "Quick edit",
"status": "Status",
"statusDesc": "When disabled, the system DNS is used",
"preferH3Desc": "Prefer HTTP/3 for DoH",
"statusDesc": "With DNS off, the system DNS is used.",
"respectRules": "Respect rules",
"respectRulesDesc": "DNS connections follow rules; requires proxy-server-nameserver",
"respectRulesDesc": "Respect rules routes DNS connections by the rules and needs a Proxy Server Nameserver.",
"dnsMode": "DNS mode",
"fakeipRange": "Fake-IP range",
"fakeipFilter": "Fake-IP filter",
"defaultNameserver": "Default nameserver",
"defaultNameserverDesc": "Used to resolve DNS servers",
"nameserver": "Nameserver",
"nameserverDesc": "Used to resolve domains",
"useHosts": "Use hosts",
"useSystemHosts": "Use system hosts",
"nameserverPolicy": "Nameserver policy",
"nameserverPolicyDesc": "Specify the nameserver policy for matching domains",
"proxyNameserver": "Proxy nameserver",
"proxyNameserverDesc": "Used to resolve proxy node domains",
"fallback": "Fallback",
"fallbackDesc": "Usually an overseas DNS",
"fallbackFilter": "Fallback filter",
"geoipCode": "GeoIP code",
"ipcidr": "IP/CIDR",
"domain": "Domain",
"listenRoutingMark": "Listen routing mark",
"listenRoutingMarkDesc": "Listen routing mark works only on Linux.",
"ipv6Timeout": "IPv6 timeout (ms)",
"cacheAlgorithm": "Cache algorithm",
"cacheMaxSize": "Cache size",
"fakeipRange6": "Fake-IP range (IPv6)",
"fakeipFilterMode": "Fake-IP filter mode",
"fakeipFilterModeDesc": "In Fake-IP filter mode, blacklist excludes matches, whitelist fakes only matches, and rule matches as rules.",
"fakeipTtl": "Fake-IP TTL",
"reset": "Reset",
"actionView": "Show/Hide",
"actionStart": "Start/Stop",
"actionMode": "Switch mode",
"actionProxy": "System proxy",
"actionTun": "TUN",
"actionRuleMode": "Rule mode",
"actionGlobalMode": "Global mode",
"actionDirectMode": "Direct mode",
"actionDelayTest": "Test all delays",
"actionUpdateProfiles": "Update profiles",
"disclaimer": "Disclaimer",
"disclaimerDesc": "This software is intended only for non-commercial uses such as learning and research. Using it for any commercial purpose is strictly prohibited; any commercial activity is unrelated to this software.",
"disclaimerDesc": "Before using FlClash (\"the Software\"), please read this statement carefully and make sure you understand all of it. Tapping \"Agree\" means you have read, understood, and accept every term below. If you do not agree, tap \"Exit\" and stop using the Software.",
"disclaimerSoftwareTitle": "Nature of the software",
"disclaimerSoftwareContent": "The Software is an open-source network proxy client built on the Clash.Meta (mihomo) core. It only provides local tooling such as configuration management, rule-based routing, and traffic forwarding.",
"disclaimerNoServiceStatement": "The Software itself does not provide any proxy server, node, subscription, or network access service, and has no partnership, agency, or guarantee relationship with any provider of such services.",
"disclaimerUsageTitle": "Restrictions on use",
"disclaimerUsageContent": "The Software is intended only for non-commercial uses such as learning, exchange, and technical research. Using it for any commercial purpose is strictly prohibited, including but not limited to paid distribution, bundled sales, use as part of a commercial service, or doing business in the name of the Software. Any commercial activity is unrelated to the Software and its developers.\n\nUsing the Software for anything that violates the laws and regulations of your country or region is strictly prohibited, including but not limited to bypassing lawfully imposed network access restrictions, spreading illegal content, launching network attacks, or infringing the lawful rights of others.",
"disclaimerResponsibilityTitle": "Your responsibility",
"disclaimerResponsibilityContent": "You are responsible for making sure that using the Software is lawful where you live, and you alone bear the legal responsibility for everything you do with it and its consequences.\n\nThe subscriptions, nodes, and configurations you import are your own choice. Whether their source is lawful, their content is safe, and their service is reliable is a matter between you and their providers.",
"disclaimerThirdPartyTitle": "Third-party content",
"disclaimerThirdPartyContent": "Subscription links, configuration files, rule sets, scripts, external resources, and external links are all provided by third parties. The developers cannot and do not review or guarantee their legality, accuracy, security, or availability.\n\nAny data leak, financial loss, account ban, or other loss caused by third-party content is to be settled between you and the third party; the developers bear no responsibility for it.",
"disclaimerWarrantyTitle": "No warranty",
"disclaimerWarrantyContent": "The Software is provided \"as is\" and \"as available\", without warranty of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, uninterrupted availability, freedom from errors, or freedom from security vulnerabilities.\n\nThe developers do not guarantee that the Software will meet your needs or that it will run without interruption or error.",
"disclaimerLiabilityTitle": "Limitation of liability",
"disclaimerLiabilityContent": "To the maximum extent permitted by applicable law, neither the developers nor any contributor shall be liable for any direct, indirect, incidental, special, punitive, or consequential damages arising from the use of or inability to use the Software, including but not limited to data loss, device damage, network failures, business interruption, lost profits, or any resulting legal dispute, even if advised of the possibility of such damages.",
"disclaimerPrivacyTitle": "Data collection and privacy",
"disclaimerPrivacyContent": "The Software does not collect or upload your subscription URLs, node details, configuration content, visited websites, connection records, traffic content, or logs. This data stays on your device and the developers have no access to it.\n\nThe Software only reaches the network when you use a feature that needs it, such as fetching the subscription URL you provided when updating a profile, or contacting GitHub when checking for updates.\n\nThe Software includes no analytics or crash reporting service on any platform.",
"disclaimerLicenseTitle": "Open-source license",
"disclaimerLicenseContent": "The Software is open source under the GPL-3.0 license. You may use, modify, and distribute it freely as long as you comply with that license, which requires derivative works to be released under GPL-3.0 as well and the original copyright notices to be kept.\n\nThird-party components in the Software, including the Clash.Meta core, follow their own licenses. The original authors are not responsible for any issue arising from modified or redistributed versions.",
"disclaimerChangesTitle": "Changes to this statement",
"disclaimerChangesContent": "The developers may revise this statement with any release, and the revision takes effect when that release is published. Continuing to use the Software after updating means you accept the revised statement.",
"disclaimerAcceptTitle": "Acceptance",
"disclaimerAcceptContent": "By installing, copying, or using the Software, you are deemed to have read and agreed to this entire statement. If you disagree with any of its terms, stop using and uninstall the Software immediately.",
"disclaimerRestateTitle": "Restate the statement",
"disclaimerRestateTip": "To confirm you understand, enter the following statement in full in the field below:",
"disclaimerRestateHint": "Enter the statement above",
"disclaimerRestateMismatch": "Does not match the statement",
"disclaimerAcknowledgeTitle": "Confirm the statement",
"disclaimerAcknowledgeTip": "To confirm you understand, read the following statement and check the box below:",
"disclaimerAcknowledgeLabel": "I have read and understand this statement",
"agree": "Agree",
"hotkeyManagement": "Hotkey management",
"hotkeyManagementDesc": "Control the app with the keyboard",
"pressKeyboard": "Please press a key",
"inputCorrectHotkey": "Please enter a valid hotkey",
"hotkeyConflict": "Hotkey conflict",
"hotkeyDesc": "Global hotkeys work even while the window is hidden. Tap an action to record its key combination.",
"hotkeyNotSet": "Not set",
"pressKeyboard": "Press a key combination",
"hotkeyNeedsModifier": "Include at least one of {modifiers}",
"hotkeyConflictWith": "Already used by “{action}”. Saving moves it here.",
"hotkeyUnavailable": "Not registered, it may be taken by another app",
"remove": "Remove",
"noHotKey": "No hotkeys yet",
"noNetwork": "No network",
"ipv6InboundDesc": "Allow IPv6 inbound",
"ipv6InboundDesc": "With IPv6 on, the VPN allows IPv6 inbound.",
"exportLogs": "Export logs",
"exportSuccess": "Export successful",
"iconStyle": "Icon style",
"onlyIcon": "Icon only",
"iconStyleFilled": "Filled",
"iconStylePlain": "Plain",
"iconStyleHidden": "Hidden",
"filter": "Filter",
"hideTimeoutProxies": "Hide timed-out proxies",
"hideTimeoutProxiesDesc": "Leave out proxies whose last delay test timed out",
"stackMode": "Stack mode",
"network": "Network",
"networkDesc": "Adjust network-related settings",
"bypassDomain": "Bypass domains",
"bypassDomainDesc": "Only takes effect while the system proxy is enabled",
"bypassDomainDesc": "Bypass domains apply only while the system proxy is on.",
"resetTip": "Are you sure you want to reset?",
"icon": "Icon",
"noData": "No data",
"fontFamily": "Font family",
"installedFonts": "Installed fonts",
"toggle": "Toggle",
"system": "System",
"routeMode": "Route mode",
"routeModeBypassPrivate": "Bypass private addresses",
"routeModeConfig": "Use config",
"routeAddress": "Route addresses",
"routeAddressDesc": "Configure the listened route addresses",
"interfaceNameMode": "Outbound interface",
"interfaceNameModeClear": "Clear",
"interfaceNameModeFollow": "Follow config",
"interfaceNameModeCustom": "Custom",
"interfaceName": "Interface name",
"interfaceNameDesc": "Network interface used for outbound connections",
"interfaceNameDesc": "Interface name is the network interface outbound connections use.",
"copyEnvVar": "Copy environment variables",
"memoryInfo": "Memory info",
"total": "Total",
"releaseMemory": "Release memory",
"memoryCoreHeapInuse": "Heap in use",
"memoryCoreHeapIdle": "Heap idle",
"memoryCoreStack": "Goroutine stacks",
"memoryCoreRuntime": "Runtime overhead",
"memoryCoreNotRunning": "Core is not running",
"memoryAppResident": "Resident memory",
"memoryAppShared": "App & shared",
"memoryEstimateDesc": "Estimated from process resident memory; it may differ from what the system reports.",
"memoryEstimateSharedDesc": "The Core runs inside the app process. Its share is estimated from runtime stats, and the rest counts as app and shared memory.",
"memoryReleased": "Memory released",
"memoryReleasedSize": "Released {size}",
"releaseMemoryFailed": "Failed to release memory",
"cancel": "Cancel",
"fileIsUpdate": "The file has been modified. Save the changes?",
"profileHasUpdate": "The profile has been modified. Turn off auto update?",
@@ -305,38 +396,38 @@
"listen": "Listen",
"undo": "Undo",
"redo": "Redo",
"lineWrap": "Word wrap",
"none": "None",
"basicConfig": "Basic configuration",
"basicConfigDesc": "Modify the basic configuration globally",
"advancedConfig": "Advanced configuration",
"advancedConfigDesc": "Provides diverse configuration options",
"selectedCountTitle": "{count} selected",
"advancedConfigDesc": "Advanced configuration holds network, DNS, added rules, and scripts.",
"addRule": "Add rule",
"ruleName": "Rule name",
"content": "Content",
"subRule": "Sub-rule",
"ruleTarget": "Rule target",
"matchTargetTitle": "Match target",
"matchTarget": "MATCH-TARGET",
"followProfile": "Follow profile",
"matchTargetDesc": "Where rules targeting MATCH-TARGET go. Defaults to the target of the final MATCH rule in this profile.",
"selectMatchTarget": "Select MATCH-TARGET",
"sourceIp": "Source IP",
"noResolve": "Don't resolve IP",
"saveChanges": "Save the changes?",
"findProcessModeDesc": "Enabling causes some performance loss",
"tabAnimationDesc": "Only effective in mobile view",
"colorSchemes": "Color schemes",
"palette": "Palette",
"tonalSpotScheme": "Tonal spot",
"fidelityScheme": "Fidelity",
"cannotSave": "Can't save",
"navigationBarStyle": "Bottom bar",
"floating": "Floating",
"docked": "Docked",
"slide": "Slide",
"fade": "Fade",
"pureBlack": "Pure black",
"customColor": "Custom color",
"colorStyle": "Color style",
"grid": "Grid",
"spectrum": "Spectrum",
"sliders": "Sliders",
"hue": "Hue",
"saturation": "Saturation",
"brightness": "Brightness",
"hex": "Hex",
"monochromeScheme": "Monochrome",
"neutralScheme": "Neutral",
"vibrantScheme": "Vibrant",
"expressiveScheme": "Expressive",
"contentScheme": "Content",
"rainbowScheme": "Rainbow",
"fruitSaladScheme": "Fruit salad",
"vividStyle": "Vivid",
"tintedStyle": "Tinted",
"softStyle": "Soft",
"developerMode": "Developer mode",
"developerModeEnableTip": "Developer mode is enabled.",
"messageTest": "Message test",
@@ -348,6 +439,8 @@
"launchInterruptedTip": "The app exited unexpectedly while it was starting up last time. Automatic setup was skipped for this launch; you can start it manually to retry.",
"clearData": "Clear data",
"textScale": "Text scaling",
"textScalePreview": "Text in the app will look like this",
"followSystem": "Follow system",
"internet": "Internet",
"systemApp": "System apps",
"noNetworkApp": "No-network apps",
@@ -357,11 +450,23 @@
"logsTest": "Logs test",
"emptyTip": "{label} cannot be empty",
"urlTip": "{label} must be a URL",
"invalidLinkTip": "Not a subscription link or a proxy share link",
"numberTip": "{label} must be a number",
"numberRangeTip": "{label} must be between {min} and {max}",
"interval": "Interval",
"existsTip": "{label} already exists",
"maxLengthTip": "{label} must be at most {max} characters",
"multipleValuesTip": "Separate multiple values with commas",
"singleAdd": "Single add",
"batchAdd": "Batch add",
"batchListInputTip": "One item per line, or separated by commas",
"batchMapInputTip": "One entry per line: key, a space, then value",
"singleImport": "Single import",
"batchImport": "Batch import",
"batchUrlInputTip": "One URL per line",
"batchLinkInputTip": "One link per line",
"batchPreviewTip": "{count} to add, {skipped} skipped as existing",
"lineIssueTip": "Line {line}: {message}",
"singleValueTip": "{label} must be a single item",
"deleteTip": "Are you sure you want to delete this {label}?",
"deleteMultipTip": "Are you sure you want to delete the selected {label}?",
"nullTip": "No {label} yet",
@@ -378,6 +483,7 @@
"import": "Import",
"importFile": "Import from file",
"importUrl": "Import from URL",
"startFromScratch": "Start from scratch",
"autoSetSystemDns": "Auto-set system DNS",
"details": "{label} details",
"creationTime": "Creation time",
@@ -396,36 +502,44 @@
"request": "Request",
"connected": "Connected",
"disconnected": "Disconnected",
"connecting": "Connecting...",
"connecting": "Connecting…",
"restartCoreTip": "Are you sure you want to restart the core?",
"forceRestartCoreTip": "Are you sure you want to force restart the core?",
"dnsHijacking": "DNS hijacking",
"coreStatus": "Core status",
"dataCollectionTip": "Data collection notice",
"dataCollectionContent": "This app uses Firebase Crashlytics to collect crash information to improve stability.\nThe collected data includes device information and crash details, and contains no personally sensitive data.\nYou can turn this off in settings.",
"crashlytics": "Crash analytics",
"crashlyticsTip": "When enabled, crash logs without sensitive information are uploaded automatically when the app crashes",
"appendSystemDns": "Append system DNS",
"appendSystemDnsTip": "Force-append the system DNS to the configuration",
"editRule": "Edit rule",
"overrideMode": "Override mode",
"standardModeDesc": "Standard mode: overrides the basic configuration and offers simple rule additions",
"scriptModeDesc": "Script mode: uses external extension scripts to override the configuration in one click",
"extendMode": "Extend mode",
"standardModeDesc": "Standard mode: adds rules on top of the profile",
"scriptModeDesc": "Script mode: rewrites the whole profile with an extension script",
"addedRules": "Added rules",
"controlGlobalAddedRules": "Control global added rules",
"overrideScript": "Override script",
"quickActions": "Quick actions",
"rulePresetBlockQuic": "Block QUIC",
"rulePresetBlockStun": "Block STUN",
"rulePresetBlockDot": "Block DNS over TLS",
"rulePresetBlockLoopback": "Block proxy loopback",
"rulePresetSystemServicesDirect": "Apple and Microsoft direct",
"rulePresetBittorrentDirect": "BitTorrent direct",
"rulePresetLanDirect": "LAN direct",
"extendScript": "Extend script",
"goToConfigureScript": "Go to script configuration",
"goToConfigureAddedRules": "Go to added rules configuration",
"editGlobalRules": "Edit global rules",
"externalFetch": "External fetch",
"confirmForceCrashCore": "Are you sure you want to force crash the core?",
"confirmClearAllData": "Are you sure you want to clear all data?",
"loading": "Loading...",
"loading": "Loading…",
"yearsAgo": "{count, plural, =1{1 year ago} other{{count} years ago}}",
"monthsAgo": "{count, plural, =1{1 month ago} other{{count} months ago}}",
"daysAgo": "{count, plural, =1{1 day ago} other{{count} days ago}}",
"hoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}",
"minutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}",
"justNow": "Just now",
"yearsAgoShort": "{count, plural, other{{count}y ago}}",
"monthsAgoShort": "{count, plural, other{{count}mo ago}}",
"daysAgoShort": "{count, plural, other{{count}d ago}}",
"hoursAgoShort": "{count, plural, other{{count}h ago}}",
"minutesAgoShort": "{count, plural, other{{count}m ago}}",
"noLongerRemind": "Don't remind me again",
"accessControlSettings": "Access control settings",
"turnOn": "Turn on",
@@ -434,39 +548,45 @@
"restart": "Restart",
"speedStatistics": "Speed statistics",
"resetPageChangesTip": "This page has changes. Are you sure you want to reset?",
"overwriteTypeCustom": "Custom",
"overwriteTypeCustomDesc": "Custom mode: fully customize proxy groups and rules",
"customProfile": "Custom",
"customProfileDesc": "Assemble proxy groups and rules from app proxies and other profiles",
"customProfileResources": "Custom profile resources",
"customProfileResourcesDesc": "Custom profile resources hold proxies, rule providers, filters, and icon sets.",
"unknownNetworkError": "Unknown network error",
"restoreException": "Restore error",
"networkException": "Network error, please check your connection and try again",
"backupFromNewerVersion": "This backup comes from a newer version of the app. Update the app before restoring it",
"networkTimeoutError": "The request timed out. Check your network or proxy, then try again",
"networkHostLookupError": "Couldn't resolve the server address. Check that the URL is correct and DNS is working",
"networkConnectionError": "Couldn't connect to the server. Check your network connection or proxy settings",
"networkTlsError": "Secure connection failed. The server's certificate may be invalid, or the connection is being intercepted",
"networkCancelledError": "The request was cancelled",
"networkAccessDeniedError": "The server denied access (HTTP {code}). The link may have expired, or the credentials are wrong",
"networkNotFoundError": "Nothing was found at this address (HTTP {code}). Check that the URL is correct",
"networkRateLimitedError": "Too many requests (HTTP 429). Wait a moment and try again",
"networkServerError": "The server ran into a problem (HTTP {code}). Try again later",
"networkBadResponseError": "The server rejected the request (HTTP {code})",
"networkRequestFailed": "Network request failed: {detail}",
"invalidBackupFile": "Invalid backup file",
"pruneCache": "Prune cache",
"backupAndRestore": "Backup and restore",
"backupAndRestoreDesc": "Sync data via WebDAV or files",
"restore": "Restore",
"restoreSuccess": "Restore successful",
"restoreFromWebDAVDesc": "Restore data from WebDAV",
"restoreFromFileDesc": "Restore data from a file",
"restoreOnlyConfig": "Restore profiles only",
"restoreAllData": "Restore all data",
"addProfile": "Add profile",
"delayTest": "Delay test",
"proxyGroupEmpty": "Proxy group is empty",
"proxyGroupNameEmpty": "Proxy group name cannot be empty",
"proxyGroupNameDuplicate": "Duplicate proxy group name",
"confirmExitWindow": "Are you sure you want to exit the current window?",
"dataChangedSave": "Data changes detected. Save them?",
"selectProxyProviders": "Select proxy providers",
"proxyFilter": "Proxy filter",
"optional": "Optional",
"maxFailedTimes": "Max failures",
"testInterval": "Test interval",
"excludeProxyFilter": "Exclude proxy filter",
"excludeType": "Exclude type",
"excludeFilter": "Exclude filter",
"expectedStatus": "Expected status",
"selectProxies": "Select proxies",
"strategy": "Strategy",
"tolerance": "Tolerance",
"inputProxyGroupName": "Enter the proxy group name",
"helperCorruptTip": "Helper service unavailable; TUN mode cannot be enabled. Reinstall FlClash to restore it.",
"tunAuthorizationFailed": "TUN could not be authorized, so it was turned off.",
"sudoPasswordTitle": "Enter your password for sudo",
"coreBlockedByPolicyTip": "Windows refused to run FlClashCore.exe (error {code}). An app control policy such as Smart App Control or AppLocker blocks unsigned programs; allow FlClash in that policy or turn it off, then try again.",
"coreBlockedBySmartAppControlTip": "Windows Smart App Control blocked FlClashCore.exe because it is not signed. Open Windows Security → App & browser control → Smart App Control settings, choose Off, then start FlClash again. Smart App Control cannot be turned back on without reinstalling Windows.",
"hideFromList": "Hide from list",
@@ -479,7 +599,6 @@
"selectRuleSet": "Please select a rule set",
"splitStrategy": "Split strategy",
"selectSplitStrategy": "Please select a split strategy",
"selectSubRule": "Please select a sub-rule",
"noResolveHostname": "Don't resolve hostname",
"matchSourceIp": "Match source IP",
"basicInfo": "Basic info",
@@ -487,21 +606,11 @@
"proxyType": "Proxy type",
"basicStrategy": "Basic strategies",
"editProxy": "Edit proxy",
"includeAllProxyProviders": "Include all proxy providers",
"includeAllProxyProvidersTip": "When enabled, the imported proxy providers are overridden",
"addProxyProviders": "Add proxy providers",
"includeAllProxies": "Include all proxies",
"includeAllProxiesTip": "Imports all proxies outside proxy groups; extra proxy groups can be added below",
"proxiesEmpty": "Proxies are empty",
"addProxies": "Add proxies",
"addNodes": "Add nodes",
"addProxyGroup": "Add proxy group",
"editProxyGroup": "Edit proxy group",
"confirmOverwriteTip": "Confirming will overwrite existing data",
"configDataDetected": "Data detected in the configuration",
"quickFill": "Quick fill",
"iconUrl": "Icon URL",
"iconRecords": "Icon records",
"noRecords": "No records",
"custom": "Custom",
"ruleActionDomainDesc": "Match the full domain",
"ruleActionDomainSuffixDesc": "Match a domain suffix",
@@ -540,53 +649,51 @@
"ruleActionNotDesc": "Logical rule NOT",
"ruleActionSubRuleDesc": "Match into a sub-rule; mind the parentheses",
"ruleActionMatchDesc": "Match all requests, no conditions needed",
"subRuleEmpty": "Sub-rule is empty",
"proxyProvidersNotEmpty": "Proxy providers cannot be empty",
"contentNotEmpty": "Content cannot be empty",
"subRuleNotEmpty": "Sub-rule cannot be empty",
"splitStrategyNotEmpty": "Split strategy cannot be empty",
"proxyProvidersEmpty": "Proxy providers are empty",
"timeout": "Timeout",
"delayFailed": "Failed",
"invalidSubRule": "{subRule} is an invalid SUB_RULE",
"invalidPolicy": "{target} is an invalid policy",
"invalidProxyProvider": "{providerName} is an invalid proxy provider",
"invalidRuleSet": "{ruleSet} is an invalid rule set",
"invalidNetworkContent": "Only tcp or udp is supported",
"invalidRangeContent": "Enter numbers or ranges such as 80 or 8000-9000, separated by /",
"invalidDscpContent": "A DSCP mark cannot exceed 63",
"invalidCidrContent": "Enter an IP range in CIDR form such as 192.168.0.0/16",
"invalidPortRangeContent": "Enter a port or a range such as 443 or 8000-9000",
"invalidHostContent": "Enter a domain or an IP address",
"invalidListenContent": "Enter an address and port such as 0.0.0.0:1053",
"invalidProxy": "{proxyName} is an invalid proxy",
"proxyGroupDetectedAbnormal": "The current proxy group is abnormal",
"proxyProviderDetectedAbnormal": "The selected proxy providers are abnormal",
"proxyDetectedAbnormal": "The selected proxies are abnormal",
"createProfile": "Create profile",
"locationPermissionRequired": "Location permission required",
"locationPermissionGuide": "1. Open System Settings > Privacy & Security\n2. Choose Location Services\n3. Find and check {appName} in the list\n\nWhen you are done, return to the app to continue. Thank you for your cooperation.",
"prerequisites": "Prerequisites",
"ignoreBatteryOptimization": "Ignore battery optimization",
"batteryOptimizationDesc": "To keep the app running in the background, disable battery optimization for it. Tap to open settings.",
"batteryOptimizationStatusTip": "Due to system limitations, the battery optimization status cannot be read correctly while running",
"batteryOptimizationDesc": "Ignore battery optimization keeps the app running in the background.",
"locationPermission": "Location permission",
"locationPermissionDesc": "The system requires location permission to read the Wi-Fi name. On Android choose \"Allow all the time\", otherwise the Wi-Fi name cannot be read while the app is in the background.",
"excludeSsids": "Exclude SSIDs",
"excludeSsidsDesc": "When connected to Wi-Fi with an excluded SSID, the app's running state switches automatically",
"excludeSsidsDesc": "When connected to Wi-Fi with an excluded SSID, the app's running state switches automatically.",
"ssidsEmpty": "SSIDs are empty",
"onDemand": "On demand",
"onDemandDesc": "Configure the app's running state for specific scenarios",
"onDemandDesc": "On demand pauses the proxy on excluded Wi-Fi.",
"locationPermissionDeniedMessage": "Location permission was denied, so the current Wi-Fi name cannot be read. Please enable location permission manually in system settings.",
"addSsid": "Add SSID",
"editSsid": "Edit SSID",
"authorized": "Authorized",
"tapToAuthorize": "Tap to authorize",
"suspended": "Suspended...",
"geoOptions": "Geo options",
"geoAutoUpdate": "Auto update",
"geoAutoUpdateInterval": "Auto-update interval",
"geoAutoUpdateIntervalTip": "The auto-update interval must be greater than 0",
"suspended": "Suspended…",
"resourceUpdateInterval": "Auto-update interval",
"resourceUpdateIntervalTip": "The auto-update interval must be greater than 0",
"hours": "hours",
"hoursCount": "{count, plural, =1{1 hour} other{{count} hours}}",
"geoResources": "Geo resources",
"minutes": "minutes",
"geoResources": "Geo",
"geoSkipped": "{name} is already up to date",
"geoUpdated": "{name} updated",
"secondsCount": "{count, plural, =1{1 second} other{{count} seconds}}",
"entriesCount": "{count, plural, =1{1 entry} other{{count} entries}}",
"proxiesCount": "{count, plural, =1{1 proxy} other{{count} proxies}}",
"rulesCount": "{count, plural, =1{1 rule} other{{count} rules}}",
"proxiesCount": "{count, plural, =1{1 proxy} other{{label} proxies}}",
"rulesCount": "{count, plural, =1{1 rule} other{{label} rules}}",
"proxyGroupsCount": "{count, plural, =1{1 proxy group} other{{label} proxy groups}}",
"changelogBreaking": "Breaking changes",
"changelogFeatures": "New features",
"changelogFixes": "Bug fixes",
@@ -600,7 +707,6 @@
"pinWindow": "Pin window",
"unpinWindow": "Unpin window",
"exitFullScreen": "Exit full screen",
"toggleLabel": "Toggle labels",
"torch": "Flashlight",
"pickFromAlbum": "Choose from album",
"blockConnection": "Block connection",
@@ -611,11 +717,227 @@
"previousMatch": "Previous match",
"nextMatch": "Next match",
"clearSearch": "Clear search",
"addWidget": "Add widget",
"noSearchResults": "No matching results",
"showPassword": "Show password",
"hidePassword": "Hide password",
"authorize": "Authorize",
"installedAppsPermissionRequired": "App list permission required",
"installedAppsPermissionDesc": "This system hides the installed app list until the permission is granted. Authorize it to configure the per-app proxy.",
"installedAppsPermissionDeniedMessage": "The app list permission was denied, so installed apps cannot be listed. Please grant it manually in system settings."
"installedAppsPermissionDeniedMessage": "The app list permission was denied, so installed apps cannot be listed. Please grant it manually in system settings.",
"outboundIp": "Outbound IP",
"ipAddress": "IP address",
"ipQualityLevel": "Level",
"ipQualityGood": "Good",
"ipQualityNormal": "Normal",
"ipQualityRisky": "Risky",
"ipType": "Type",
"ipTypeResidential": "Residential",
"ipTypeMobile": "Mobile network",
"ipTypeBusiness": "Business",
"ipTypeHosting": "Data center",
"ipFlags": "Flags",
"ipFlagProxy": "Proxy",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "Abuse history",
"ipOrganization": "Organization",
"ipAsn": "ASN",
"ipQualitySource": "Answered by",
"ipQualitySources": "Sources",
"ipQualityFailed": "Couldn't determine the IP type",
"ipQualityRetry": "Check again",
"ipSourceNoType": "No type",
"ipSourceRateLimited": "Rate limited",
"ipSourceIpMismatch": "Different outbound IP",
"hideIp": "Hide IP",
"editorUnavailable": "Editor unavailable",
"replace": "Replace",
"replaceAll": "Replace all",
"cameraPermissionRequired": "Camera permission required",
"cameraPermissionDesc": "Allow camera access in system settings to scan QR codes, or choose a QR code image from the album.",
"cameraUnavailable": "Camera unavailable",
"retry": "Retry",
"invalidProfileQrcode": "This QR code doesn't contain a profile link",
"dnsQueries": "DNS queries",
"recordType": "Record type",
"answers": "Answers",
"responseCode": "Response code",
"error": "Error",
"initiator": "Initiator",
"cache": "Cache",
"yes": "Yes",
"no": "No",
"nonTextProviderFile": "This external resource is not a text file",
"addCustomProxy": "Add proxy",
"definitionNotMap": "The configuration must be a YAML mapping with a name and a type",
"ruleTextInvalid": "The rule must start with a supported type, such as DOMAIN,example.com,DIRECT",
"ruleListInvalid": "The rules must be a YAML list with one rule per item, such as - DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "No proxy could be read from the links",
"shareLinkUnreadable": "No proxy could be read from {link}",
"proxiesProfileLabel": "{name} and others ({count} proxies)",
"singleShareLinkOnly": "Enter one link here; edit several from the quick edit of the list",
"username": "Username",
"emptyFallback": "Empty fallback",
"defaultSelected": "Default selection",
"hashByInUser": "Hash by inbound user",
"appProxiesEmpty": "No local proxies",
"nodes": "Nodes",
"localProxies": "Local proxies",
"customIssueEmptyName": "The name is empty",
"customIssueReservedName": "{name} is a built-in policy name and cannot be used here",
"customIssueDuplicateName": "The name {name} is already used by another proxy or proxy group",
"customIssueCoreRejected": "The core cannot parse this proxy: {message}",
"customIssueMissingProxies": "These proxies or policies do not exist: {names}",
"customIssueMissingProviders": "These proxy providers do not exist: {names}",
"customIssueNoProxySource": "No proxies or proxy providers are selected, so the core rejects this group",
"customIssueGroupLoop": "Proxy groups reference each other in a loop: {path}",
"customIssueInvalidEmptyFallback": "The empty fallback must be a proxy, but {name} does not exist or is a proxy group",
"customIssueInvalidFilter": "{name} is not a valid regex: {message}",
"customIssueMissingDialer": "The dialer proxy {name} does not exist",
"customIssueDialerLoop": "{proxy} dials through {target}, which leads back to {proxy}, so the dial loops",
"dialerProxyLoopStopped": "Dialing through {name} leads back to the proxy that uses it, so the connection was stopped",
"customIssuesSummary": "{count} items have problems, and applying this profile may fail",
"providerInUse": "{label} is still used by {profiles}, which will not work properly once it is deleted. Are you sure you want to delete it?",
"proxyGroupInUse": "{label} is still named by rules, proxy groups, dialer proxies, DNS or NTP in this profile, which stop working once it is deleted. Are you sure you want to delete it?",
"proxiesRemovedTip": "These proxies will be deleted: {removed}. A line whose name changed counts as a new proxy, and custom profiles naming the deleted ones will flag them as missing. Apply anyway?",
"proxiesReplacedTip": "These proxies will be deleted: {removed}. These will be added: {added}. A line whose name changed counts as a new proxy, and custom profiles naming the deleted ones will flag them as missing. Apply anyway?",
"fontSize": "Size",
"large": "Large",
"extraLarge": "Extra large",
"switchProfile": "Switch profile",
"lastUpdated": "Last updated",
"filters": "Filters",
"label": "Label",
"regex": "Regex",
"matchCase": "Match case",
"iconSets": "Icon sets",
"invalidIconSet": "Not a valid icon set",
"recommendedIcons": "Suggested",
"localImage": "Local image",
"iconSetsEmptyTip": "No icon sets yet. Add them in Custom profile resources → Icon sets",
"outbound": "Outbound",
"strictRoute": "Strict route",
"congestionController": "TCP congestion control",
"congestionControllerDesc": "TCP congestion control works only with the mips stack.",
"routeExcludeAddress": "Route exclude addresses",
"keepAliveIdle": "TCP keep-alive idle",
"disableKeepAlive": "Disable TCP keep-alive",
"routingMark": "Routing mark",
"strictRouteDesc": "Strict route keeps traffic such as DNS from bypassing TUN, and other devices can no longer reach this one.",
"initFailed": "Startup failed",
"initFailedTip": "FlClash ran into an error while starting and can't continue. Copy the details to report the problem. If it keeps happening, clearing the data usually lets the app start again.",
"errorDetails": "Error details",
"stackTrace": "Stack trace",
"clearDataAndExitTip": "This deletes all profiles, settings, and local data, then closes the app. Open it again to start fresh.",
"clearDataFailed": "Couldn't clear the data: {detail}",
"bypassPrivateRoute": "Bypass private addresses",
"bypassPrivateRouteDesc": "With Bypass private addresses on, LAN, loopback and multicast addresses skip TUN.",
"disableIcmpForwarding": "Disable ICMP forwarding",
"disableIcmpForwardingDesc": "With ICMP forwarding disabled, TUN answers ping itself, so ping no longer shows the real latency.",
"excludeInterface": "Excluded interfaces",
"excludeInterfaceDesc": "Traffic arriving on excluded interfaces, such as docker0, skips TUN.",
"includeInterface": "Included interfaces",
"includeInterfaceDesc": "Only traffic arriving on included interfaces goes through TUN, and excluded interfaces no longer apply.",
"includeUid": "Included user IDs",
"includeUidDesc": "Only traffic from processes of included user IDs goes through TUN.",
"excludeUid": "Excluded user IDs",
"excludeUidDesc": "Traffic from processes of excluded user IDs skips TUN.",
"excludeSrcPort": "Excluded source ports",
"excludeSrcPortDesc": "Traffic from excluded source ports skips TUN.",
"excludeDstPort": "Excluded destination ports",
"excludeDstPortDesc": "Traffic to excluded destination ports skips TUN.",
"udpTimeout": "UDP timeout",
"udpTimeoutDesc": "The UDP timeout is in seconds and works only with the gvisor, system and mixed stacks.",
"dnsHijackDesc": "Without this entry, TUN hijacks every DNS query to port 53 (any:53).",
"tunOverrideDesc": "Entries added here replace the TUN settings of every profile; anything not added keeps the profile's own setting. Entries a custom profile adds itself take precedence.",
"profileTunSettingsDesc": "Applies to this profile only and takes precedence over the TUN override entries in the network settings.",
"settingsCount": "{count, plural, =1{1 setting} other{{count} settings}}",
"sectionsInEffect": "{count} in effect",
"itemsCount": "{count, plural, =1{1 item} other{{count} items}}",
"additionalPrefix": "Name prefix",
"additionalSuffix": "Name suffix",
"skipCertVerify": "Skip certificate verification",
"ipVersion": "IP version",
"addFilters": "Add filters",
"failedItem": "{label}: {message}",
"failedCount": "{count} items failed",
"importConfigInvalid": "This file is not a config that can be imported",
"importConfigReplaceTip": "Importing replaces the proxy groups, rules and settings this profile has now",
"networkName": "Network name",
"networkSecret": "Network secret",
"peers": "Peers",
"virtualIpv4": "Virtual IPv4",
"hostname": "Hostname",
"exitNodes": "Exit nodes",
"cipher": "Cipher",
"protocol": "Protocol",
"protocolParam": "Protocol parameter",
"obfs": "Obfuscation",
"obfsParam": "Obfuscation parameter",
"obfsPassword": "Obfuscation password",
"obfsMode": "Obfuscation mode",
"obfsHost": "Obfuscation host",
"plugin": "Plugin",
"pluginMode": "Plugin mode",
"pluginHost": "Plugin host",
"privateKey": "Private key",
"publicKey": "Public key",
"privateKeyPassphrase": "Private key passphrase",
"preSharedKey": "Pre-shared key",
"secretKey": "Key",
"transport": "Transport",
"uploadBandwidth": "Upload bandwidth",
"downloadBandwidth": "Download bandwidth",
"protocolVersion": "Protocol version",
"portHopping": "Port hopping",
"hopInterval": "Hop interval",
"congestionControl": "Congestion control",
"udpRelayMode": "UDP relay mode",
"allowedIps": "Allowed IPs",
"remoteDnsResolve": "Remote DNS resolution",
"clientFingerprint": "Client fingerprint",
"required": "Required",
"ageSecretKey": "Age secret key",
"ageEncryptedProfile": "Encrypted profile",
"ageSecretKeyRequiredTip": "This profile is encrypted with age and needs an age secret key to decrypt",
"currentOutboundDesc": "Holds the start button and can't be removed",
"running": "Running",
"notRunning": "Not running",
"trafficPeak": "Peak {value}",
"trafficTotal": "Total {value}",
"ipQualityShort": "Quality",
"ipTypeUnknown": "Unknown type",
"tapIconToRetry": "Tap the icon to retry",
"networkTakeover": "Traffic takeover",
"portValue": "Port {port}",
"serviceAbnormalCount": "{count, plural, =1{1 issue} other{{count} issues}}",
"serviceAllAvailable": "All available",
"serviceShownOnDashboard": "Shown on the dashboard",
"groupTimeoutCount": "{count} timed out",
"groupCountUnit": "groups",
"groupCount": "{count, plural, =1{1 group} other{{count} groups}}",
"proxyGroupsAfterStart": "Groups show up here once the core starts",
"usedShort": "used",
"profileUpdatedAt": "Updated {time}",
"profileExpiresIn": "Expires {date}, {days, plural, =1{1 day} other{{days} days}} left",
"profileDaysLeft": "{days, plural, =1{1 day left} other{{days} days left}}",
"target": "Target",
"outboundExit": "Exit",
"duration": "Duration",
"traffic": "Traffic",
"totalDownload": "Downloaded",
"totalUpload": "Uploaded",
"initiatorShort": "From",
"result": "Result",
"elapsed": "Took",
"connectionStats": "Connection stats",
"connectionStatsDesc": "Live connections, recent requests and DNS queries",
"memoryInfoDesc": "Memory the app and the core use",
"editDashboard": "Edit dashboard",
"done": "Done",
"dashboardAddableSection": "Available to add",
"customImportDroppedSubRules": "{count, plural, =1{1 SUB-RULE rule was dropped, since custom profiles do not support sub-rules} other{{label} SUB-RULE rules were dropped, since custom profiles do not support sub-rules}}",
"autoInclude": "Auto-include",
"allReferencedProxies": "All referenced proxies",
"allReferencedProxyProviders": "All referenced proxy providers"
}
+943
View File
@@ -0,0 +1,943 @@
{
"rule": "Regla",
"rules": "Reglas",
"global": "Global",
"direct": "Directo",
"dashboard": "Panel",
"proxies": "Proxies",
"profile": "Perfil",
"profiles": "Perfiles",
"tools": "Herramientas",
"logs": "Registros",
"resources": "Recursos",
"trafficUsage": "Uso de tráfico",
"networkSpeed": "Velocidad de red",
"outboundMode": "Modo de salida",
"serviceStatus": "Estado del servicio",
"serviceRestricted": "Acceso restringido",
"serviceAvailable": "Disponible",
"serviceUnavailable": "No disponible",
"serviceFailed": "Error en la comprobación",
"serviceDisallowedIsp": "ISP no permitido",
"serviceBlocked": "Bloqueado",
"serviceUnsupportedRegion": "Región no compatible",
"serviceOriginalsOnly": "Solo originales",
"serviceComingSoon": "Próximamente",
"servicePending": "Sin comprobar",
"serviceManage": "Gestionar servicios",
"serviceCheck": "Comprobar",
"serviceCheckAll": "Comprobar todo",
"serviceCheckedAt": "Comprobado a las {time}",
"upload": "Subida",
"download": "Descarga",
"usedTraffic": "Tráfico usado",
"totalTraffic": "Tráfico total",
"expireTime": "Fecha de vencimiento",
"nullProfileDesc": "Añade un perfil para empezar",
"settings": "Ajustes",
"language": "Idioma",
"defaultText": "Predeterminado",
"more": "Más",
"other": "Otros",
"about": "Acerca de",
"en": "Inglés",
"es": "Español",
"fr": "Francés",
"ja": "Japonés",
"ko": "Coreano",
"ru": "Ruso",
"zhCN": "Chino simplificado",
"theme": "Tema",
"extend": "Extensión",
"runtimeConfig": "Configuración de ejecución",
"allowLan": "Permitir LAN",
"tun": "TUN",
"tunDesc": "TUN solo funciona en modo administrador.",
"minimizeOnExit": "Minimizar al salir",
"autoLaunch": "Inicio automático",
"silentLaunch": "Inicio silencioso",
"silentLaunchDesc": "El inicio silencioso arranca sin mostrar la ventana.",
"autoRun": "Ejecución automática",
"autoRunDesc": "La ejecución automática inicia el proxy al abrir la app.",
"logcat": "Logcat",
"logcatDesc": "Si desactivas Logcat, se oculta la entrada de registros.",
"autoCheckUpdate": "Buscar actualizaciones automáticamente",
"checkCertificate": "Verificar certificados TLS",
"checkCertificateDesc": "Con la verificación TLS desactivada, las suscripciones y las copias de seguridad quedan expuestas a ataques de intermediario.",
"accessControl": "Control de acceso",
"accessControlDesc": "El control de acceso elige qué apps usan el proxy.",
"edit": "Editar",
"confirm": "Confirmar",
"update": "Actualizar",
"add": "Añadir",
"save": "Guardar",
"delete": "Eliminar",
"seconds": "segundos",
"qrcode": "Código QR",
"qrcodeDesc": "Escanea un código QR para obtener un perfil",
"url": "URL",
"link": "Enlace",
"linkDesc": "Obtén un perfil desde un enlace de suscripción o un enlace para compartir proxies",
"file": "Archivo",
"fileDesc": "Sube un archivo de perfil directamente",
"name": "Nombre",
"profileNameNullValidationDesc": "Introduce el nombre del perfil",
"rename": "Renombrar",
"profileUrlNullValidationDesc": "Introduce la URL del perfil",
"profileUrlInvalidValidationDesc": "Introduce una URL de perfil válida",
"autoUpdate": "Actualización automática",
"autoUpdateInterval": "Intervalo de actualización automática",
"profileAutoUpdateIntervalNullValidationDesc": "Introduce el intervalo de actualización automática",
"profileAutoUpdateIntervalInvalidValidationDesc": "Introduce un intervalo válido",
"themeMode": "Modo del tema",
"themeColor": "Color del tema",
"preview": "Vista previa",
"auto": "Automático",
"light": "Claro",
"dark": "Oscuro",
"importFromLink": "Importar desde enlace",
"submit": "Enviar",
"createProfileFromUrlTip": "¿Quieres crear un perfil a partir de {url}?",
"create": "Crear",
"pleaseUploadValidQrcode": "Sube un código QR válido",
"blacklistMode": "Modo lista negra",
"whitelistMode": "Modo lista blanca",
"selectAll": "Seleccionar todo",
"cancelSelectAll": "Deseleccionar todo",
"appAccessControl": "Control de acceso de apps",
"accessControlAllowDesc": "Solo las apps seleccionadas usan la VPN",
"accessControlNotAllowDesc": "Las apps seleccionadas quedan excluidas de la VPN",
"accessControlDisabledDesc": "El control de acceso de apps está desactivado",
"selected": "Seleccionado",
"port": "Puerto",
"logLevel": "Nivel de registro",
"show": "Mostrar",
"exit": "Salir",
"systemProxy": "Proxy del sistema",
"safeMode": "Modo seguro",
"safeModeAppTitle": "{appName} (Modo seguro)",
"project": "Proyecto",
"core": "Núcleo",
"tabAnimation": "Animación de pestañas",
"desc": "Un cliente proxy multiplataforma basado en ClashMeta, sencillo de usar, de código abierto y sin anuncios.",
"startVpn": "Iniciando VPN…",
"stopVpn": "Deteniendo VPN…",
"localNetworkDeniedTip": "Permiso de red local denegado: con la pila gvisor, la LAN no es accesible.",
"compatible": "Modo de compatibilidad",
"notSelectedTip": "No se puede seleccionar el grupo de proxies actual",
"changeProxyFailedTip": "No se pudo cambiar de proxy; se ha restaurado la selección anterior",
"databaseWriteFailedTip": "No se pudo guardar el cambio; se ha revertido",
"tip": "Consejo",
"account": "Cuenta",
"backup": "Copia de seguridad",
"backupSuccess": "Copia de seguridad completada",
"noInfo": "Sin información",
"pleaseBindWebDAV": "Vincula WebDAV",
"bind": "Vincular",
"connectivity": "Conectividad: ",
"webDAVConfiguration": "Configuración de WebDAV",
"address": "Dirección",
"addressHelp": "Dirección del servidor WebDAV",
"addressTip": "Introduce una dirección WebDAV válida",
"password": "Contraseña",
"authentication": "Autenticación",
"authenticationDesc": "La autenticación pide usuario y contraseña en el puerto del proxy local.",
"authenticationSystemProxyDesc": "El proxy del sistema no se aplica mientras la autenticación esté activada.",
"checkUpdate": "Buscar actualizaciones",
"discoverNewVersion": "Nueva versión disponible",
"checkUpdateError": "La app ya está actualizada",
"goDownload": "Descargar",
"unknown": "Desconocido",
"country": "Región",
"search": "Buscar",
"allowBypass": "Permitir que las apps eludan la VPN",
"externalController": "Controlador externo",
"externalControllerDesc": "El controlador externo permite controlar el núcleo en el puerto 9090.",
"externalControllerSecret": "Clave del controlador externo",
"ipv6Desc": "El tráfico IPv6 solo se recibe mientras IPv6 esté activado.",
"app": "App",
"general": "General",
"startupAndBackground": "Inicio y segundo plano",
"requestsAndUpdates": "Solicitudes y actualizaciones",
"logsAndDiagnostics": "Registros y diagnóstico",
"userAgent": "User-Agent",
"unifiedDelay": "Latencia unificada",
"tcpConcurrent": "TCP concurrente",
"geodataLoader": "Modo geo de poca memoria",
"requests": "Solicitudes",
"recentRequests": "Solicitudes recientes",
"findProcessMode": "Buscar proceso",
"init": "Inicio",
"infiniteTime": "No caduca",
"connections": "Conexiones",
"liveConnections": "Conexiones activas",
"inbound": "Entrada",
"intranetIP": "IP de intranet",
"view": "Ver",
"cut": "Cortar",
"copy": "Copiar",
"clipboardWriteFailed": "No se pudo copiar al portapapeles. Puede que la selección sea demasiado grande",
"paste": "Pegar",
"testUrl": "URL de prueba",
"sync": "Sincronizar",
"exclude": "Ocultar de aplicaciones recientes",
"compact": "Compacto",
"tab": "Pestaña",
"list": "Lista",
"delay": "Latencia",
"style": "Estilo",
"size": "Tamaño",
"sort": "Ordenar",
"columns": "Columnas",
"proxyGroup": "Grupo de proxies",
"go": "Ir",
"externalLink": "Enlace externo",
"otherContributors": "Otros colaboradores",
"appIconDesign": "Diseño del icono de la app",
"autoCloseConnections": "Cerrar conexiones automáticamente",
"autoCloseConnectionsDesc": "Cerrar conexiones automáticamente corta las conexiones abiertas al cambiar de proxy.",
"onlyStatisticsProxy": "Contar solo el tráfico del proxy",
"showNotificationStopAction": "Botón de detener en la notificación",
"pureBlackMode": "Modo negro puro",
"sidebarBlur": "Desenfoque de la barra lateral",
"sidebarBlurDesc": "Muestra el escritorio desenfocado detrás de la ventana a través de la barra lateral",
"liquidGlass": "Liquid Glass",
"keepAliveIntervalDesc": "Intervalo de keep-alive TCP",
"entries": " entradas",
"local": "Local",
"remote": "Remoto",
"mode": "Modo",
"time": "Hora",
"source": "Origen",
"action": "Acción",
"intelligentSelected": "Selección inteligente",
"clipboardImport": "Importar desde el portapapeles",
"clipboardExport": "Exportar al portapapeles",
"layout": "Diseño",
"tight": "Ajustado",
"standard": "Estándar",
"ultra": "Ultra",
"loose": "Amplio",
"profilesSort": "Ordenar perfiles",
"start": "Iniciar",
"runTime": "Tiempo de ejecución",
"stop": "Detener",
"key": "Clave",
"value": "Valor",
"vpnEnableDesc": "La VPN enruta automáticamente todo el tráfico del sistema a través de VpnService.",
"options": "Opciones",
"enabled": "Activado",
"disabled": "Desactivado",
"loopback": "Exención de loopback de UWP",
"providers": "Recursos externos",
"proxyProviders": "Proveedores de proxies",
"ruleProviders": "Proveedores de reglas",
"providerUrlTip": "Solo se admiten proveedores remotos",
"ruleSetEmptyTip": "El conjunto de reglas no tiene entradas utilizables",
"ruleSetMixedTip": "El conjunto de reglas mezcla dominios y rangos de IP, por lo que no se puede determinar su tipo",
"ruleSetInvalidTip": "No se puede leer este archivo mrs",
"subscriptionInfo": "Información de la suscripción",
"ntpStatusDesc": "NTP obtiene la hora de un servidor NTP en lugar del reloj del sistema.",
"server": "Servidor",
"ntpInterval": "Intervalo de sincronización (minutos)",
"dialerProxy": "Proxy de marcación",
"dialerProxyDesc": "El proxy de marcación es la salida que se usa para llegar al servidor NTP.",
"writeToSystem": "Escribir en el sistema",
"writeToSystemDesc": "Escribir en el sistema también ajusta el reloj del sistema; Android lo ignora.",
"sniffer": "Sniffer",
"snifferStatusDesc": "El sniffer lee el dominio del tráfico TLS, HTTP y QUIC para que las reglas puedan aplicarse.",
"overrideDestination": "Sobrescribir destino",
"overrideDestinationDesc": "Sobrescribir destino se conecta al dominio detectado en lugar de a la dirección original.",
"forceDnsMapping": "Forzar mapeo DNS",
"forceDnsMappingDesc": "Forzar mapeo DNS también analiza las conexiones cuyo dominio provino de un mapeo DNS.",
"parsePureIp": "Analizar IP pura",
"parsePureIpDesc": "Analizar IP pura analiza las conexiones que solo llevan una dirección IP.",
"forceDomain": "Forzar dominios",
"forceDomainDesc": "Los dominios forzados también se analizan; las demás conexiones que ya tienen dominio se dejan como están.",
"skipDomain": "Omitir dominios",
"skipDomainDesc": "Un dominio detectado que esté en la lista de dominios omitidos no se usa.",
"skipSrcAddress": "Omitir direcciones de origen",
"skipDstAddress": "Omitir direcciones de destino",
"sniffProtocols": "Protocolos",
"ports": "Puertos",
"overrideEntries": "Entradas de sobrescritura",
"addOverrideEntry": "Añadir entrada de sobrescritura",
"dnsOverrideDesc": "Las entradas añadidas aquí sustituyen el DNS de todos los perfiles de URL y de archivo; lo que no añadas conserva el ajuste del propio perfil. Los perfiles personalizados definen su DNS en su contenido.",
"settingEntries": "ajustes",
"addSettingEntry": "Añadir ajuste",
"profileSettingsDesc": "Se aplica solo a este perfil; lo que no añadas usa su valor predeterminado.",
"healthCheck": "Comprobación de estado",
"discardChanges": "¿Descartar los cambios?",
"quickEdit": "Edición rápida",
"status": "Estado",
"statusDesc": "Con DNS desactivado, se usa el DNS del sistema.",
"respectRules": "Respetar reglas",
"respectRulesDesc": "Respetar reglas enruta las conexiones DNS según las reglas y requiere un Proxy Server Nameserver.",
"dnsMode": "Modo DNS",
"fakeipRange": "Rango Fake-IP",
"fakeipFilter": "Filtro Fake-IP",
"useHosts": "Usar hosts",
"useSystemHosts": "Usar hosts del sistema",
"fallbackFilter": "Filtro de respaldo",
"domain": "Dominio",
"listenRoutingMark": "Marca de enrutamiento de escucha",
"listenRoutingMarkDesc": "La marca de enrutamiento de escucha solo funciona en Linux.",
"ipv6Timeout": "Tiempo de espera IPv6 (ms)",
"cacheAlgorithm": "Algoritmo de caché",
"cacheMaxSize": "Tamaño de caché",
"fakeipRange6": "Rango Fake-IP (IPv6)",
"fakeipFilterMode": "Modo de filtro Fake-IP",
"fakeipFilterModeDesc": "En el modo de filtro Fake-IP, la lista negra excluye las coincidencias, la lista blanca aplica Fake-IP solo a las coincidencias y rule coincide según las reglas.",
"fakeipTtl": "TTL de Fake-IP",
"reset": "Restablecer",
"actionView": "Mostrar/Ocultar",
"actionStart": "Iniciar/Detener",
"actionMode": "Cambiar modo",
"actionProxy": "Proxy del sistema",
"actionTun": "TUN",
"actionRuleMode": "Modo regla",
"actionGlobalMode": "Modo global",
"actionDirectMode": "Modo directo",
"actionDelayTest": "Probar la latencia de todos",
"actionUpdateProfiles": "Actualizar perfiles",
"disclaimer": "Aviso legal",
"disclaimerDesc": "Antes de usar FlClash (“el Software”), lee con atención esta declaración y asegúrate de entenderla por completo. Pulsar “Aceptar” significa que has leído, comprendido y aceptado todos los términos siguientes. Si no estás de acuerdo, pulsa “Salir” y deja de usar el Software.",
"disclaimerSoftwareTitle": "Naturaleza del software",
"disclaimerSoftwareContent": "El Software es un cliente proxy de red de código abierto basado en el núcleo Clash.Meta (mihomo). Solo ofrece herramientas locales como la gestión de configuración, el enrutamiento basado en reglas y el reenvío de tráfico.",
"disclaimerNoServiceStatement": "El Software en sí no proporciona ningún servidor proxy, nodo, suscripción ni servicio de acceso a la red, y no mantiene ninguna relación de asociación, representación ni garantía con ningún proveedor de dichos servicios.",
"disclaimerUsageTitle": "Restricciones de uso",
"disclaimerUsageContent": "El Software está destinado únicamente a usos no comerciales, como el aprendizaje, el intercambio y la investigación técnica. Queda estrictamente prohibido usarlo con cualquier fin comercial, incluidos, entre otros, la distribución de pago, la venta en paquete, su uso como parte de un servicio comercial o hacer negocio en nombre del Software. Cualquier actividad comercial no guarda relación con el Software ni con sus desarrolladores.\n\nQueda estrictamente prohibido usar el Software para cualquier fin que infrinja las leyes y normativas de tu país o región, incluidos, entre otros, eludir restricciones de acceso a la red impuestas legalmente, difundir contenido ilegal, lanzar ataques a la red o vulnerar los derechos legítimos de otras personas.",
"disclaimerResponsibilityTitle": "Tu responsabilidad",
"disclaimerResponsibilityContent": "Eres responsable de asegurarte de que el uso del Software sea legal donde vives, y asumes tú solo la responsabilidad legal de todo lo que hagas con él y de sus consecuencias.\n\nLas suscripciones, los nodos y las configuraciones que importas son decisión tuya. Que su origen sea legal, su contenido seguro y su servicio fiable es un asunto entre tú y sus proveedores.",
"disclaimerThirdPartyTitle": "Contenido de terceros",
"disclaimerThirdPartyContent": "Los enlaces de suscripción, los archivos de configuración, los conjuntos de reglas, los scripts, los recursos externos y los enlaces externos los proporcionan terceros. Los desarrolladores no pueden revisar ni garantizar, ni lo hacen, su legalidad, exactitud, seguridad o disponibilidad.\n\nCualquier fuga de datos, pérdida económica, bloqueo de cuenta u otra pérdida causada por contenido de terceros debe resolverse entre tú y el tercero; los desarrolladores no asumen ninguna responsabilidad por ello.",
"disclaimerWarrantyTitle": "Sin garantía",
"disclaimerWarrantyContent": "El Software se proporciona “tal cual” y “según disponibilidad”, sin garantía de ningún tipo, expresa o implícita, incluidas, entre otras, las garantías de comerciabilidad, idoneidad para un fin concreto, no infracción, disponibilidad ininterrumpida, ausencia de errores o ausencia de vulnerabilidades de seguridad.\n\nLos desarrolladores no garantizan que el Software satisfaga tus necesidades ni que funcione sin interrupciones ni errores.",
"disclaimerLiabilityTitle": "Limitación de responsabilidad",
"disclaimerLiabilityContent": "En la máxima medida permitida por la ley aplicable, ni los desarrolladores ni ningún colaborador serán responsables de ningún daño directo, indirecto, incidental, especial, punitivo o emergente derivado del uso o de la imposibilidad de usar el Software, incluidos, entre otros, la pérdida de datos, los daños al dispositivo, los fallos de red, la interrupción de la actividad, el lucro cesante o cualquier disputa legal resultante, incluso si se les hubiera advertido de la posibilidad de tales daños.",
"disclaimerPrivacyTitle": "Recopilación de datos y privacidad",
"disclaimerPrivacyContent": "El Software no recopila ni sube tus URL de suscripción, los detalles de los nodos, el contenido de las configuraciones, los sitios web visitados, los registros de conexión, el contenido del tráfico ni los registros. Estos datos permanecen en tu dispositivo y los desarrolladores no tienen acceso a ellos.\n\nEl Software solo accede a la red cuando usas una función que lo necesita, como obtener la URL de suscripción que indicaste al actualizar un perfil o conectarse a GitHub al buscar actualizaciones.\n\nEl Software no incluye ningún servicio de analítica ni de informes de fallos en ninguna plataforma.",
"disclaimerLicenseTitle": "Licencia de código abierto",
"disclaimerLicenseContent": "El Software es de código abierto bajo la licencia GPL-3.0. Puedes usarlo, modificarlo y distribuirlo libremente siempre que cumplas dicha licencia, que exige que las obras derivadas se publiquen también bajo GPL-3.0 y que se conserven los avisos de derechos de autor originales.\n\nLos componentes de terceros del Software, incluido el núcleo Clash.Meta, siguen sus propias licencias. Los autores originales no se hacen responsables de ningún problema derivado de versiones modificadas o redistribuidas.",
"disclaimerChangesTitle": "Cambios en esta declaración",
"disclaimerChangesContent": "Los desarrolladores pueden revisar esta declaración con cualquier versión, y la revisión entra en vigor cuando se publique dicha versión. Seguir usando el Software después de actualizar implica que aceptas la declaración revisada.",
"disclaimerAcceptTitle": "Aceptación",
"disclaimerAcceptContent": "Al instalar, copiar o usar el Software, se considera que has leído y aceptado esta declaración en su totalidad. Si no estás de acuerdo con alguno de sus términos, deja de usar el Software y desinstálalo de inmediato.",
"disclaimerRestateTitle": "Repite la declaración",
"disclaimerRestateTip": "Para confirmar que lo entiendes, escribe la siguiente declaración completa en el campo de abajo:",
"disclaimerRestateHint": "Escribe la declaración anterior",
"disclaimerRestateMismatch": "No coincide con la declaración",
"disclaimerAcknowledgeTitle": "Confirma la declaración",
"disclaimerAcknowledgeTip": "Para confirmar que lo entiendes, lee la siguiente declaración y marca la casilla de abajo:",
"disclaimerAcknowledgeLabel": "He leído y entiendo esta declaración",
"agree": "Aceptar",
"hotkeyManagement": "Gestión de atajos",
"hotkeyDesc": "Los atajos globales funcionan incluso con la ventana oculta. Toca una acción para grabar su combinación de teclas.",
"hotkeyNotSet": "Sin asignar",
"pressKeyboard": "Pulsa una combinación de teclas",
"hotkeyNeedsModifier": "Incluye al menos una de estas teclas: {modifiers}",
"hotkeyConflictWith": "Ya lo usa “{action}”. Al guardar, pasará a este.",
"hotkeyUnavailable": "No registrado; puede que otra app lo esté usando",
"remove": "Quitar",
"noNetwork": "Sin red",
"ipv6InboundDesc": "Con IPv6 activado, la VPN permite el tráfico IPv6 entrante.",
"exportLogs": "Exportar registros",
"exportSuccess": "Exportación completada",
"iconStyle": "Estilo de icono",
"iconStyleFilled": "Relleno",
"iconStylePlain": "Simple",
"iconStyleHidden": "Oculto",
"filter": "Filtro",
"hideTimeoutProxies": "Ocultar proxies sin respuesta",
"hideTimeoutProxiesDesc": "Omite los proxies cuya última prueba de latencia superó el tiempo de espera",
"stackMode": "Modo de pila",
"network": "Red",
"bypassDomain": "Dominios excluidos",
"bypassDomainDesc": "Los dominios excluidos solo se aplican mientras el proxy del sistema esté activado.",
"resetTip": "¿Seguro que quieres restablecer?",
"icon": "Icono",
"noData": "Sin datos",
"fontFamily": "Familia de fuente",
"installedFonts": "Fuentes instaladas",
"toggle": "Alternar",
"system": "Sistema",
"routeAddress": "Direcciones de ruta",
"interfaceNameMode": "Interfaz de salida",
"interfaceNameModeClear": "Ninguna",
"interfaceNameModeFollow": "Seguir configuración",
"interfaceNameModeCustom": "Personalizada",
"interfaceName": "Nombre de interfaz",
"interfaceNameDesc": "El nombre de interfaz es la interfaz de red que usan las conexiones salientes.",
"copyEnvVar": "Copiar variables de entorno",
"memoryInfo": "Información de memoria",
"total": "Total",
"releaseMemory": "Liberar memoria",
"memoryCoreHeapInuse": "Heap en uso",
"memoryCoreHeapIdle": "Heap inactivo",
"memoryCoreStack": "Pilas de goroutines",
"memoryCoreRuntime": "Sobrecarga del runtime",
"memoryCoreNotRunning": "El núcleo no se está ejecutando",
"memoryAppResident": "Memoria residente",
"memoryAppShared": "App y compartida",
"memoryEstimateDesc": "Estimado a partir de la memoria residente del proceso; puede diferir de lo que indica el sistema.",
"memoryEstimateSharedDesc": "El núcleo se ejecuta dentro del proceso de la app. Su parte se estima a partir de las estadísticas del runtime, y el resto cuenta como memoria de la app y compartida.",
"memoryReleased": "Memoria liberada",
"memoryReleasedSize": "Liberado: {size}",
"releaseMemoryFailed": "No se pudo liberar la memoria",
"cancel": "Cancelar",
"fileIsUpdate": "El archivo se ha modificado. ¿Guardar los cambios?",
"profileHasUpdate": "El perfil se ha modificado. ¿Desactivar la actualización automática?",
"hasCacheChange": "¿Guardar los cambios en la caché?",
"copySuccess": "Copiado correctamente",
"copyLink": "Copiar enlace",
"exportFile": "Exportar archivo",
"cacheCorrupt": "La caché está dañada. ¿Borrarla?",
"detectionTip": "Depende de una API de terceros; solo con fines informativos",
"listen": "Escucha",
"undo": "Deshacer",
"redo": "Rehacer",
"lineWrap": "Ajuste de línea",
"none": "Ninguno",
"advancedConfig": "Configuración avanzada",
"advancedConfigDesc": "La configuración avanzada incluye la red, el DNS, las reglas añadidas y los scripts.",
"addRule": "Añadir regla",
"content": "Contenido",
"ruleTarget": "Destino de la regla",
"matchTarget": "MATCH-TARGET",
"followProfile": "Seguir perfil",
"sourceIp": "IP de origen",
"noResolve": "No resolver IP",
"saveChanges": "¿Guardar los cambios?",
"cannotSave": "No se puede guardar",
"navigationBarStyle": "Barra inferior",
"floating": "Flotante",
"docked": "Fija",
"slide": "Deslizar",
"fade": "Fundido",
"pureBlack": "Negro puro",
"customColor": "Color personalizado",
"colorStyle": "Estilo de color",
"grid": "Cuadrícula",
"spectrum": "Espectro",
"sliders": "Reguladores",
"hue": "Tono",
"saturation": "Saturación",
"brightness": "Brillo",
"hex": "Hex",
"monochromeScheme": "Monocromo",
"vividStyle": "Vivo",
"tintedStyle": "Tintado",
"softStyle": "Suave",
"developerMode": "Modo desarrollador",
"developerModeEnableTip": "El modo desarrollador está activado.",
"messageTest": "Prueba de mensaje",
"messageTestTip": "Esto es un mensaje.",
"crashTest": "Prueba de fallo",
"crashDetected": "Fallo detectado",
"crashDetectedTip": "La app no pudo terminar de iniciarse dos veces seguidas. Para romper el ciclo, se ha deseleccionado el perfil {name} y se ha omitido la configuración automática. Puedes volver a seleccionarlo en cualquier momento.",
"launchInterrupted": "El inicio no se completó",
"launchInterruptedTip": "La última vez, la app se cerró inesperadamente mientras se iniciaba. En este inicio se ha omitido la configuración automática; puedes iniciarla manualmente para reintentarlo.",
"clearData": "Borrar datos",
"textScale": "Escala del texto",
"textScalePreview": "Así se verá el texto en la app",
"followSystem": "Seguir el sistema",
"internet": "Internet",
"systemApp": "Apps del sistema",
"noNetworkApp": "Apps sin red",
"restoreStrategy": "Estrategia de restauración",
"restoreStrategyOverride": "Sobrescribir",
"restoreStrategyCompatible": "Compatible",
"logsTest": "Prueba de registros",
"emptyTip": "{label} no puede estar vacío",
"urlTip": "{label} debe ser una URL",
"invalidLinkTip": "No es un enlace de suscripción ni un enlace para compartir proxies",
"numberTip": "{label} debe ser un número",
"numberRangeTip": "{label} debe estar entre {min} y {max}",
"interval": "Intervalo",
"existsTip": "{label} ya existe",
"maxLengthTip": "{label} debe tener como máximo {max} caracteres",
"singleAdd": "Añadir uno",
"batchAdd": "Añadir en lote",
"batchListInputTip": "Un elemento por línea, o separados por comas",
"batchMapInputTip": "Una entrada por línea: clave, un espacio y el valor",
"singleImport": "Importar uno",
"batchImport": "Importar en lote",
"batchUrlInputTip": "Una URL por línea",
"batchLinkInputTip": "Un enlace por línea",
"batchPreviewTip": "{count} por añadir, {skipped} omitidos por ya existir",
"lineIssueTip": "Línea {line}: {message}",
"singleValueTip": "{label} debe ser un solo elemento",
"deleteTip": "¿Seguro que quieres eliminar este elemento ({label})?",
"deleteMultipTip": "¿Seguro que quieres eliminar los elementos seleccionados ({label})?",
"nullTip": "Aún no hay ningún elemento ({label})",
"script": "Script",
"color": "Color",
"unnamed": "Sin nombre",
"pleaseEnterScriptName": "Introduce un nombre para el script",
"mixedPort": "Puerto mixto",
"socksPort": "Puerto SOCKS",
"redirPort": "Puerto Redir",
"tproxyPort": "Puerto TProxy",
"portTip": "{label} debe estar entre 1024 y 49151",
"portConflictTip": "Introduce un puerto diferente",
"import": "Importar",
"importFile": "Importar desde archivo",
"importUrl": "Importar desde URL",
"startFromScratch": "Empezar desde cero",
"autoSetSystemDns": "Configurar DNS del sistema automáticamente",
"details": "Detalles de {label}",
"creationTime": "Fecha de creación",
"process": "Proceso",
"host": "Host",
"destination": "Destino",
"destinationGeoIP": "GeoIP de destino",
"destinationIPASN": "ASN de la IP de destino",
"specialProxy": "Proxy especial",
"specialRules": "Reglas especiales",
"remoteDestination": "Destino remoto",
"networkType": "Tipo de red",
"proxyChains": "Cadena de proxies",
"log": "Registro",
"connection": "Conexión",
"request": "Solicitud",
"connected": "Conectado",
"disconnected": "Desconectado",
"connecting": "Conectando…",
"restartCoreTip": "¿Seguro que quieres reiniciar el núcleo?",
"forceRestartCoreTip": "¿Seguro que quieres forzar el reinicio del núcleo?",
"dnsHijacking": "Secuestro de DNS",
"coreStatus": "Estado del núcleo",
"appendSystemDns": "Añadir DNS del sistema",
"editRule": "Editar regla",
"extendMode": "Modo de extensión",
"standardModeDesc": "Modo estándar: añade reglas sobre el perfil",
"scriptModeDesc": "Modo script: reescribe todo el perfil con un script de extensión",
"addedRules": "Reglas añadidas",
"controlGlobalAddedRules": "Controlar las reglas añadidas globales",
"quickActions": "Acciones rápidas",
"rulePresetBlockQuic": "Bloquear QUIC",
"rulePresetBlockStun": "Bloquear STUN",
"rulePresetBlockDot": "Bloquear DNS sobre TLS",
"rulePresetBlockLoopback": "Bloquear bucle del proxy",
"rulePresetSystemServicesDirect": "Apple y Microsoft directos",
"rulePresetBittorrentDirect": "BitTorrent directo",
"rulePresetLanDirect": "LAN directa",
"extendScript": "Script de extensión",
"goToConfigureScript": "Ir a la configuración del script",
"goToConfigureAddedRules": "Ir a la configuración de las reglas añadidas",
"editGlobalRules": "Editar reglas globales",
"confirmForceCrashCore": "¿Seguro que quieres forzar el fallo del núcleo?",
"confirmClearAllData": "¿Seguro que quieres borrar todos los datos?",
"loading": "Cargando…",
"yearsAgo": "{count, plural, =1{hace 1 año} other{hace {count} años}}",
"monthsAgo": "{count, plural, =1{hace 1 mes} other{hace {count} meses}}",
"daysAgo": "{count, plural, =1{hace 1 día} other{hace {count} días}}",
"hoursAgo": "{count, plural, =1{hace 1 hora} other{hace {count} horas}}",
"minutesAgo": "{count, plural, =1{hace 1 minuto} other{hace {count} minutos}}",
"justNow": "Ahora mismo",
"yearsAgoShort": "{count, plural, other{hace {count} a}}",
"monthsAgoShort": "{count, plural, other{hace {count} m}}",
"daysAgoShort": "{count, plural, other{hace {count} d}}",
"hoursAgoShort": "{count, plural, other{hace {count} h}}",
"minutesAgoShort": "{count, plural, other{hace {count} min}}",
"noLongerRemind": "No volver a recordármelo",
"accessControlSettings": "Ajustes de control de acceso",
"turnOn": "Activar",
"turnOff": "Desactivar",
"vpnConfigChangeDetected": "Se detectó un cambio en la configuración relacionada con la VPN",
"restart": "Reiniciar",
"speedStatistics": "Estadísticas de velocidad",
"resetPageChangesTip": "Esta página tiene cambios. ¿Seguro que quieres restablecer?",
"customProfile": "Personalizado",
"customProfileDesc": "Crea grupos de proxies y reglas a partir de los proxies de la app y de otros perfiles",
"customProfileResources": "Recursos de perfil personalizado",
"customProfileResourcesDesc": "Los recursos de perfil personalizado incluyen proxies, proveedores de reglas, filtros y conjuntos de iconos.",
"unknownNetworkError": "Error de red desconocido",
"backupFromNewerVersion": "Esta copia de seguridad procede de una versión más reciente de la app. Actualiza la app antes de restaurarla",
"networkTimeoutError": "Se agotó el tiempo de espera de la solicitud. Comprueba tu red o el proxy e inténtalo de nuevo",
"networkHostLookupError": "No se pudo resolver la dirección del servidor. Comprueba que la URL sea correcta y que el DNS funcione",
"networkConnectionError": "No se pudo conectar con el servidor. Comprueba tu conexión de red o los ajustes del proxy",
"networkTlsError": "Falló la conexión segura. Puede que el certificado del servidor no sea válido o que la conexión esté siendo interceptada",
"networkCancelledError": "Se canceló la solicitud",
"networkAccessDeniedError": "El servidor denegó el acceso (HTTP {code}). Puede que el enlace haya caducado o que las credenciales sean incorrectas",
"networkNotFoundError": "No se encontró nada en esta dirección (HTTP {code}). Comprueba que la URL sea correcta",
"networkRateLimitedError": "Demasiadas solicitudes (HTTP 429). Espera un momento e inténtalo de nuevo",
"networkServerError": "El servidor tuvo un problema (HTTP {code}). Inténtalo de nuevo más tarde",
"networkBadResponseError": "El servidor rechazó la solicitud (HTTP {code})",
"networkRequestFailed": "Falló la solicitud de red: {detail}",
"invalidBackupFile": "Archivo de copia de seguridad no válido",
"pruneCache": "Depurar caché",
"backupAndRestore": "Copia de seguridad y restauración",
"restore": "Restaurar",
"restoreSuccess": "Restauración completada",
"restoreOnlyConfig": "Restaurar solo los perfiles",
"restoreAllData": "Restaurar todos los datos",
"addProfile": "Añadir perfil",
"delayTest": "Prueba de latencia",
"proxyGroupEmpty": "El grupo de proxies está vacío",
"confirmExitWindow": "¿Seguro que quieres salir de la ventana actual?",
"optional": "Opcional",
"maxFailedTimes": "Máximo de fallos",
"testInterval": "Intervalo de prueba",
"excludeFilter": "Filtro de exclusión",
"expectedStatus": "Estado esperado",
"strategy": "Estrategia",
"tolerance": "Tolerancia",
"inputProxyGroupName": "Introduce el nombre del grupo de proxies",
"helperCorruptTip": "El servicio auxiliar (Helper) no está disponible; no se puede activar el modo TUN. Reinstala FlClash para restaurarlo.",
"tunAuthorizationFailed": "No se pudo autorizar TUN, por lo que se desactivó.",
"sudoPasswordTitle": "Introduce tu contraseña para sudo",
"coreBlockedByPolicyTip": "Windows se negó a ejecutar FlClashCore.exe (error {code}). Una directiva de control de aplicaciones, como Smart App Control o AppLocker, bloquea los programas sin firmar; permite FlClash en esa directiva o desactívala e inténtalo de nuevo.",
"coreBlockedBySmartAppControlTip": "Smart App Control de Windows bloqueó FlClashCore.exe porque no está firmado. Abre Seguridad de Windows → Control de aplicaciones y navegador → Configuración de Smart App Control, elige Desactivado y vuelve a iniciar FlClash. Smart App Control no se puede volver a activar sin reinstalar Windows.",
"hideFromList": "Ocultar de la lista",
"testWhenUsed": "Probar al usarse",
"disableUDP": "Desactivar UDP",
"confirmDeleteProxyGroup": "¿Seguro que quieres eliminar este grupo de proxies?",
"ruleEmpty": "La regla está vacía",
"inputRuleContent": "Introduce el contenido de la regla",
"ruleSet": "Conjunto de reglas",
"selectRuleSet": "Selecciona un conjunto de reglas",
"splitStrategy": "Estrategia de división",
"selectSplitStrategy": "Selecciona una estrategia de división",
"noResolveHostname": "No resolver nombre de host",
"matchSourceIp": "Coincidir con IP de origen",
"basicInfo": "Información básica",
"additionalParameters": "Parámetros adicionales",
"proxyType": "Tipo de proxy",
"basicStrategy": "Estrategias básicas",
"editProxy": "Editar proxy",
"addNodes": "Añadir nodos",
"addProxyGroup": "Añadir grupo de proxies",
"editProxyGroup": "Editar grupo de proxies",
"iconUrl": "URL del icono",
"iconRecords": "Registros de iconos",
"custom": "Personalizado",
"ruleActionDomainDesc": "Coincide con el dominio completo",
"ruleActionDomainSuffixDesc": "Coincide con un sufijo de dominio",
"ruleActionDomainKeywordDesc": "Coincide con una palabra clave del dominio",
"ruleActionDomainRegexDesc": "Coincide con una expresión regular de dominio",
"ruleActionDomainWildcardDesc": "Coincidencia con comodines; solo se admiten * y ?",
"ruleActionGeositeDesc": "Coincide con los dominios de Geosite",
"ruleActionIpCidrDesc": "Coincide con un rango de direcciones IP",
"ruleActionIpCidr6Desc": "Coincide con un rango de direcciones IP; IP-CIDR6 es solo un alias",
"ruleActionIpSuffixDesc": "Coincide con un rango de sufijo de IP",
"ruleActionIpAsnDesc": "Coincide con el ASN de la IP",
"ruleActionGeoipDesc": "Coincide con el código de país de la IP",
"ruleActionSrcGeoipDesc": "Coincide con el código de país de la IP de origen",
"ruleActionSrcIpAsnDesc": "Coincide con el ASN de la IP de origen",
"ruleActionSrcIpCidrDesc": "Coincide con un rango de direcciones IP de origen",
"ruleActionSrcIpSuffixDesc": "Coincide con un rango de sufijo de IP de origen",
"ruleActionDstPortDesc": "Coincide con el rango de puertos de destino",
"ruleActionSrcPortDesc": "Coincide con el rango de puertos de origen",
"ruleActionInPortDesc": "Coincide con el puerto de entrada",
"ruleActionInTypeDesc": "Coincide con el tipo de entrada",
"ruleActionInUserDesc": "Coincide con el nombre de usuario de entrada; separa varios usuarios con /",
"ruleActionInNameDesc": "Coincide con el nombre de la entrada",
"ruleActionRematchNameDesc": "Coincide con el nombre de rematch; separa varios nombres con /",
"ruleActionProcessPathDesc": "Coincide con la ruta completa del proceso",
"ruleActionProcessPathRegexDesc": "Coincide con una expresión regular de la ruta del proceso",
"ruleActionProcessPathWildcardDesc": "Coincide con comodines en la ruta del proceso; solo se admiten * y ?",
"ruleActionProcessNameDesc": "Coincide con el nombre del proceso; en Android coincide con el nombre del paquete",
"ruleActionProcessNameRegexDesc": "Coincide con una expresión regular del nombre del proceso; en Android coincide con el nombre del paquete",
"ruleActionProcessNameWildcardDesc": "Coincide con comodines en el nombre del proceso; solo se admiten * y ?",
"ruleActionUidDesc": "Coincide con el ID de usuario de Linux",
"ruleActionNetworkDesc": "Coincide con TCP o UDP",
"ruleActionDscpDesc": "Coincide con la marca DSCP (solo entrada UDP de tproxy)",
"ruleActionRuleSetDesc": "Hace referencia a un conjunto de reglas; requiere rule-providers",
"ruleActionAndDesc": "Regla lógica AND",
"ruleActionOrDesc": "Regla lógica OR",
"ruleActionNotDesc": "Regla lógica NOT",
"ruleActionSubRuleDesc": "Coincide con una subregla; cuidado con los paréntesis",
"ruleActionMatchDesc": "Coincide con todas las solicitudes, sin necesidad de condiciones",
"contentNotEmpty": "El contenido no puede estar vacío",
"splitStrategyNotEmpty": "La estrategia de división no puede estar vacía",
"timeout": "Tiempo agotado",
"delayFailed": "Fallido",
"invalidSubRule": "{subRule} no es una SUB_RULE válida",
"invalidPolicy": "{target} no es una política válida",
"invalidProxyProvider": "{providerName} no es un proveedor de proxies válido",
"invalidRuleSet": "{ruleSet} no es un conjunto de reglas válido",
"invalidNetworkContent": "Solo se admite tcp o udp",
"invalidRangeContent": "Introduce números o rangos como 80 u 8000-9000, separados por /",
"invalidDscpContent": "Una marca DSCP no puede superar 63",
"invalidCidrContent": "Introduce un rango de IP en formato CIDR, como 192.168.0.0/16",
"invalidPortRangeContent": "Introduce un puerto o un rango como 443 u 8000-9000",
"invalidHostContent": "Introduce un dominio o una dirección IP",
"invalidListenContent": "Introduce una dirección y un puerto, como 0.0.0.0:1053",
"invalidProxy": "{proxyName} no es un proxy válido",
"locationPermissionRequired": "Se requiere permiso de ubicación",
"locationPermissionGuide": "1. Abre Ajustes del sistema > Privacidad y seguridad\n2. Elige Servicios de localización\n3. Busca {appName} en la lista y márcalo\n\nCuando termines, vuelve a la app para continuar. Gracias por tu colaboración.",
"prerequisites": "Requisitos previos",
"ignoreBatteryOptimization": "Ignorar optimización de batería",
"batteryOptimizationDesc": "Ignorar la optimización de batería mantiene la app en ejecución en segundo plano.",
"locationPermission": "Permiso de ubicación",
"locationPermissionDesc": "El sistema requiere el permiso de ubicación para leer el nombre de la red Wi-Fi. En Android elige “Permitir todo el tiempo”; de lo contrario, no se podrá leer el nombre de la red Wi-Fi mientras la app esté en segundo plano.",
"excludeSsids": "Excluir SSID",
"excludeSsidsDesc": "Al conectarte a una red Wi-Fi con un SSID excluido, el estado de ejecución de la app cambia automáticamente.",
"ssidsEmpty": "No hay SSID",
"onDemand": "Bajo demanda",
"onDemandDesc": "Bajo demanda pausa el proxy en las redes Wi-Fi excluidas.",
"locationPermissionDeniedMessage": "Se denegó el permiso de ubicación, por lo que no se puede leer el nombre de la red Wi-Fi actual. Activa el permiso de ubicación manualmente en los ajustes del sistema.",
"addSsid": "Añadir SSID",
"editSsid": "Editar SSID",
"authorized": "Autorizado",
"tapToAuthorize": "Toca para autorizar",
"suspended": "Suspendido…",
"resourceUpdateInterval": "Intervalo de actualización automática",
"resourceUpdateIntervalTip": "El intervalo de actualización automática debe ser mayor que 0",
"hours": "horas",
"minutes": "minutos",
"geoResources": "Geo",
"geoSkipped": "{name} ya está actualizado",
"geoUpdated": "{name} actualizado",
"secondsCount": "{count, plural, =1{1 segundo} other{{count} segundos}}",
"proxiesCount": "{count, plural, =1{1 proxy} other{{label} proxies}}",
"rulesCount": "{count, plural, =1{1 regla} other{{label} reglas}}",
"proxyGroupsCount": "{count, plural, =1{1 grupo de proxies} other{{label} grupos de proxies}}",
"changelogBreaking": "Cambios incompatibles",
"changelogFeatures": "Novedades",
"changelogFixes": "Correcciones",
"changelogPerformance": "Rendimiento",
"changelogReverts": "Reversiones",
"close": "Cerrar",
"back": "Atrás",
"minimize": "Minimizar",
"maximize": "Maximizar",
"unmaximize": "Restaurar tamaño",
"pinWindow": "Fijar ventana",
"unpinWindow": "Desfijar ventana",
"exitFullScreen": "Salir de pantalla completa",
"torch": "Linterna",
"pickFromAlbum": "Elegir del álbum",
"blockConnection": "Bloquear conexión",
"closeConnections": "Cerrar conexiones",
"scrollToSelected": "Ir a la selección",
"showMore": "Expandir",
"showLess": "Contraer",
"previousMatch": "Coincidencia anterior",
"nextMatch": "Coincidencia siguiente",
"clearSearch": "Borrar búsqueda",
"noSearchResults": "No hay resultados",
"showPassword": "Mostrar contraseña",
"hidePassword": "Ocultar contraseña",
"authorize": "Autorizar",
"installedAppsPermissionRequired": "Se requiere permiso de lista de apps",
"installedAppsPermissionDesc": "Este sistema oculta la lista de apps instaladas hasta que se concede el permiso. Autorízalo para configurar el proxy por app.",
"installedAppsPermissionDeniedMessage": "Se denegó el permiso de lista de apps, por lo que no se pueden mostrar las apps instaladas. Concédelo manualmente en los ajustes del sistema.",
"outboundIp": "IP de salida",
"ipAddress": "Dirección IP",
"ipQualityLevel": "Nivel",
"ipQualityGood": "Buena",
"ipQualityNormal": "Normal",
"ipQualityRisky": "Arriesgada",
"ipType": "Tipo",
"ipTypeResidential": "Residencial",
"ipTypeMobile": "Red móvil",
"ipTypeBusiness": "Empresarial",
"ipTypeHosting": "Centro de datos",
"ipFlags": "Indicadores",
"ipFlagProxy": "Proxy",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "Historial de abusos",
"ipOrganization": "Organización",
"ipAsn": "ASN",
"ipQualitySource": "Respondido por",
"ipQualitySources": "Fuentes",
"ipQualityFailed": "No se pudo determinar el tipo de IP",
"ipQualityRetry": "Comprobar de nuevo",
"ipSourceNoType": "Sin tipo",
"ipSourceRateLimited": "Límite de solicitudes",
"ipSourceIpMismatch": "IP de salida distinta",
"hideIp": "Ocultar IP",
"editorUnavailable": "Editor no disponible",
"replace": "Reemplazar",
"replaceAll": "Reemplazar todo",
"cameraPermissionRequired": "Se requiere permiso de cámara",
"cameraPermissionDesc": "Permite el acceso a la cámara en los ajustes del sistema para escanear códigos QR, o elige una imagen de código QR del álbum.",
"cameraUnavailable": "Cámara no disponible",
"retry": "Reintentar",
"invalidProfileQrcode": "Este código QR no contiene un enlace de perfil",
"dnsQueries": "Consultas DNS",
"recordType": "Tipo de registro",
"answers": "Respuestas",
"responseCode": "Código de respuesta",
"error": "Error",
"initiator": "Origen de la solicitud",
"cache": "Caché",
"yes": "Sí",
"no": "No",
"nonTextProviderFile": "Este recurso externo no es un archivo de texto",
"addCustomProxy": "Añadir proxy",
"definitionNotMap": "La configuración debe ser un mapa YAML con un nombre y un tipo",
"ruleTextInvalid": "La regla debe empezar con un tipo admitido, por ejemplo DOMAIN,example.com,DIRECT",
"ruleListInvalid": "Las reglas deben ser una lista YAML con una regla por elemento, por ejemplo - DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "No se pudo leer ningún proxy de los enlaces",
"shareLinkUnreadable": "No se pudo leer ningún proxy de {link}",
"proxiesProfileLabel": "{name} y otros ({count} proxies)",
"singleShareLinkOnly": "Introduce aquí un solo enlace; para varios, usa la edición rápida de la lista",
"username": "Usuario",
"emptyFallback": "Respaldo si está vacío",
"defaultSelected": "Selección predeterminada",
"hashByInUser": "Hash por usuario de entrada",
"appProxiesEmpty": "No hay proxies locales",
"nodes": "Nodos",
"localProxies": "Proxies locales",
"customIssueEmptyName": "El nombre está vacío",
"customIssueReservedName": "{name} es un nombre de política integrada y no se puede usar aquí",
"customIssueDuplicateName": "El nombre {name} ya lo usa otro proxy o grupo de proxies",
"customIssueCoreRejected": "El núcleo no puede interpretar este proxy: {message}",
"customIssueMissingProxies": "Estos proxies o políticas no existen: {names}",
"customIssueMissingProviders": "Estos proveedores de proxies no existen: {names}",
"customIssueNoProxySource": "No hay proxies ni proveedores de proxies seleccionados, por lo que el núcleo rechaza este grupo",
"customIssueGroupLoop": "Los grupos de proxies se referencian entre sí en bucle: {path}",
"customIssueInvalidEmptyFallback": "El respaldo si está vacío debe ser un proxy, pero {name} no existe o es un grupo de proxies",
"customIssueInvalidFilter": "{name} no es una expresión regular válida: {message}",
"customIssueMissingDialer": "El proxy de marcación {name} no existe",
"customIssueDialerLoop": "{proxy} marca a través de {target}, que lleva de vuelta a {proxy}, por lo que la marcación entra en bucle",
"dialerProxyLoopStopped": "La marcación a través de {name} lleva de vuelta al proxy que lo usa, por lo que se detuvo la conexión",
"customIssuesSummary": "{count} elementos tienen problemas, y aplicar este perfil puede fallar",
"providerInUse": "{label} sigue en uso en {profiles}, que dejarán de funcionar correctamente si se elimina. ¿Seguro que quieres eliminarlo?",
"proxyGroupInUse": "{label} sigue mencionado por reglas, grupos de proxies, proxies de marcación, DNS o NTP de este perfil, que dejarán de funcionar si se elimina. ¿Seguro que quieres eliminarlo?",
"proxiesRemovedTip": "Se eliminarán estos proxies: {removed}. Una línea cuyo nombre cambió cuenta como un proxy nuevo, y los perfiles personalizados que mencionen los eliminados los marcarán como ausentes. ¿Aplicar de todos modos?",
"proxiesReplacedTip": "Se eliminarán estos proxies: {removed}. Se añadirán estos: {added}. Una línea cuyo nombre cambió cuenta como un proxy nuevo, y los perfiles personalizados que mencionen los eliminados los marcarán como ausentes. ¿Aplicar de todos modos?",
"fontSize": "Tamaño",
"large": "Grande",
"extraLarge": "Muy grande",
"switchProfile": "Cambiar de perfil",
"lastUpdated": "Última actualización",
"filters": "Filtros",
"label": "Etiqueta",
"regex": "Expresión regular",
"matchCase": "Coincidir mayúsculas y minúsculas",
"iconSets": "Conjuntos de iconos",
"invalidIconSet": "No es un conjunto de iconos válido",
"recommendedIcons": "Sugeridos",
"localImage": "Imagen local",
"iconSetsEmptyTip": "Aún no hay conjuntos de iconos. Añádelos en Recursos de perfil personalizado → Conjuntos de iconos",
"outbound": "Salida",
"strictRoute": "Ruta estricta",
"congestionController": "Control de congestión TCP",
"congestionControllerDesc": "El control de congestión TCP solo funciona con la pila mips.",
"routeExcludeAddress": "Direcciones excluidas de la ruta",
"keepAliveIdle": "Inactividad de keep-alive TCP",
"disableKeepAlive": "Desactivar keep-alive TCP",
"routingMark": "Marca de enrutamiento",
"strictRouteDesc": "La ruta estricta evita que el tráfico, como el DNS, eluda TUN, y los demás dispositivos ya no pueden acceder a este.",
"initFailed": "Error al iniciar",
"initFailedTip": "FlClash encontró un error al iniciarse y no puede continuar. Copia los detalles para informar del problema. Si sigue ocurriendo, borrar los datos suele permitir que la app vuelva a iniciarse.",
"errorDetails": "Detalles del error",
"stackTrace": "Seguimiento de pila",
"clearDataAndExitTip": "Esto elimina todos los perfiles, ajustes y datos locales, y luego cierra la app. Ábrela de nuevo para empezar de cero.",
"clearDataFailed": "No se pudieron borrar los datos: {detail}",
"bypassPrivateRoute": "Omitir direcciones privadas",
"bypassPrivateRouteDesc": "Con Omitir direcciones privadas activado, las direcciones de LAN, loopback y multidifusión no pasan por TUN.",
"disableIcmpForwarding": "Desactivar reenvío ICMP",
"disableIcmpForwardingDesc": "Con el reenvío ICMP desactivado, TUN responde él mismo al ping, por lo que ping ya no muestra la latencia real.",
"excludeInterface": "Interfaces excluidas",
"excludeInterfaceDesc": "El tráfico que llega por las interfaces excluidas, como docker0, no pasa por TUN.",
"includeInterface": "Interfaces incluidas",
"includeInterfaceDesc": "Solo el tráfico que llega por las interfaces incluidas pasa por TUN, y las interfaces excluidas dejan de aplicarse.",
"includeUid": "ID de usuario incluidos",
"includeUidDesc": "Solo el tráfico de los procesos de los ID de usuario incluidos pasa por TUN.",
"excludeUid": "ID de usuario excluidos",
"excludeUidDesc": "El tráfico de los procesos de los ID de usuario excluidos no pasa por TUN.",
"excludeSrcPort": "Puertos de origen excluidos",
"excludeSrcPortDesc": "El tráfico desde los puertos de origen excluidos no pasa por TUN.",
"excludeDstPort": "Puertos de destino excluidos",
"excludeDstPortDesc": "El tráfico hacia los puertos de destino excluidos no pasa por TUN.",
"udpTimeout": "Tiempo de espera UDP",
"udpTimeoutDesc": "El tiempo de espera UDP se indica en segundos y solo funciona con las pilas gvisor, system y mixed.",
"dnsHijackDesc": "Sin esta entrada, TUN secuestra todas las consultas DNS al puerto 53 (any:53).",
"tunOverrideDesc": "Las entradas añadidas aquí sustituyen los ajustes de TUN de todos los perfiles; lo que no añadas conserva el ajuste del propio perfil. Las entradas que añade un perfil personalizado tienen prioridad.",
"profileTunSettingsDesc": "Se aplica solo a este perfil y tiene prioridad sobre las entradas de sobrescritura de TUN de los ajustes de red.",
"settingsCount": "{count, plural, =1{1 ajuste} other{{count} ajustes}}",
"sectionsInEffect": "{count} en vigor",
"itemsCount": "{count, plural, =1{1 elemento} other{{count} elementos}}",
"additionalPrefix": "Prefijo del nombre",
"additionalSuffix": "Sufijo del nombre",
"skipCertVerify": "Omitir verificación de certificado",
"ipVersion": "Versión de IP",
"addFilters": "Añadir filtros",
"failedItem": "{label}: {message}",
"failedCount": "{count} elementos fallaron",
"importConfigInvalid": "Este archivo no es una configuración importable",
"importConfigReplaceTip": "Al importar se reemplazan los grupos de proxies, las reglas y los ajustes que tiene ahora este perfil",
"networkName": "Nombre de la red",
"networkSecret": "Clave de la red",
"peers": "Pares",
"virtualIpv4": "IPv4 virtual",
"hostname": "Nombre de host",
"exitNodes": "Nodos de salida",
"cipher": "Cifrado",
"protocol": "Protocolo",
"protocolParam": "Parámetro del protocolo",
"obfs": "Ofuscación",
"obfsParam": "Parámetro de ofuscación",
"obfsPassword": "Contraseña de ofuscación",
"obfsMode": "Modo de ofuscación",
"obfsHost": "Host de ofuscación",
"plugin": "Complemento",
"pluginMode": "Modo del complemento",
"pluginHost": "Host del complemento",
"privateKey": "Clave privada",
"publicKey": "Clave pública",
"privateKeyPassphrase": "Frase de contraseña de la clave privada",
"preSharedKey": "Clave precompartida",
"secretKey": "Clave",
"transport": "Transporte",
"uploadBandwidth": "Ancho de banda de subida",
"downloadBandwidth": "Ancho de banda de descarga",
"protocolVersion": "Versión del protocolo",
"portHopping": "Salto de puertos",
"hopInterval": "Intervalo de salto",
"congestionControl": "Control de congestión",
"udpRelayMode": "Modo de retransmisión UDP",
"allowedIps": "IP permitidas",
"remoteDnsResolve": "Resolución DNS remota",
"clientFingerprint": "Huella del cliente",
"required": "Obligatorio",
"ageSecretKey": "Clave secreta de age",
"ageEncryptedProfile": "Perfil cifrado",
"ageSecretKeyRequiredTip": "Este perfil está cifrado con age y necesita una clave secreta de age para descifrarse",
"currentOutboundDesc": "Contiene el botón de inicio y no se puede quitar",
"running": "En ejecución",
"notRunning": "Detenido",
"trafficPeak": "Pico: {value}",
"trafficTotal": "Total: {value}",
"ipQualityShort": "Calidad",
"ipTypeUnknown": "Tipo desconocido",
"tapIconToRetry": "Toca el icono para reintentar",
"networkTakeover": "Control del tráfico",
"portValue": "Puerto {port}",
"serviceAbnormalCount": "{count, plural, =1{1 problema} other{{count} problemas}}",
"serviceAllAvailable": "Todo disponible",
"serviceShownOnDashboard": "Se muestra en el panel",
"groupTimeoutCount": "{count} sin respuesta",
"groupCountUnit": "grupos",
"groupCount": "{count, plural, =1{1 grupo} other{{count} grupos}}",
"proxyGroupsAfterStart": "Los grupos aparecen aquí cuando se inicia el núcleo",
"usedShort": "usado",
"profileUpdatedAt": "Actualizado {time}",
"profileExpiresIn": "Vence el {date}, {days, plural, =1{queda 1 día} other{quedan {days} días}}",
"profileDaysLeft": "{days, plural, =1{Queda 1 día} other{Quedan {days} días}}",
"target": "Destino",
"outboundExit": "Salida",
"duration": "Duración",
"traffic": "Tráfico",
"totalDownload": "Descargado",
"totalUpload": "Subido",
"initiatorShort": "De",
"result": "Resultado",
"elapsed": "Tardó",
"connectionStats": "Estadísticas de conexión",
"connectionStatsDesc": "Conexiones activas, solicitudes recientes y consultas DNS",
"memoryInfoDesc": "Memoria que usan la app y el núcleo",
"editDashboard": "Editar panel",
"done": "Hecho",
"dashboardAddableSection": "Disponibles para añadir",
"customImportDroppedSubRules": "{count, plural, =1{Se descartó 1 regla SUB-RULE, porque los perfiles personalizados no admiten subreglas} other{Se descartaron {label} reglas SUB-RULE, porque los perfiles personalizados no admiten subreglas}}",
"autoInclude": "Incluir automáticamente",
"allReferencedProxies": "Todos los proxies referenciados",
"allReferencedProxyProviders": "Todos los proveedores de proxies referenciados"
}
+943
View File
@@ -0,0 +1,943 @@
{
"rule": "Règle",
"rules": "Règles",
"global": "Global",
"direct": "Direct",
"dashboard": "Tableau de bord",
"proxies": "Proxys",
"profile": "Profil",
"profiles": "Profils",
"tools": "Outils",
"logs": "Journaux",
"resources": "Ressources",
"trafficUsage": "Trafic utilisé",
"networkSpeed": "Débit réseau",
"outboundMode": "Mode sortant",
"serviceStatus": "État des services",
"serviceRestricted": "Accès restreint",
"serviceAvailable": "Disponible",
"serviceUnavailable": "Indisponible",
"serviceFailed": "Échec de la vérification",
"serviceDisallowedIsp": "FAI non autorisé",
"serviceBlocked": "Bloqué",
"serviceUnsupportedRegion": "Région non prise en charge",
"serviceOriginalsOnly": "Contenus originaux uniquement",
"serviceComingSoon": "Bientôt disponible",
"servicePending": "Non vérifié",
"serviceManage": "Gérer les services",
"serviceCheck": "Vérifier",
"serviceCheckAll": "Tout vérifier",
"serviceCheckedAt": "Vérifié à {time}",
"upload": "Envoi",
"download": "Réception",
"usedTraffic": "Trafic utilisé",
"totalTraffic": "Trafic total",
"expireTime": "Date d’expiration",
"nullProfileDesc": "Ajoutez un profil pour commencer",
"settings": "Paramètres",
"language": "Langue",
"defaultText": "Par défaut",
"more": "Plus",
"other": "Autre",
"about": "À propos",
"en": "Anglais",
"es": "Espagnol",
"fr": "Français",
"ja": "Japonais",
"ko": "Coréen",
"ru": "Russe",
"zhCN": "Chinois simplifié",
"theme": "Thème",
"extend": "Extension",
"runtimeConfig": "Configuration d’exécution",
"allowLan": "Autoriser le LAN",
"tun": "TUN",
"tunDesc": "TUN ne fonctionne qu’en mode administrateur.",
"minimizeOnExit": "Réduire à la fermeture",
"autoLaunch": "Lancement automatique",
"silentLaunch": "Lancement silencieux",
"silentLaunchDesc": "Le lancement silencieux démarre l’app sans afficher la fenêtre.",
"autoRun": "Démarrage automatique",
"autoRunDesc": "Le démarrage automatique active le proxy à l’ouverture de l’app.",
"logcat": "Logcat",
"logcatDesc": "Désactiver Logcat masque l’entrée Journaux.",
"autoCheckUpdate": "Rechercher les mises à jour automatiquement",
"checkCertificate": "Vérifier les certificats TLS",
"checkCertificateDesc": "Sans vérification TLS, les abonnements et les sauvegardes sont exposés aux attaques de l’homme du milieu.",
"accessControl": "Contrôle d’accès",
"accessControlDesc": "Le contrôle d’accès choisit les apps qui utilisent le proxy.",
"edit": "Modifier",
"confirm": "Confirmer",
"update": "Mettre à jour",
"add": "Ajouter",
"save": "Enregistrer",
"delete": "Supprimer",
"seconds": "secondes",
"qrcode": "Code QR",
"qrcodeDesc": "Scannez un code QR pour obtenir un profil",
"url": "URL",
"link": "Lien",
"linkDesc": "Obtenir un profil depuis un lien d’abonnement ou un lien de partage de proxy",
"file": "Fichier",
"fileDesc": "Importer directement un fichier de profil",
"name": "Nom",
"profileNameNullValidationDesc": "Veuillez saisir le nom du profil",
"rename": "Renommer",
"profileUrlNullValidationDesc": "Veuillez saisir l’URL du profil",
"profileUrlInvalidValidationDesc": "Veuillez saisir une URL de profil valide",
"autoUpdate": "Mise à jour automatique",
"autoUpdateInterval": "Intervalle de mise à jour automatique",
"profileAutoUpdateIntervalNullValidationDesc": "Veuillez saisir l’intervalle de mise à jour automatique",
"profileAutoUpdateIntervalInvalidValidationDesc": "Veuillez saisir un intervalle valide",
"themeMode": "Mode du thème",
"themeColor": "Couleur du thème",
"preview": "Aperçu",
"auto": "Auto",
"light": "Clair",
"dark": "Sombre",
"importFromLink": "Importer depuis un lien",
"submit": "Valider",
"createProfileFromUrlTip": "Créer un profil à partir de {url} ?",
"create": "Créer",
"pleaseUploadValidQrcode": "Veuillez importer un code QR valide",
"blacklistMode": "Mode liste noire",
"whitelistMode": "Mode liste blanche",
"selectAll": "Tout sélectionner",
"cancelSelectAll": "Tout désélectionner",
"appAccessControl": "Contrôle d’accès des apps",
"accessControlAllowDesc": "Seules les apps sélectionnées passent par le VPN",
"accessControlNotAllowDesc": "Les apps sélectionnées sont exclues du VPN",
"accessControlDisabledDesc": "Le contrôle d’accès des apps est désactivé",
"selected": "Sélectionné",
"port": "Port",
"logLevel": "Niveau de journalisation",
"show": "Afficher",
"exit": "Quitter",
"systemProxy": "Proxy système",
"safeMode": "Mode sans échec",
"safeModeAppTitle": "{appName} (mode sans échec)",
"project": "Projet",
"core": "Cœur",
"tabAnimation": "Animation des onglets",
"desc": "Un client proxy multiplateforme basé sur ClashMeta, simple d’utilisation, open source et sans publicité.",
"startVpn": "Démarrage du VPN…",
"stopVpn": "Arrêt du VPN…",
"localNetworkDeniedTip": "Autorisation du réseau local refusée : la pile gvisor est utilisée, le LAN est inaccessible.",
"compatible": "Mode de compatibilité",
"notSelectedTip": "Le groupe de proxys actuel n’est pas sélectionnable",
"changeProxyFailedTip": "Échec du changement de proxy ; la sélection précédente a été rétablie",
"databaseWriteFailedTip": "Échec de l’enregistrement de la modification ; elle a été annulée",
"tip": "Astuce",
"account": "Compte",
"backup": "Sauvegarde",
"backupSuccess": "Sauvegarde réussie",
"noInfo": "Aucune information",
"pleaseBindWebDAV": "Veuillez lier un WebDAV",
"bind": "Lier",
"connectivity": "Connectivité : ",
"webDAVConfiguration": "Configuration WebDAV",
"address": "Adresse",
"addressHelp": "Adresse du serveur WebDAV",
"addressTip": "Veuillez saisir une adresse WebDAV valide",
"password": "Mot de passe",
"authentication": "Authentification",
"authenticationDesc": "L’authentification demande un nom d’utilisateur et un mot de passe sur le port du proxy local.",
"authenticationSystemProxyDesc": "Le proxy système n’est pas appliqué tant que l’authentification est activée.",
"checkUpdate": "Rechercher des mises à jour",
"discoverNewVersion": "Nouvelle version disponible",
"checkUpdateError": "L’app est déjà à jour",
"goDownload": "Télécharger",
"unknown": "Inconnu",
"country": "Région",
"search": "Rechercher",
"allowBypass": "Autoriser les apps à contourner le VPN",
"externalController": "Contrôleur externe",
"externalControllerDesc": "Le contrôleur externe permet de piloter le cœur via le port 9090.",
"externalControllerSecret": "Secret du contrôleur externe",
"ipv6Desc": "Le trafic IPv6 n’est reçu que si IPv6 est activé.",
"app": "App",
"general": "Général",
"startupAndBackground": "Démarrage et arrière-plan",
"requestsAndUpdates": "Requêtes et mises à jour",
"logsAndDiagnostics": "Journaux et diagnostic",
"userAgent": "User-Agent",
"unifiedDelay": "Délai unifié",
"tcpConcurrent": "TCP simultané",
"geodataLoader": "Mode Geo à faible mémoire",
"requests": "Requêtes",
"recentRequests": "Requêtes récentes",
"findProcessMode": "Recherche de processus",
"init": "Initialisation",
"infiniteTime": "N’expire jamais",
"connections": "Connexions",
"liveConnections": "Connexions en direct",
"inbound": "Entrant",
"intranetIP": "IP du réseau local",
"view": "Afficher",
"cut": "Couper",
"copy": "Copier",
"clipboardWriteFailed": "Impossible de copier dans le presse-papiers. La sélection est peut-être trop volumineuse",
"paste": "Coller",
"testUrl": "URL de test",
"sync": "Synchroniser",
"exclude": "Masquer des tâches récentes",
"compact": "Compact",
"tab": "Onglet",
"list": "Liste",
"delay": "Délai",
"style": "Style",
"size": "Taille",
"sort": "Trier",
"columns": "Colonnes",
"proxyGroup": "Groupe de proxys",
"go": "Aller",
"externalLink": "Lien externe",
"otherContributors": "Autres contributeurs",
"appIconDesign": "Conception de l’icône de l’app",
"autoCloseConnections": "Fermer les connexions automatiquement",
"autoCloseConnectionsDesc": "La fermeture automatique coupe les connexions ouvertes lorsque vous changez de proxy.",
"onlyStatisticsProxy": "Ne compter que le trafic proxy",
"showNotificationStopAction": "Bouton Arrêter dans la notification",
"pureBlackMode": "Mode noir pur",
"sidebarBlur": "Flou de la barre latérale",
"sidebarBlurDesc": "Afficher le bureau flouté derrière la fenêtre à travers la barre latérale",
"liquidGlass": "Liquid Glass",
"keepAliveIntervalDesc": "Intervalle keep-alive TCP",
"entries": " entrées",
"local": "Local",
"remote": "Distant",
"mode": "Mode",
"time": "Heure",
"source": "Source",
"action": "Action",
"intelligentSelected": "Sélection intelligente",
"clipboardImport": "Importer depuis le presse-papiers",
"clipboardExport": "Exporter vers le presse-papiers",
"layout": "Disposition",
"tight": "Serrée",
"standard": "Standard",
"ultra": "Ultra",
"loose": "Aérée",
"profilesSort": "Trier les profils",
"start": "Démarrer",
"runTime": "Durée d’exécution",
"stop": "Arrêter",
"key": "Clé",
"value": "Valeur",
"vpnEnableDesc": "Le VPN fait passer automatiquement tout le trafic du système par VpnService.",
"options": "Options",
"enabled": "Activé",
"disabled": "Désactivé",
"loopback": "Exemption de bouclage UWP",
"providers": "Ressources externes",
"proxyProviders": "Fournisseurs de proxys",
"ruleProviders": "Fournisseurs de règles",
"providerUrlTip": "Seuls les fournisseurs distants sont pris en charge",
"ruleSetEmptyTip": "Le jeu de règles ne contient aucune entrée exploitable",
"ruleSetMixedTip": "Le jeu de règles mélange des domaines et des plages d’IP : son type est indéterminable",
"ruleSetInvalidTip": "Ce fichier mrs est illisible",
"subscriptionInfo": "Infos d’abonnement",
"ntpStatusDesc": "NTP récupère l’heure auprès d’un serveur NTP au lieu de l’horloge système.",
"server": "Serveur",
"ntpInterval": "Intervalle de synchronisation (minutes)",
"dialerProxy": "Proxy de connexion",
"dialerProxyDesc": "Le proxy de connexion est la sortie utilisée pour joindre le serveur NTP.",
"writeToSystem": "Écrire dans le système",
"writeToSystemDesc": "Écrire dans le système règle aussi l’horloge système ; Android l’ignore.",
"sniffer": "Sniffer",
"snifferStatusDesc": "Le sniffer lit le domaine dans le trafic TLS, HTTP et QUIC afin que les règles puissent le reconnaître.",
"overrideDestination": "Remplacer la destination",
"overrideDestinationDesc": "Remplacer la destination se connecte au domaine détecté plutôt qu’à l’adresse d’origine.",
"forceDnsMapping": "Forcer le mappage DNS",
"forceDnsMappingDesc": "Forcer le mappage DNS analyse aussi les connexions dont le domaine provient d’un mappage DNS.",
"parsePureIp": "Analyser les IP seules",
"parsePureIpDesc": "Analyser les IP seules inspecte les connexions qui ne portent qu’une adresse IP.",
"forceDomain": "Domaines forcés",
"forceDomainDesc": "Les domaines forcés sont aussi analysés ; les autres connexions qui ont déjà un domaine sont laissées telles quelles.",
"skipDomain": "Domaines ignorés",
"skipDomainDesc": "Un domaine détecté figurant dans la liste des domaines ignorés n’est pas utilisé.",
"skipSrcAddress": "Adresses sources ignorées",
"skipDstAddress": "Adresses de destination ignorées",
"sniffProtocols": "Protocoles",
"ports": "Ports",
"overrideEntries": "Entrées de remplacement",
"addOverrideEntry": "Ajouter une entrée de remplacement",
"dnsOverrideDesc": "Les entrées ajoutées ici remplacent le DNS de tous les profils URL et fichier ; tout ce qui n’est pas ajouté garde le réglage du profil. Les profils personnalisés définissent leur DNS dans leur contenu.",
"settingEntries": "réglages",
"addSettingEntry": "Ajouter un réglage",
"profileSettingsDesc": "S’applique uniquement à ce profil ; tout ce qui n’est pas ajouté utilise la valeur par défaut.",
"healthCheck": "Contrôle d’état",
"discardChanges": "Abandonner les modifications ?",
"quickEdit": "Modification rapide",
"status": "État",
"statusDesc": "Si DNS est désactivé, le DNS du système est utilisé.",
"respectRules": "Respecter les règles",
"respectRulesDesc": "Respecter les règles fait passer les connexions DNS selon les règles et nécessite un Proxy Server Nameserver.",
"dnsMode": "Mode DNS",
"fakeipRange": "Plage Fake-IP",
"fakeipFilter": "Filtre Fake-IP",
"useHosts": "Utiliser hosts",
"useSystemHosts": "Utiliser les hosts du système",
"fallbackFilter": "Filtre de repli",
"domain": "Domaine",
"listenRoutingMark": "Marque de routage d’écoute",
"listenRoutingMarkDesc": "La marque de routage d’écoute ne fonctionne que sous Linux.",
"ipv6Timeout": "Délai d’expiration IPv6 (ms)",
"cacheAlgorithm": "Algorithme de cache",
"cacheMaxSize": "Taille du cache",
"fakeipRange6": "Plage Fake-IP (IPv6)",
"fakeipFilterMode": "Mode du filtre Fake-IP",
"fakeipFilterModeDesc": "En mode filtre Fake-IP, la liste noire exclut les correspondances, la liste blanche ne falsifie que les correspondances, et « rule » les traite comme des règles.",
"fakeipTtl": "TTL Fake-IP",
"reset": "Réinitialiser",
"actionView": "Afficher/Masquer",
"actionStart": "Démarrer/Arrêter",
"actionMode": "Changer de mode",
"actionProxy": "Proxy système",
"actionTun": "TUN",
"actionRuleMode": "Mode règles",
"actionGlobalMode": "Mode global",
"actionDirectMode": "Mode direct",
"actionDelayTest": "Tester tous les délais",
"actionUpdateProfiles": "Mettre à jour les profils",
"disclaimer": "Avertissement",
"disclaimerDesc": "Avant d’utiliser FlClash (« le Logiciel »), veuillez lire attentivement cette déclaration et vous assurer de l’avoir comprise dans son intégralité. Appuyer sur « Accepter » signifie que vous avez lu, compris et accepté chacune des conditions ci-dessous. Si vous n’êtes pas d’accord, appuyez sur « Quitter » et cessez d’utiliser le Logiciel.",
"disclaimerSoftwareTitle": "Nature du logiciel",
"disclaimerSoftwareContent": "Le Logiciel est un client proxy réseau open source construit sur le cœur Clash.Meta (mihomo). Il se limite à fournir des outils locaux tels que la gestion de la configuration, le routage par règles et le transfert du trafic.",
"disclaimerNoServiceStatement": "Le Logiciel ne fournit lui-même aucun serveur proxy, nœud, abonnement ni service d’accès au réseau, et n’a aucun partenariat, mandat ni lien de garantie avec un quelconque fournisseur de tels services.",
"disclaimerUsageTitle": "Restrictions d’utilisation",
"disclaimerUsageContent": "Le Logiciel est destiné uniquement à des usages non commerciaux tels que l’apprentissage, les échanges et la recherche technique. Toute utilisation à des fins commerciales est strictement interdite, y compris, sans s’y limiter, la distribution payante, la vente groupée, l’intégration à un service commercial ou l’exercice d’une activité au nom du Logiciel. Toute activité commerciale est étrangère au Logiciel et à ses développeurs.\n\nIl est strictement interdit d’utiliser le Logiciel à des fins contraires aux lois et règlements de votre pays ou de votre région, y compris, sans s’y limiter, le contournement de restrictions d’accès au réseau légalement imposées, la diffusion de contenus illicites, le lancement d’attaques réseau ou l’atteinte aux droits légitimes d’autrui.",
"disclaimerResponsibilityTitle": "Votre responsabilité",
"disclaimerResponsibilityContent": "Il vous incombe de vous assurer que l’utilisation du Logiciel est licite là où vous vivez, et vous assumez seul la responsabilité juridique de tout ce que vous faites avec lui et de ses conséquences.\n\nLes abonnements, nœuds et configurations que vous importez relèvent de votre choix. La licéité de leur source, la sécurité de leur contenu et la fiabilité de leur service sont une affaire entre vous et leurs fournisseurs.",
"disclaimerThirdPartyTitle": "Contenus tiers",
"disclaimerThirdPartyContent": "Les liens d’abonnement, fichiers de configuration, jeux de règles, scripts, ressources externes et liens externes sont tous fournis par des tiers. Les développeurs ne peuvent ni ne vont vérifier ou garantir leur légalité, leur exactitude, leur sécurité ou leur disponibilité.\n\nTout vol de données, perte financière, bannissement de compte ou autre dommage causé par un contenu tiers se règle entre vous et le tiers concerné ; les développeurs n’en assument aucune responsabilité.",
"disclaimerWarrantyTitle": "Absence de garantie",
"disclaimerWarrantyContent": "Le Logiciel est fourni « tel quel » et « selon disponibilité », sans garantie d’aucune sorte, expresse ou implicite, y compris, sans s’y limiter, les garanties de qualité marchande, d’adéquation à un usage particulier, d’absence de contrefaçon, de disponibilité ininterrompue, d’absence d’erreurs ou d’absence de failles de sécurité.\n\nLes développeurs ne garantissent pas que le Logiciel répondra à vos besoins ni qu’il fonctionnera sans interruption ni erreur.",
"disclaimerLiabilityTitle": "Limitation de responsabilité",
"disclaimerLiabilityContent": "Dans toute la mesure permise par la loi applicable, ni les développeurs ni aucun contributeur ne sauraient être tenus responsables de dommages directs, indirects, accessoires, spéciaux, punitifs ou consécutifs résultant de l’utilisation ou de l’impossibilité d’utiliser le Logiciel, y compris, sans s’y limiter, la perte de données, les dommages matériels, les pannes réseau, l’interruption d’activité, le manque à gagner ou tout litige qui en découlerait, même s’ils ont été avertis de la possibilité de tels dommages.",
"disclaimerPrivacyTitle": "Collecte de données et confidentialité",
"disclaimerPrivacyContent": "Le Logiciel ne collecte ni ne transmet vos URL d’abonnement, détails de nœuds, contenus de configuration, sites visités, historiques de connexion, contenus de trafic ou journaux. Ces données restent sur votre appareil et les développeurs n’y ont pas accès.\n\nLe Logiciel n’accède au réseau que lorsque vous utilisez une fonction qui en a besoin, par exemple pour récupérer l’URL d’abonnement que vous avez fournie lors de la mise à jour d’un profil, ou pour contacter GitHub lors de la recherche de mises à jour.\n\nLe Logiciel n’inclut aucun service d’analyse ni de rapport de plantage, sur aucune plateforme.",
"disclaimerLicenseTitle": "Licence open source",
"disclaimerLicenseContent": "Le Logiciel est open source sous licence GPL-3.0. Vous pouvez l’utiliser, le modifier et le distribuer librement tant que vous respectez cette licence, qui exige que les œuvres dérivées soient elles aussi publiées sous GPL-3.0 et que les mentions de droit d’auteur d’origine soient conservées.\n\nLes composants tiers du Logiciel, dont le cœur Clash.Meta, suivent leurs propres licences. Les auteurs d’origine ne sont pas responsables des problèmes liés aux versions modifiées ou redistribuées.",
"disclaimerChangesTitle": "Modifications de cette déclaration",
"disclaimerChangesContent": "Les développeurs peuvent réviser cette déclaration à chaque version, et la révision prend effet à la publication de cette version. Continuer à utiliser le Logiciel après une mise à jour signifie que vous acceptez la déclaration révisée.",
"disclaimerAcceptTitle": "Acceptation",
"disclaimerAcceptContent": "En installant, copiant ou utilisant le Logiciel, vous êtes réputé avoir lu et accepté l’intégralité de cette déclaration. Si vous n’êtes pas d’accord avec l’une de ses conditions, cessez immédiatement d’utiliser le Logiciel et désinstallez-le.",
"disclaimerRestateTitle": "Recopier la déclaration",
"disclaimerRestateTip": "Pour confirmer que vous avez compris, saisissez la déclaration suivante en entier dans le champ ci-dessous :",
"disclaimerRestateHint": "Saisissez la déclaration ci-dessus",
"disclaimerRestateMismatch": "Ne correspond pas à la déclaration",
"disclaimerAcknowledgeTitle": "Confirmer la déclaration",
"disclaimerAcknowledgeTip": "Pour confirmer que vous avez compris, lisez la déclaration suivante et cochez la case ci-dessous :",
"disclaimerAcknowledgeLabel": "J'ai lu et compris cette déclaration",
"agree": "Accepter",
"hotkeyManagement": "Gestion des raccourcis",
"hotkeyDesc": "Les raccourcis globaux fonctionnent même lorsque la fenêtre est masquée. Appuyez sur une action pour enregistrer sa combinaison de touches.",
"hotkeyNotSet": "Non défini",
"pressKeyboard": "Appuyez sur une combinaison de touches",
"hotkeyNeedsModifier": "Incluez au moins l’une de ces touches : {modifiers}",
"hotkeyConflictWith": "Déjà utilisé par « {action} ». L’enregistrer le déplace ici.",
"hotkeyUnavailable": "Non enregistré, il est peut-être pris par une autre app",
"remove": "Retirer",
"noNetwork": "Aucun réseau",
"ipv6InboundDesc": "Avec IPv6 activé, le VPN autorise le trafic IPv6 entrant.",
"exportLogs": "Exporter les journaux",
"exportSuccess": "Exportation réussie",
"iconStyle": "Style des icônes",
"iconStyleFilled": "Pleines",
"iconStylePlain": "Simples",
"iconStyleHidden": "Masquées",
"filter": "Filtrer",
"hideTimeoutProxies": "Masquer les proxys expirés",
"hideTimeoutProxiesDesc": "Ignorer les proxys dont le dernier test de délai a expiré",
"stackMode": "Mode de pile",
"network": "Réseau",
"bypassDomain": "Domaines contournés",
"bypassDomainDesc": "Les domaines contournés ne s’appliquent que si le proxy système est activé.",
"resetTip": "Voulez-vous vraiment réinitialiser ?",
"icon": "Icône",
"noData": "Aucune donnée",
"fontFamily": "Police",
"installedFonts": "Polices installées",
"toggle": "Basculer",
"system": "Système",
"routeAddress": "Adresses de route",
"interfaceNameMode": "Interface sortante",
"interfaceNameModeClear": "Aucune",
"interfaceNameModeFollow": "Suivre la configuration",
"interfaceNameModeCustom": "Personnalisée",
"interfaceName": "Nom de l’interface",
"interfaceNameDesc": "Le nom de l’interface est l’interface réseau utilisée par les connexions sortantes.",
"copyEnvVar": "Copier les variables d’environnement",
"memoryInfo": "Infos mémoire",
"total": "Total",
"releaseMemory": "Libérer la mémoire",
"memoryCoreHeapInuse": "Tas utilisé",
"memoryCoreHeapIdle": "Tas inactif",
"memoryCoreStack": "Piles de goroutines",
"memoryCoreRuntime": "Surcharge du runtime",
"memoryCoreNotRunning": "Le cœur n’est pas en cours d’exécution",
"memoryAppResident": "Mémoire résidente",
"memoryAppShared": "App et partagée",
"memoryEstimateDesc": "Estimation d’après la mémoire résidente du processus ; elle peut différer de ce qu’indique le système.",
"memoryEstimateSharedDesc": "Le cœur s’exécute dans le processus de l’app. Sa part est estimée à partir des statistiques du runtime, et le reste compte comme mémoire de l’app et partagée.",
"memoryReleased": "Mémoire libérée",
"memoryReleasedSize": "{size} libérés",
"releaseMemoryFailed": "Échec de la libération de la mémoire",
"cancel": "Annuler",
"fileIsUpdate": "Le fichier a été modifié. Enregistrer les modifications ?",
"profileHasUpdate": "Le profil a été modifié. Désactiver la mise à jour automatique ?",
"hasCacheChange": "Mettre les modifications en cache ?",
"copySuccess": "Copié avec succès",
"copyLink": "Copier le lien",
"exportFile": "Exporter le fichier",
"cacheCorrupt": "Le cache est corrompu. Le vider ?",
"detectionTip": "Repose sur une API tierce ; à titre indicatif uniquement",
"listen": "Écoute",
"undo": "Annuler",
"redo": "Rétablir",
"lineWrap": "Retour à la ligne",
"none": "Aucun",
"advancedConfig": "Configuration avancée",
"advancedConfigDesc": "La configuration avancée regroupe le réseau, le DNS, les règles ajoutées et les scripts.",
"addRule": "Ajouter une règle",
"content": "Contenu",
"ruleTarget": "Cible de la règle",
"matchTarget": "MATCH-TARGET",
"followProfile": "Suivre le profil",
"sourceIp": "IP source",
"noResolve": "Ne pas résoudre l’IP",
"saveChanges": "Enregistrer les modifications ?",
"cannotSave": "Enregistrement impossible",
"navigationBarStyle": "Barre inférieure",
"floating": "Flottante",
"docked": "Ancrée",
"slide": "Glissement",
"fade": "Fondu",
"pureBlack": "Noir pur",
"customColor": "Couleur personnalisée",
"colorStyle": "Style de couleur",
"grid": "Grille",
"spectrum": "Spectre",
"sliders": "Curseurs",
"hue": "Teinte",
"saturation": "Saturation",
"brightness": "Luminosité",
"hex": "Hex",
"monochromeScheme": "Monochrome",
"vividStyle": "Vif",
"tintedStyle": "Teinté",
"softStyle": "Doux",
"developerMode": "Mode développeur",
"developerModeEnableTip": "Le mode développeur est activé.",
"messageTest": "Test de message",
"messageTestTip": "Ceci est un message.",
"crashTest": "Test de plantage",
"crashDetected": "Plantage détecté",
"crashDetectedTip": "L’app n’a pas réussi à finir de démarrer deux fois de suite. Pour sortir de cette boucle, le profil {name} a été désélectionné et la configuration automatique a été ignorée. Vous pouvez le sélectionner de nouveau à tout moment.",
"launchInterrupted": "Démarrage inachevé",
"launchInterruptedTip": "L’app s’est arrêtée de façon inattendue lors de son dernier démarrage. La configuration automatique a été ignorée pour ce lancement ; vous pouvez la démarrer manuellement pour réessayer.",
"clearData": "Effacer les données",
"textScale": "Taille du texte",
"textScalePreview": "Le texte de l’app ressemblera à ceci",
"followSystem": "Suivre le système",
"internet": "Internet",
"systemApp": "Apps système",
"noNetworkApp": "Apps sans réseau",
"restoreStrategy": "Stratégie de restauration",
"restoreStrategyOverride": "Écraser",
"restoreStrategyCompatible": "Compatible",
"logsTest": "Test des journaux",
"emptyTip": "{label} ne peut pas être vide",
"urlTip": "{label} doit être une URL",
"invalidLinkTip": "Ce n’est ni un lien d’abonnement ni un lien de partage de proxy",
"numberTip": "{label} doit être un nombre",
"numberRangeTip": "{label} doit être compris entre {min} et {max}",
"interval": "Intervalle",
"existsTip": "{label} existe déjà",
"maxLengthTip": "{label} ne doit pas dépasser {max} caractères",
"singleAdd": "Ajout unique",
"batchAdd": "Ajout groupé",
"batchListInputTip": "Un élément par ligne, ou séparés par des virgules",
"batchMapInputTip": "Une entrée par ligne : la clé, un espace, puis la valeur",
"singleImport": "Importation unique",
"batchImport": "Importation groupée",
"batchUrlInputTip": "Une URL par ligne",
"batchLinkInputTip": "Un lien par ligne",
"batchPreviewTip": "{count} à ajouter, {skipped} ignorés car déjà présents",
"lineIssueTip": "Ligne {line} : {message}",
"singleValueTip": "{label} doit être un seul élément",
"deleteTip": "Voulez-vous vraiment supprimer cet élément ({label}) ?",
"deleteMultipTip": "Voulez-vous vraiment supprimer les éléments sélectionnés ({label}) ?",
"nullTip": "Aucun élément ({label}) pour le moment",
"script": "Script",
"color": "Couleur",
"unnamed": "Sans nom",
"pleaseEnterScriptName": "Veuillez saisir un nom de script",
"mixedPort": "Port mixte",
"socksPort": "Port SOCKS",
"redirPort": "Port Redir",
"tproxyPort": "Port TProxy",
"portTip": "{label} doit être compris entre 1024 et 49151",
"portConflictTip": "Veuillez saisir un autre port",
"import": "Importer",
"importFile": "Importer depuis un fichier",
"importUrl": "Importer depuis une URL",
"startFromScratch": "Partir de zéro",
"autoSetSystemDns": "Définir automatiquement le DNS du système",
"details": "Détails de {label}",
"creationTime": "Date de création",
"process": "Processus",
"host": "Hôte",
"destination": "Destination",
"destinationGeoIP": "GeoIP de destination",
"destinationIPASN": "ASN de l’IP de destination",
"specialProxy": "Proxy spécial",
"specialRules": "Règles spéciales",
"remoteDestination": "Destination distante",
"networkType": "Type de réseau",
"proxyChains": "Chaîne de proxys",
"log": "Journal",
"connection": "Connexion",
"request": "Requête",
"connected": "Connecté",
"disconnected": "Déconnecté",
"connecting": "Connexion…",
"restartCoreTip": "Voulez-vous vraiment redémarrer le cœur ?",
"forceRestartCoreTip": "Voulez-vous vraiment forcer le redémarrage du cœur ?",
"dnsHijacking": "Détournement DNS",
"coreStatus": "État du cœur",
"appendSystemDns": "Ajouter le DNS du système",
"editRule": "Modifier la règle",
"extendMode": "Mode d’extension",
"standardModeDesc": "Mode standard : ajoute des règles par-dessus le profil",
"scriptModeDesc": "Mode script : réécrit tout le profil avec un script d’extension",
"addedRules": "Règles ajoutées",
"controlGlobalAddedRules": "Gérer les règles ajoutées globales",
"quickActions": "Actions rapides",
"rulePresetBlockQuic": "Bloquer QUIC",
"rulePresetBlockStun": "Bloquer STUN",
"rulePresetBlockDot": "Bloquer DNS over TLS",
"rulePresetBlockLoopback": "Bloquer le bouclage du proxy",
"rulePresetSystemServicesDirect": "Apple et Microsoft en direct",
"rulePresetBittorrentDirect": "BitTorrent en direct",
"rulePresetLanDirect": "LAN en direct",
"extendScript": "Script d’extension",
"goToConfigureScript": "Aller à la configuration du script",
"goToConfigureAddedRules": "Aller à la configuration des règles ajoutées",
"editGlobalRules": "Modifier les règles globales",
"confirmForceCrashCore": "Voulez-vous vraiment forcer le plantage du cœur ?",
"confirmClearAllData": "Voulez-vous vraiment effacer toutes les données ?",
"loading": "Chargement…",
"yearsAgo": "{count, plural, =1{il y a 1 an} other{il y a {count} ans}}",
"monthsAgo": "{count, plural, =1{il y a 1 mois} other{il y a {count} mois}}",
"daysAgo": "{count, plural, =1{il y a 1 jour} other{il y a {count} jours}}",
"hoursAgo": "{count, plural, =1{il y a 1 heure} other{il y a {count} heures}}",
"minutesAgo": "{count, plural, =1{il y a 1 minute} other{il y a {count} minutes}}",
"justNow": "À l’instant",
"yearsAgoShort": "{count, plural, other{il y a {count} a}}",
"monthsAgoShort": "{count, plural, other{il y a {count} m}}",
"daysAgoShort": "{count, plural, other{il y a {count} j}}",
"hoursAgoShort": "{count, plural, other{il y a {count} h}}",
"minutesAgoShort": "{count, plural, other{il y a {count} min}}",
"noLongerRemind": "Ne plus me le rappeler",
"accessControlSettings": "Réglages du contrôle d’accès",
"turnOn": "Activer",
"turnOff": "Désactiver",
"vpnConfigChangeDetected": "Modification de la configuration liée au VPN détectée",
"restart": "Redémarrer",
"speedStatistics": "Statistiques de débit",
"resetPageChangesTip": "Cette page contient des modifications. Voulez-vous vraiment réinitialiser ?",
"customProfile": "Personnalisé",
"customProfileDesc": "Assembler des groupes de proxys et des règles à partir des proxys de l’app et d’autres profils",
"customProfileResources": "Ressources des profils personnalisés",
"customProfileResourcesDesc": "Les ressources des profils personnalisés regroupent des proxys, des fournisseurs de règles, des filtres et des jeux d’icônes.",
"unknownNetworkError": "Erreur réseau inconnue",
"backupFromNewerVersion": "Cette sauvegarde provient d’une version plus récente de l’app. Mettez l’app à jour avant de la restaurer",
"networkTimeoutError": "La requête a expiré. Vérifiez votre réseau ou votre proxy, puis réessayez",
"networkHostLookupError": "Impossible de résoudre l’adresse du serveur. Vérifiez que l’URL est correcte et que le DNS fonctionne",
"networkConnectionError": "Impossible de se connecter au serveur. Vérifiez votre connexion réseau ou vos réglages de proxy",
"networkTlsError": "Échec de la connexion sécurisée. Le certificat du serveur est peut-être invalide, ou la connexion est interceptée",
"networkCancelledError": "La requête a été annulée",
"networkAccessDeniedError": "Le serveur a refusé l’accès (HTTP {code}). Le lien a peut-être expiré, ou les identifiants sont incorrects",
"networkNotFoundError": "Rien n’a été trouvé à cette adresse (HTTP {code}). Vérifiez que l’URL est correcte",
"networkRateLimitedError": "Trop de requêtes (HTTP 429). Patientez un instant puis réessayez",
"networkServerError": "Le serveur a rencontré un problème (HTTP {code}). Réessayez plus tard",
"networkBadResponseError": "Le serveur a rejeté la requête (HTTP {code})",
"networkRequestFailed": "Échec de la requête réseau : {detail}",
"invalidBackupFile": "Fichier de sauvegarde non valide",
"pruneCache": "Nettoyer le cache",
"backupAndRestore": "Sauvegarde et restauration",
"restore": "Restaurer",
"restoreSuccess": "Restauration réussie",
"restoreOnlyConfig": "Restaurer uniquement les profils",
"restoreAllData": "Restaurer toutes les données",
"addProfile": "Ajouter un profil",
"delayTest": "Test de délai",
"proxyGroupEmpty": "Le groupe de proxys est vide",
"confirmExitWindow": "Voulez-vous vraiment quitter la fenêtre actuelle ?",
"optional": "Facultatif",
"maxFailedTimes": "Échecs maximum",
"testInterval": "Intervalle de test",
"excludeFilter": "Filtre d’exclusion",
"expectedStatus": "Statut attendu",
"strategy": "Stratégie",
"tolerance": "Tolérance",
"inputProxyGroupName": "Saisissez le nom du groupe de proxys",
"helperCorruptTip": "Service Helper indisponible ; le mode TUN ne peut pas être activé. Réinstallez FlClash pour le rétablir.",
"tunAuthorizationFailed": "TUN n’a pas pu être autorisé et a donc été désactivé.",
"sudoPasswordTitle": "Saisissez votre mot de passe pour sudo",
"coreBlockedByPolicyTip": "Windows a refusé d’exécuter FlClashCore.exe (erreur {code}). Une stratégie de contrôle des applications, telle que Smart App Control ou AppLocker, bloque les programmes non signés ; autorisez FlClash dans cette stratégie ou désactivez-la, puis réessayez.",
"coreBlockedBySmartAppControlTip": "Windows Smart App Control a bloqué FlClashCore.exe car il n’est pas signé. Ouvrez Sécurité Windows → Contrôle des applications et du navigateur → Paramètres de Smart App Control, choisissez Désactivé, puis relancez FlClash. Smart App Control ne peut pas être réactivé sans réinstaller Windows.",
"hideFromList": "Masquer de la liste",
"testWhenUsed": "Tester à l’utilisation",
"disableUDP": "Désactiver UDP",
"confirmDeleteProxyGroup": "Voulez-vous vraiment supprimer ce groupe de proxys ?",
"ruleEmpty": "La règle est vide",
"inputRuleContent": "Saisissez le contenu de la règle",
"ruleSet": "Jeu de règles",
"selectRuleSet": "Veuillez sélectionner un jeu de règles",
"splitStrategy": "Stratégie de répartition",
"selectSplitStrategy": "Veuillez sélectionner une stratégie de répartition",
"noResolveHostname": "Ne pas résoudre le nom d’hôte",
"matchSourceIp": "Faire correspondre l’IP source",
"basicInfo": "Infos de base",
"additionalParameters": "Paramètres supplémentaires",
"proxyType": "Type de proxy",
"basicStrategy": "Stratégies de base",
"editProxy": "Modifier le proxy",
"addNodes": "Ajouter des nœuds",
"addProxyGroup": "Ajouter un groupe de proxys",
"editProxyGroup": "Modifier le groupe de proxys",
"iconUrl": "URL de l’icône",
"iconRecords": "Historique des icônes",
"custom": "Personnalisé",
"ruleActionDomainDesc": "Correspond au domaine complet",
"ruleActionDomainSuffixDesc": "Correspond à un suffixe de domaine",
"ruleActionDomainKeywordDesc": "Correspond à un mot-clé de domaine",
"ruleActionDomainRegexDesc": "Correspond à une regex de domaine",
"ruleActionDomainWildcardDesc": "Correspondance par caractères génériques ; seuls * et ? sont pris en charge",
"ruleActionGeositeDesc": "Correspond aux domaines de Geosite",
"ruleActionIpCidrDesc": "Correspond à une plage d’adresses IP",
"ruleActionIpCidr6Desc": "Correspond à une plage d’adresses IP ; IP-CIDR6 n’est qu’un alias",
"ruleActionIpSuffixDesc": "Correspond à une plage de suffixes d’IP",
"ruleActionIpAsnDesc": "Correspond à l’ASN de l’IP",
"ruleActionGeoipDesc": "Correspond au code pays de l’IP",
"ruleActionSrcGeoipDesc": "Correspond au code pays de l’IP source",
"ruleActionSrcIpAsnDesc": "Correspond à l’ASN de l’IP source",
"ruleActionSrcIpCidrDesc": "Correspond à une plage d’adresses IP source",
"ruleActionSrcIpSuffixDesc": "Correspond à une plage de suffixes d’IP source",
"ruleActionDstPortDesc": "Correspond à la plage de ports de destination",
"ruleActionSrcPortDesc": "Correspond à la plage de ports source",
"ruleActionInPortDesc": "Correspond au port entrant",
"ruleActionInTypeDesc": "Correspond au type entrant",
"ruleActionInUserDesc": "Correspond au nom d’utilisateur entrant ; séparez plusieurs noms par /",
"ruleActionInNameDesc": "Correspond au nom entrant",
"ruleActionRematchNameDesc": "Correspond au nom de rematch ; séparez plusieurs noms par /",
"ruleActionProcessPathDesc": "Correspond au chemin complet du processus",
"ruleActionProcessPathRegexDesc": "Correspond par regex au chemin du processus",
"ruleActionProcessPathWildcardDesc": "Correspond par caractères génériques au chemin du processus ; seuls * et ? sont pris en charge",
"ruleActionProcessNameDesc": "Correspond au nom du processus ; correspond au nom du paquet sur Android",
"ruleActionProcessNameRegexDesc": "Correspond par regex au nom du processus ; correspond au nom du paquet sur Android",
"ruleActionProcessNameWildcardDesc": "Correspond par caractères génériques au nom du processus ; seuls * et ? sont pris en charge",
"ruleActionUidDesc": "Correspond à l’ID d’utilisateur Linux",
"ruleActionNetworkDesc": "Correspond à TCP ou UDP",
"ruleActionDscpDesc": "Correspond à la marque DSCP (entrant UDP tproxy uniquement)",
"ruleActionRuleSetDesc": "Référence un jeu de règles ; nécessite rule-providers",
"ruleActionAndDesc": "Règle logique ET",
"ruleActionOrDesc": "Règle logique OU",
"ruleActionNotDesc": "Règle logique NON",
"ruleActionSubRuleDesc": "Correspond vers une sous-règle ; attention aux parenthèses",
"ruleActionMatchDesc": "Correspond à toutes les requêtes, sans condition",
"contentNotEmpty": "Le contenu ne peut pas être vide",
"splitStrategyNotEmpty": "La stratégie de répartition ne peut pas être vide",
"timeout": "Expiré",
"delayFailed": "Échec",
"invalidSubRule": "{subRule} n’est pas un SUB_RULE valide",
"invalidPolicy": "{target} n’est pas une stratégie valide",
"invalidProxyProvider": "{providerName} n’est pas un fournisseur de proxys valide",
"invalidRuleSet": "{ruleSet} n’est pas un jeu de règles valide",
"invalidNetworkContent": "Seuls tcp ou udp sont pris en charge",
"invalidRangeContent": "Saisissez des nombres ou des plages tels que 80 ou 8000-9000, séparés par /",
"invalidDscpContent": "Une marque DSCP ne peut pas dépasser 63",
"invalidCidrContent": "Saisissez une plage d’IP au format CIDR, par exemple 192.168.0.0/16",
"invalidPortRangeContent": "Saisissez un port ou une plage, par exemple 443 ou 8000-9000",
"invalidHostContent": "Saisissez un domaine ou une adresse IP",
"invalidListenContent": "Saisissez une adresse et un port, par exemple 0.0.0.0:1053",
"invalidProxy": "{proxyName} n’est pas un proxy valide",
"locationPermissionRequired": "Autorisation de localisation requise",
"locationPermissionGuide": "1. Ouvrez Réglages système > Confidentialité et sécurité\n2. Choisissez Service de localisation\n3. Trouvez {appName} dans la liste et cochez-le\n\nUne fois terminé, revenez dans l’app pour continuer. Merci de votre coopération.",
"prerequisites": "Prérequis",
"ignoreBatteryOptimization": "Ignorer l’optimisation de la batterie",
"batteryOptimizationDesc": "Ignorer l’optimisation de la batterie maintient l’app active en arrière-plan.",
"locationPermission": "Autorisation de localisation",
"locationPermissionDesc": "Le système exige l’autorisation de localisation pour lire le nom du Wi-Fi. Sur Android, choisissez « Toujours autoriser », sinon le nom du Wi-Fi ne peut pas être lu lorsque l’app est en arrière-plan.",
"excludeSsids": "SSID exclus",
"excludeSsidsDesc": "Lorsque vous êtes connecté à un Wi-Fi dont le SSID est exclu, l’état d’exécution de l’app change automatiquement.",
"ssidsEmpty": "Les SSID sont vides",
"onDemand": "À la demande",
"onDemandDesc": "À la demande met le proxy en pause sur les Wi-Fi exclus.",
"locationPermissionDeniedMessage": "L’autorisation de localisation a été refusée ; le nom du Wi-Fi actuel ne peut donc pas être lu. Veuillez l’activer manuellement dans les réglages du système.",
"addSsid": "Ajouter un SSID",
"editSsid": "Modifier le SSID",
"authorized": "Autorisé",
"tapToAuthorize": "Appuyez pour autoriser",
"suspended": "Suspendu…",
"resourceUpdateInterval": "Intervalle de mise à jour automatique",
"resourceUpdateIntervalTip": "L’intervalle de mise à jour automatique doit être supérieur à 0",
"hours": "heures",
"minutes": "minutes",
"geoResources": "Geo",
"geoSkipped": "{name} est déjà à jour",
"geoUpdated": "{name} mis à jour",
"secondsCount": "{count, plural, =1{1 seconde} other{{count} secondes}}",
"proxiesCount": "{count, plural, =1{1 proxy} other{{label} proxys}}",
"rulesCount": "{count, plural, =1{1 règle} other{{label} règles}}",
"proxyGroupsCount": "{count, plural, =1{1 groupe de proxys} other{{label} groupes de proxys}}",
"changelogBreaking": "Changements majeurs",
"changelogFeatures": "Nouveautés",
"changelogFixes": "Corrections de bugs",
"changelogPerformance": "Performances",
"changelogReverts": "Annulations",
"close": "Fermer",
"back": "Retour",
"minimize": "Réduire",
"maximize": "Agrandir",
"unmaximize": "Restaurer",
"pinWindow": "Épingler la fenêtre",
"unpinWindow": "Désépingler la fenêtre",
"exitFullScreen": "Quitter le plein écran",
"torch": "Lampe torche",
"pickFromAlbum": "Choisir dans l’album",
"blockConnection": "Bloquer la connexion",
"closeConnections": "Fermer les connexions",
"scrollToSelected": "Défiler jusqu’à la sélection",
"showMore": "Développer",
"showLess": "Réduire",
"previousMatch": "Résultat précédent",
"nextMatch": "Résultat suivant",
"clearSearch": "Effacer la recherche",
"noSearchResults": "Aucun résultat",
"showPassword": "Afficher le mot de passe",
"hidePassword": "Masquer le mot de passe",
"authorize": "Autoriser",
"installedAppsPermissionRequired": "Autorisation de la liste des apps requise",
"installedAppsPermissionDesc": "Ce système masque la liste des apps installées tant que l’autorisation n’est pas accordée. Accordez-la pour configurer le proxy par app.",
"installedAppsPermissionDeniedMessage": "L’autorisation de la liste des apps a été refusée ; les apps installées ne peuvent donc pas être listées. Veuillez l’accorder manuellement dans les réglages du système.",
"outboundIp": "IP sortante",
"ipAddress": "Adresse IP",
"ipQualityLevel": "Niveau",
"ipQualityGood": "Bonne",
"ipQualityNormal": "Normale",
"ipQualityRisky": "À risque",
"ipType": "Type",
"ipTypeResidential": "Résidentielle",
"ipTypeMobile": "Réseau mobile",
"ipTypeBusiness": "Entreprise",
"ipTypeHosting": "Centre de données",
"ipFlags": "Indicateurs",
"ipFlagProxy": "Proxy",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "Antécédents d’abus",
"ipOrganization": "Organisation",
"ipAsn": "ASN",
"ipQualitySource": "Réponse de",
"ipQualitySources": "Sources",
"ipQualityFailed": "Impossible de déterminer le type de l’IP",
"ipQualityRetry": "Vérifier à nouveau",
"ipSourceNoType": "Aucun type",
"ipSourceRateLimited": "Limite de débit atteinte",
"ipSourceIpMismatch": "IP sortante différente",
"hideIp": "Masquer l’IP",
"editorUnavailable": "Éditeur indisponible",
"replace": "Remplacer",
"replaceAll": "Tout remplacer",
"cameraPermissionRequired": "Autorisation de l’appareil photo requise",
"cameraPermissionDesc": "Autorisez l’accès à l’appareil photo dans les réglages du système pour scanner des codes QR, ou choisissez une image de code QR dans l’album.",
"cameraUnavailable": "Appareil photo indisponible",
"retry": "Réessayer",
"invalidProfileQrcode": "Ce code QR ne contient pas de lien de profil",
"dnsQueries": "Requêtes DNS",
"recordType": "Type d’enregistrement",
"answers": "Réponses",
"responseCode": "Code de réponse",
"error": "Erreur",
"initiator": "Initiateur",
"cache": "Cache",
"yes": "Oui",
"no": "Non",
"nonTextProviderFile": "Cette ressource externe n’est pas un fichier texte",
"addCustomProxy": "Ajouter un proxy",
"definitionNotMap": "La configuration doit être un mappage YAML avec un nom et un type",
"ruleTextInvalid": "La règle doit commencer par un type pris en charge, par exemple DOMAIN,example.com,DIRECT",
"ruleListInvalid": "Les règles doivent former une liste YAML avec une règle par élément, par exemple - DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "Aucun proxy n’a pu être lu dans les liens",
"shareLinkUnreadable": "Aucun proxy n’a pu être lu dans {link}",
"proxiesProfileLabel": "{name} et autres ({count} proxys)",
"singleShareLinkOnly": "Saisissez un seul lien ici ; pour en modifier plusieurs, utilisez la modification rapide de la liste",
"username": "Nom d’utilisateur",
"emptyFallback": "Repli si vide",
"defaultSelected": "Sélection par défaut",
"hashByInUser": "Hachage par utilisateur entrant",
"appProxiesEmpty": "Aucun proxy local",
"nodes": "Nœuds",
"localProxies": "Proxys locaux",
"customIssueEmptyName": "Le nom est vide",
"customIssueReservedName": "{name} est un nom de stratégie intégré et ne peut pas être utilisé ici",
"customIssueDuplicateName": "Le nom {name} est déjà utilisé par un autre proxy ou groupe de proxys",
"customIssueCoreRejected": "Le cœur ne peut pas analyser ce proxy : {message}",
"customIssueMissingProxies": "Ces proxys ou stratégies n’existent pas : {names}",
"customIssueMissingProviders": "Ces fournisseurs de proxys n’existent pas : {names}",
"customIssueNoProxySource": "Aucun proxy ni fournisseur de proxys n’est sélectionné ; le cœur rejette donc ce groupe",
"customIssueGroupLoop": "Les groupes de proxys se référencent mutuellement en boucle : {path}",
"customIssueInvalidEmptyFallback": "Le repli si vide doit être un proxy, mais {name} n’existe pas ou est un groupe de proxys",
"customIssueInvalidFilter": "{name} n’est pas une regex valide : {message}",
"customIssueMissingDialer": "Le proxy de connexion {name} n’existe pas",
"customIssueDialerLoop": "{proxy} se connecte via {target}, qui renvoie à {proxy} ; la connexion boucle",
"dialerProxyLoopStopped": "La connexion via {name} renvoie au proxy qui l’utilise ; la connexion a donc été arrêtée",
"customIssuesSummary": "{count} éléments posent problème, et l’application de ce profil peut échouer",
"providerInUse": "{label} est encore utilisé par {profiles}, qui ne fonctionneront plus correctement s’il est supprimé. Voulez-vous vraiment le supprimer ?",
"proxyGroupInUse": "{label} est encore nommé par des règles, des groupes de proxys, des proxys de connexion, le DNS ou le NTP de ce profil, qui cesseront de fonctionner s’il est supprimé. Voulez-vous vraiment le supprimer ?",
"proxiesRemovedTip": "Ces proxys seront supprimés : {removed}. Une ligne dont le nom a changé compte comme un nouveau proxy, et les profils personnalisés qui nomment les proxys supprimés les signaleront comme manquants. Appliquer quand même ?",
"proxiesReplacedTip": "Ces proxys seront supprimés : {removed}. Ceux-ci seront ajoutés : {added}. Une ligne dont le nom a changé compte comme un nouveau proxy, et les profils personnalisés qui nomment les proxys supprimés les signaleront comme manquants. Appliquer quand même ?",
"fontSize": "Taille",
"large": "Grande",
"extraLarge": "Très grande",
"switchProfile": "Changer de profil",
"lastUpdated": "Dernière mise à jour",
"filters": "Filtres",
"label": "Libellé",
"regex": "Regex",
"matchCase": "Respecter la casse",
"iconSets": "Jeux d’icônes",
"invalidIconSet": "Jeu d’icônes non valide",
"recommendedIcons": "Suggestions",
"localImage": "Image locale",
"iconSetsEmptyTip": "Aucun jeu d’icônes pour le moment. Ajoutez-en dans Ressources des profils personnalisés → Jeux d’icônes",
"outbound": "Sortant",
"strictRoute": "Route stricte",
"congestionController": "Contrôle de congestion TCP",
"congestionControllerDesc": "Le contrôle de congestion TCP ne fonctionne qu’avec la pile mips.",
"routeExcludeAddress": "Adresses de route exclues",
"keepAliveIdle": "Inactivité keep-alive TCP",
"disableKeepAlive": "Désactiver le keep-alive TCP",
"routingMark": "Marque de routage",
"strictRouteDesc": "La route stricte empêche le trafic tel que le DNS de contourner TUN, et les autres appareils ne peuvent plus joindre celui-ci.",
"initFailed": "Échec du démarrage",
"initFailedTip": "FlClash a rencontré une erreur au démarrage et ne peut pas continuer. Copiez les détails pour signaler le problème. Si cela se reproduit, effacer les données permet généralement de relancer l’app.",
"errorDetails": "Détails de l’erreur",
"stackTrace": "Trace de pile",
"clearDataAndExitTip": "Cela supprime tous les profils, réglages et données locales, puis ferme l’app. Rouvrez-la pour repartir de zéro.",
"clearDataFailed": "Impossible d’effacer les données : {detail}",
"bypassPrivateRoute": "Contourner les adresses privées",
"bypassPrivateRouteDesc": "Avec « Contourner les adresses privées » activé, les adresses LAN, de bouclage et multicast ne passent pas par TUN.",
"disableIcmpForwarding": "Désactiver le transfert ICMP",
"disableIcmpForwardingDesc": "Avec le transfert ICMP désactivé, TUN répond lui-même au ping, qui n’indique donc plus la latence réelle.",
"excludeInterface": "Interfaces exclues",
"excludeInterfaceDesc": "Le trafic qui arrive sur les interfaces exclues, par exemple docker0, ne passe pas par TUN.",
"includeInterface": "Interfaces incluses",
"includeInterfaceDesc": "Seul le trafic arrivant sur les interfaces incluses passe par TUN, et les interfaces exclues ne s’appliquent plus.",
"includeUid": "ID utilisateur inclus",
"includeUidDesc": "Seul le trafic des processus des ID utilisateur inclus passe par TUN.",
"excludeUid": "ID utilisateur exclus",
"excludeUidDesc": "Le trafic des processus des ID utilisateur exclus ne passe pas par TUN.",
"excludeSrcPort": "Ports source exclus",
"excludeSrcPortDesc": "Le trafic issu des ports source exclus ne passe pas par TUN.",
"excludeDstPort": "Ports de destination exclus",
"excludeDstPortDesc": "Le trafic vers les ports de destination exclus ne passe pas par TUN.",
"udpTimeout": "Délai UDP",
"udpTimeoutDesc": "Le délai UDP est en secondes et ne fonctionne qu’avec les piles gvisor, system et mixed.",
"dnsHijackDesc": "Sans cette entrée, TUN détourne toutes les requêtes DNS vers le port 53 (any:53).",
"tunOverrideDesc": "Les entrées ajoutées ici remplacent les réglages TUN de tous les profils ; tout ce qui n’est pas ajouté garde le réglage du profil. Les entrées qu’un profil personnalisé ajoute lui-même priment.",
"profileTunSettingsDesc": "S’applique uniquement à ce profil et prime sur les entrées de remplacement TUN des réglages réseau.",
"settingsCount": "{count, plural, =1{1 réglage} other{{count} réglages}}",
"sectionsInEffect": "{count} actifs",
"itemsCount": "{count, plural, =1{1 élément} other{{count} éléments}}",
"additionalPrefix": "Préfixe du nom",
"additionalSuffix": "Suffixe du nom",
"skipCertVerify": "Ignorer la vérification du certificat",
"ipVersion": "Version d’IP",
"addFilters": "Ajouter des filtres",
"failedItem": "{label} : {message}",
"failedCount": "{count} éléments en échec",
"importConfigInvalid": "Ce fichier n’est pas une configuration importable",
"importConfigReplaceTip": "L’importation remplace les groupes de proxys, les règles et les réglages actuels de ce profil",
"networkName": "Nom du réseau",
"networkSecret": "Secret du réseau",
"peers": "Pairs",
"virtualIpv4": "IPv4 virtuelle",
"hostname": "Nom d’hôte",
"exitNodes": "Nœuds de sortie",
"cipher": "Chiffrement",
"protocol": "Protocole",
"protocolParam": "Paramètre du protocole",
"obfs": "Obfuscation",
"obfsParam": "Paramètre d’obfuscation",
"obfsPassword": "Mot de passe d’obfuscation",
"obfsMode": "Mode d’obfuscation",
"obfsHost": "Hôte d’obfuscation",
"plugin": "Plugin",
"pluginMode": "Mode du plugin",
"pluginHost": "Hôte du plugin",
"privateKey": "Clé privée",
"publicKey": "Clé publique",
"privateKeyPassphrase": "Phrase secrète de la clé privée",
"preSharedKey": "Clé prépartagée",
"secretKey": "Clé",
"transport": "Transport",
"uploadBandwidth": "Bande passante d’envoi",
"downloadBandwidth": "Bande passante de réception",
"protocolVersion": "Version du protocole",
"portHopping": "Saut de port",
"hopInterval": "Intervalle de saut",
"congestionControl": "Contrôle de congestion",
"udpRelayMode": "Mode de relais UDP",
"allowedIps": "IP autorisées",
"remoteDnsResolve": "Résolution DNS distante",
"clientFingerprint": "Empreinte client",
"required": "Obligatoire",
"ageSecretKey": "Clé secrète age",
"ageEncryptedProfile": "Profil chiffré",
"ageSecretKeyRequiredTip": "Ce profil est chiffré avec age et nécessite une clé secrète age pour être déchiffré",
"currentOutboundDesc": "Contient le bouton de démarrage et ne peut pas être retiré",
"running": "En cours d’exécution",
"notRunning": "À l’arrêt",
"trafficPeak": "Pic {value}",
"trafficTotal": "Total {value}",
"ipQualityShort": "Qualité",
"ipTypeUnknown": "Type inconnu",
"tapIconToRetry": "Appuyez sur l’icône pour réessayer",
"networkTakeover": "Prise en charge du trafic",
"portValue": "Port {port}",
"serviceAbnormalCount": "{count, plural, =1{1 problème} other{{count} problèmes}}",
"serviceAllAvailable": "Tout est disponible",
"serviceShownOnDashboard": "Affiché sur le tableau de bord",
"groupTimeoutCount": "{count} expirés",
"groupCountUnit": "groupes",
"groupCount": "{count, plural, =1{1 groupe} other{{count} groupes}}",
"proxyGroupsAfterStart": "Les groupes s’affichent ici une fois le cœur démarré",
"usedShort": "utilisé",
"profileUpdatedAt": "Mis à jour {time}",
"profileExpiresIn": "Expire le {date}, {days, plural, =1{1 jour restant} other{{days} jours restants}}",
"profileDaysLeft": "{days, plural, =1{1 jour restant} other{{days} jours restants}}",
"target": "Cible",
"outboundExit": "Sortie",
"duration": "Durée",
"traffic": "Trafic",
"totalDownload": "Reçu",
"totalUpload": "Envoyé",
"initiatorShort": "De",
"result": "Résultat",
"elapsed": "Durée",
"connectionStats": "Statistiques de connexion",
"connectionStatsDesc": "Connexions en direct, requêtes récentes et requêtes DNS",
"memoryInfoDesc": "Mémoire utilisée par l’app et le cœur",
"editDashboard": "Modifier le tableau de bord",
"done": "Terminé",
"dashboardAddableSection": "Disponibles à l’ajout",
"customImportDroppedSubRules": "{count, plural, =1{1 règle SUB-RULE a été abandonnée, car les profils personnalisés ne prennent pas en charge les sous-règles} other{{label} règles SUB-RULE ont été abandonnées, car les profils personnalisés ne prennent pas en charge les sous-règles}}",
"autoInclude": "Inclure automatiquement",
"allReferencedProxies": "Tous les proxys référencés",
"allReferencedProxyProviders": "Tous les fournisseurs de proxys référencés"
}
+490 -168
View File
@@ -9,19 +9,31 @@
"profiles": "プロファイル",
"tools": "ツール",
"logs": "ログ",
"logsDesc": "キャプチャしたログの記録",
"resources": "リソース",
"resourcesDesc": "外部リソースの関連情報",
"trafficUsage": "トラフィック統計",
"networkSpeed": "ネットワーク速度",
"outboundMode": "アウトバウンドモード",
"networkDetection": "ネットワーク検出",
"serviceStatus": "サービスの状態",
"serviceRestricted": "アクセス制限",
"serviceAvailable": "利用可能",
"serviceUnavailable": "利用不可",
"serviceFailed": "検出に失敗しました",
"serviceDisallowedIsp": "許可されていない ISP",
"serviceBlocked": "ブロック済み",
"serviceUnsupportedRegion": "対象外の地域",
"serviceOriginalsOnly": "オリジナル作品のみ",
"serviceComingSoon": "近日提供予定",
"servicePending": "未検査",
"serviceManage": "サービスを管理",
"serviceCheck": "検査",
"serviceCheckAll": "すべて検査",
"serviceCheckedAt": "{time} に検査",
"upload": "アップロード",
"download": "ダウンロード",
"usedTraffic": "使用済みトラフィック",
"totalTraffic": "合計トラフィック",
"expireTime": "有効期限",
"nullProfileDesc": "プロファイルがありません。先にプロファイルを追加してください",
"nullProfileDesc": "プロファイルを追加して始めましょう",
"settings": "設定",
"language": "言語",
"defaultText": "デフォルト",
@@ -29,34 +41,31 @@
"other": "その他",
"about": "アプリについて",
"en": "英語",
"es": "スペイン語",
"fr": "フランス語",
"ja": "日本語",
"ko": "韓国語",
"ru": "ロシア語",
"zhCN": "簡体字中国語",
"theme": "テーマ",
"themeDesc": "ダークモードの設定と色の調整",
"override": "上書き",
"extend": "拡張",
"runtimeConfig": "ランタイム設定",
"allowLan": "LANプロキシ",
"allowLanDesc": "LAN経由でのプロキシ利用を許可します",
"tun": "TUN",
"tunDesc": "管理者モードでのみ有効",
"tunDesc": "TUN は管理者モードでのみ有効です。",
"minimizeOnExit": "終了時に最小化",
"minimizeOnExitDesc": "システム標準の終了動作を変更します",
"autoLaunch": "自動起動",
"autoLaunchDesc": "システム起動時に自動的に起動します",
"silentLaunch": "サイレント起動",
"silentLaunchDesc": "バックグラウンドで起動します",
"silentLaunchDesc": "サイレント起動ではウィンドウを表示しません。",
"autoRun": "自動実行",
"autoRunDesc": "アプリを開いたときに自動的に実行します",
"autoRunDesc": "自動実行はアプリを開いたときにプロキシを開始します。",
"logcat": "ログキャプチャ",
"logcatDesc": "無効にするとログの入り口が非表示になります",
"logcatDesc": "ログキャプチャを無効にすると、ログの入り口が非表示になります。",
"autoCheckUpdate": "更新の自動チェック",
"autoCheckUpdateDesc": "アプリ起動時に更新を自動的にチェックします",
"checkCertificate": "TLS証明書を検証",
"checkCertificateDesc": "信頼できない証明書を拒否します。無効にすると、サブスクリプションやバックアップが中間者攻撃にさらされます",
"checkCertificateDesc": "TLS 証明書の検証を無効にすると、サブスクリプションとバックアップが中間者攻撃にさらされます。",
"accessControl": "アクセス制御",
"accessControlDesc": "プロキシを利用するアプリを設定します",
"application": "アプリケーション",
"applicationDesc": "アプリケーション関連の設定を変更します",
"accessControlDesc": "アクセス制御では、プロキシを利用するアプリを選びます。",
"edit": "編集",
"confirm": "OK",
"update": "更新",
@@ -67,15 +76,17 @@
"qrcode": "QRコード",
"qrcodeDesc": "QRコードをスキャンしてプロファイルを取得します",
"url": "URL",
"urlDesc": "URLからプロファイルを取得します",
"link": "リンク",
"linkDesc": "サブスクリプションリンクまたはプロキシの共有リンクからプロファイルを取得します",
"file": "ファイル",
"fileDesc": "プロファイルファイルを直接アップロードします",
"name": "名前",
"profileNameNullValidationDesc": "プロファイル名を入力してください",
"rename": "名前を変更",
"profileUrlNullValidationDesc": "プロファイルのURLを入力してください",
"profileUrlInvalidValidationDesc": "有効なプロファイルURLを入力してください",
"autoUpdate": "自動更新",
"autoUpdateInterval": "自動更新間隔(分)",
"autoUpdateInterval": "自動更新間隔",
"profileAutoUpdateIntervalNullValidationDesc": "自動更新間隔を入力してください",
"profileAutoUpdateIntervalInvalidValidationDesc": "有効な間隔を入力してください",
"themeMode": "テーマモード",
@@ -84,7 +95,7 @@
"auto": "自動",
"light": "ライト",
"dark": "ダーク",
"importFromURL": "URLからインポート",
"importFromLink": "リンクからインポート",
"submit": "送信",
"createProfileFromUrlTip": "{url} からプロファイルを作成しますか?",
"create": "作成",
@@ -103,12 +114,15 @@
"show": "表示",
"exit": "終了",
"systemProxy": "システムプロキシ",
"safeMode": "セーフモード",
"safeModeAppTitle": "{appName}(セーフモード)",
"project": "プロジェクト",
"core": "コア",
"tabAnimation": "タブアニメーション",
"desc": "ClashMetaベースのマルチプラットフォーム対応プロキシクライアント。シンプルで使いやすく、オープンソースで広告もありません。",
"startVpn": "VPNを起動しています...",
"stopVpn": "VPNを停止しています...",
"startVpn": "VPNを起動しています…",
"stopVpn": "VPNを停止しています…",
"localNetworkDeniedTip": "ローカルネットワークの権限が拒否されたため gvisor スタックを使用します。LAN にはアクセスできません。",
"compatible": "互換モード",
"notSelectedTip": "現在のプロキシグループは選択できません",
"changeProxyFailedTip": "プロキシの切り替えに失敗したため、前回の選択に戻しました",
@@ -127,8 +141,8 @@
"addressTip": "有効なWebDAVアドレスを入力してください",
"password": "パスワード",
"authentication": "認証",
"authenticationDesc": "ローカルプロキシポートに認証を要求し、他のアプリによる無断利用を防ぎます",
"authenticationSystemProxyDesc": "認証が有効な間は適用されません",
"authenticationDesc": "認証を有効にすると、ローカルプロキシポートの利用にユーザー名とパスワードが必要になります。",
"authenticationSystemProxyDesc": "認証が有効な間、システムプロキシは適用されません。",
"checkUpdate": "更新を確認",
"discoverNewVersion": "新しいバージョンが見つかりました",
"checkUpdateError": "すでに最新バージョンです",
@@ -137,40 +151,37 @@
"country": "地域",
"search": "検索",
"allowBypass": "アプリによるVPNバイパスを許可",
"allowBypassDesc": "有効にすると、一部のアプリがVPNをバイパスできます",
"externalController": "外部コントローラー",
"externalControllerDesc": "有効にすると、ポート9090でClashコアを制御できます",
"ipv6Desc": "有効にすると、IPv6トラフィックを受信できます",
"externalControllerDesc": "外部コントローラーを有効にすると、ポート 9090 でコアを制御できます。",
"externalControllerSecret": "外部コントローラーのシークレット",
"ipv6Desc": "IPv6 トラフィックは IPv6 が有効なときだけ受信します。",
"app": "アプリ",
"general": "一般",
"systemProxyDesc": "システムプロキシを設定します",
"startupAndBackground": "起動とバックグラウンド",
"requestsAndUpdates": "リクエストと更新",
"logsAndDiagnostics": "ログと診断",
"userAgent": "User-Agent",
"unifiedDelay": "統一遅延",
"unifiedDelayDesc": "ハンドシェイクなどの余分な遅延を除きます",
"tcpConcurrent": "TCP同時接続",
"tcpConcurrentDesc": "有効にすると、TCPの同時接続を許可します",
"geodataLoader": "Geo低メモリモード",
"geodataLoaderDesc": "有効にすると、低メモリのGeoローダーを使用します",
"requests": "リクエスト",
"requestsDesc": "最近のリクエスト記録を表示します",
"recentRequests": "最近のリクエスト",
"findProcessMode": "プロセス検出",
"init": "初期化",
"infiniteTime": "無期限",
"connections": "接続",
"liveConnections": "リアルタイム接続",
"inbound": "インバウンド",
"connectionsDesc": "現在の接続データを表示します",
"intranetIP": "イントラネットIP",
"view": "表示",
"cut": "切り取り",
"copy": "コピー",
"clipboardWriteFailed": "クリップボードにコピーできませんでした。選択範囲が大きすぎる可能性があります",
"paste": "貼り付け",
"testUrl": "テストURL",
"sync": "同期",
"exclude": "最近のタスクから隠す",
"excludeDesc": "バックグラウンド時に、最近のタスクからアプリを隠します",
"expand": "標準",
"shrink": "コンパクト",
"min": "最小",
"compact": "コンパクト",
"tab": "タブ",
"list": "リスト",
"delay": "遅延",
@@ -182,19 +193,19 @@
"go": "開く",
"externalLink": "外部リンク",
"otherContributors": "その他の貢献者",
"appIconDesign": "アプリアイコンのデザイン",
"autoCloseConnections": "接続を自動的に閉じる",
"autoCloseConnectionsDesc": "ノードの切り替え後、接続を自動的に閉じます",
"onlyStatisticsProxy": "プロキシのみ集計",
"onlyStatisticsProxyDesc": "有効にすると、プロキシのトラフィックのみを集計します",
"autoCloseConnectionsDesc": "プロキシを切り替えると、既存の接続を自動的に閉じます。",
"onlyStatisticsProxy": "プロキシトラフィックのみ集計",
"showNotificationStopAction": "通知に停止ボタンを表示",
"showNotificationStopActionDesc": "常駐通知に停止ボタンを表示します。これが原因で通知が常に展開される場合はオフにしてください",
"pureBlackMode": "ピュアブラックモード",
"sidebarBlur": "サイドバーのぼかし",
"sidebarBlurDesc": "ウィンドウ背後のデスクトップをぼかしてサイドバーに透過します",
"liquidGlass": "リキッドグラス",
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
"entries": " 件",
"local": "ローカル",
"remote": "リモート",
"remoteBackupDesc": "WebDAVにデータをバックアップします",
"localBackupDesc": "ローカルにデータをバックアップします",
"mode": "モード",
"time": "時刻",
"source": "ソース",
@@ -205,94 +216,174 @@
"layout": "レイアウト",
"tight": "コンパクト",
"standard": "標準",
"ultra": "最高",
"loose": "ゆったり",
"profilesSort": "プロファイルの並べ替え",
"start": "開始",
"runTime": "稼働時間",
"stop": "停止",
"dnsDesc": "DNS関連の設定を更新します",
"key": "キー",
"value": "値",
"hostsDesc": "Hostsを追加します",
"vpnTip": "変更はVPNの再起動後に有効になります",
"vpnEnableDesc": "VpnServiceでシステムの全トラフィックを自動的にルーティングします",
"vpnEnableDesc": "VPN は VpnService でシステムの全トラフィックを自動的にルーティングします。",
"options": "オプション",
"loopback": "ループバック解除ツール",
"loopbackDesc": "UWPのループバック解除に使用します",
"enabled": "有効",
"disabled": "無効",
"loopback": "UWP ループバック解除",
"providers": "外部リソース",
"proxyProviders": "プロキシプロバイダー",
"ruleProviders": "ルールプロバイダー",
"providerUrlTip": "リモートリソースのみ対応しています",
"ruleSetEmptyTip": "ルールセットに使用できるエントリがありません",
"ruleSetMixedTip": "ルールセットにドメインと IP 範囲が混在しているため、種類を判別できません",
"ruleSetInvalidTip": "この mrs ファイルを読み込めません",
"subscriptionInfo": "サブスクリプション情報",
"overrideDns": "DNSを上書き",
"overrideDnsDesc": "有効にすると、プロファイル内のDNS設定を上書きします",
"ntpStatusDesc": "NTP を有効にすると、システムクロックではなく NTP サーバーから時刻を取得します。",
"server": "サーバー",
"ntpInterval": "同期間隔(分)",
"dialerProxy": "ダイヤラープロキシ",
"dialerProxyDesc": "ダイヤラープロキシは NTP サーバーへの接続に使用するアウトバウンドです。",
"writeToSystem": "システムに書き込む",
"writeToSystemDesc": "システムに書き込むを有効にするとシステムクロックも設定します。Android では無視されます。",
"sniffer": "スニッファー",
"snifferStatusDesc": "スニッファーは TLS・HTTP・QUIC の通信からドメインを読み取り、ルールで照合できるようにします。",
"overrideDestination": "宛先を上書き",
"overrideDestinationDesc": "宛先を上書きすると、元のアドレスではなく検出したドメインに接続します。",
"forceDnsMapping": "DNSマッピングを強制",
"forceDnsMappingDesc": "DNS マッピングを強制すると、DNS マッピングでドメインを得た接続もスニッフィングします。",
"parsePureIp": "純粋なIPを解析",
"parsePureIpDesc": "純粋な IP を解析すると、IP アドレスしかない接続をスニッフィングします。",
"forceDomain": "強制するドメイン",
"forceDomainDesc": "強制するドメインもスニッフィングします。ドメインを持つその他の接続は対象外です。",
"skipDomain": "スキップするドメイン",
"skipDomainDesc": "スキップするドメインにあるドメインを検出しても使用しません。",
"skipSrcAddress": "スキップする送信元アドレス",
"skipDstAddress": "スキップする宛先アドレス",
"sniffProtocols": "プロトコル",
"ports": "ポート",
"overrideEntries": "上書き項目",
"addOverrideEntry": "上書き項目を追加",
"dnsOverrideDesc": "ここで追加した項目は、すべての URL・ファイルプロファイルの DNS を上書きします。追加していない項目はプロファイル自身の設定のままです。カスタムプロファイルの DNS はその内容で設定します。",
"settingEntries": "設定項目",
"addSettingEntry": "設定項目を追加",
"profileSettingsDesc": "このプロファイルにのみ適用されます。追加していない項目はデフォルト値を使用します。",
"healthCheck": "ヘルスチェック",
"discardChanges": "変更を破棄しますか?",
"quickEdit": "クイック編集",
"status": "状態",
"statusDesc": "無効にすると、システムDNSを使用します",
"preferH3Desc": "DoHでHTTP/3を優先します",
"statusDesc": "DNS を無効にすると、システム DNS を使用します。",
"respectRules": "ルールに従う",
"respectRulesDesc": "DNS接続がルールに従います。proxy-server-nameserverの設定が必要です",
"respectRulesDesc": "ルールに従うを有効にすると DNS 接続がルールに従います。Proxy Server Nameserver の設定が必要です。",
"dnsMode": "DNSモード",
"fakeipRange": "Fake-IP範囲",
"fakeipFilter": "Fake-IPフィルター",
"defaultNameserver": "デフォルトネームサーバー",
"defaultNameserverDesc": "DNSサーバーの名前解決に使用します",
"nameserver": "ネームサーバー",
"nameserverDesc": "ドメインの名前解決に使用します",
"useHosts": "Hostsを使用",
"useSystemHosts": "システムのHostsを使用",
"nameserverPolicy": "ネームサーバーポリシー",
"nameserverPolicyDesc": "ドメインごとのネームサーバーポリシーを指定します",
"proxyNameserver": "プロキシネームサーバー",
"proxyNameserverDesc": "プロキシノードのドメイン解決に使用します",
"fallback": "フォールバック",
"fallbackDesc": "通常は国外のDNSを使用します",
"fallbackFilter": "フォールバックフィルター",
"geoipCode": "GeoIPコード",
"ipcidr": "IP/CIDR",
"domain": "ドメイン",
"listenRoutingMark": "リッスンのルーティングマーク",
"listenRoutingMarkDesc": "リッスンのルーティングマークは Linux でのみ有効です。",
"ipv6Timeout": "IPv6タイムアウト(ms)",
"cacheAlgorithm": "キャッシュアルゴリズム",
"cacheMaxSize": "キャッシュサイズ",
"fakeipRange6": "Fake-IP範囲(IPv6)",
"fakeipFilterMode": "Fake-IPフィルターモード",
"fakeipFilterModeDesc": "Fake-IP フィルターモードでは、blacklist は一致を除外、whitelist は一致のみ、rule はルールで判定します。",
"fakeipTtl": "Fake-IP TTL",
"reset": "リセット",
"actionView": "表示/非表示",
"actionStart": "開始/停止",
"actionMode": "モード切替",
"actionProxy": "システムプロキシ",
"actionTun": "TUN",
"actionRuleMode": "ルールモード",
"actionGlobalMode": "グローバルモード",
"actionDirectMode": "ダイレクトモード",
"actionDelayTest": "すべての遅延をテスト",
"actionUpdateProfiles": "プロファイルを更新",
"disclaimer": "免責事項",
"disclaimerDesc": "本ソフトウェアは、学習・交流や研究などの非商用目的でのみ使用できます。商用目的での使用は固く禁じられています。いかなる商業行為も本ソフトウェアとは一切関係ありません。",
"disclaimerDesc": "FlClash(以下「本ソフトウェア」)をご利用になる前に、本声明の内容をよくお読みになり、十分にご理解ください。「同意する」をタップすると、以下のすべての条項を読み、理解し、承諾したものとみなされます。同意いただけない場合は「終了」をタップし、本ソフトウェアの使用を中止してください。",
"disclaimerSoftwareTitle": "ソフトウェアの性質",
"disclaimerSoftwareContent": "本ソフトウェアは Clash.Meta(mihomo)コアをベースとしたオープンソースのネットワークプロキシクライアントであり、設定管理、ルールによる振り分け、トラフィック転送などのローカルツール機能のみを提供します。",
"disclaimerNoServiceStatement": "本ソフトウェア自体は、プロキシサーバー、ノード、サブスクリプション、ネットワーク接続サービスを一切提供しておらず、それらのサービス提供者と提携、代理、保証の関係はありません。",
"disclaimerUsageTitle": "使用の制限",
"disclaimerUsageContent": "本ソフトウェアは学習、交流、技術研究などの非商用目的でのみ使用できます。有償配布、抱き合わせ販売、商用サービスの一部としての利用、本ソフトウェアの名義での事業活動など、あらゆる商用目的での使用を固く禁じます。いかなる商業行為も本ソフトウェアおよび開発者とは一切関係ありません。\n\nお住まいの国または地域の法令に違反する目的での使用を固く禁じます。これには、法に基づくネットワークアクセス制限の回避、違法情報の拡散、サイバー攻撃、他者の権利の侵害などが含まれますが、これらに限りません。",
"disclaimerResponsibilityTitle": "利用者の責任",
"disclaimerResponsibilityContent": "お住まいの国または地域で本ソフトウェアの使用が合法であることはご自身で確認する必要があり、本ソフトウェアを使用したすべての行為とその結果について、ご自身が単独で法的責任を負います。\n\nインポートするサブスクリプション、ノード、設定はご自身の判断で選択したものです。その出所の適法性、内容の安全性、サービスの安定性については、利用者と各提供者の間で解決してください。",
"disclaimerThirdPartyTitle": "サードパーティのコンテンツ",
"disclaimerThirdPartyContent": "サブスクリプションリンク、設定ファイル、ルールセット、スクリプト、外部リソース、外部リンクはすべて第三者が提供するものであり、開発者はその適法性、正確性、安全性、可用性を審査または保証することはできず、また行いません。\n\nサードパーティのコンテンツの使用に起因する情報漏えい、財産上の損失、アカウント停止その他の損失は、利用者と第三者の間で解決するものとし、開発者は一切責任を負いません。",
"disclaimerWarrantyTitle": "無保証",
"disclaimerWarrantyContent": "本ソフトウェアは「現状のまま」かつ「提供可能な範囲で」提供され、商品性、特定目的への適合性、非侵害、継続的な可用性、エラーやセキュリティ上の脆弱性がないことの保証を含め、明示または黙示を問わずいかなる保証も伴いません。\n\n開発者は、本ソフトウェアが利用者の要件を満たすこと、また中断やエラーなく動作することを保証しません。",
"disclaimerLiabilityTitle": "責任の制限",
"disclaimerLiabilityContent": "適用法で認められる最大限の範囲において、開発者およびすべての貢献者は、本ソフトウェアの使用または使用不能に起因する直接的、間接的、偶発的、特別、懲罰的または結果的な損害(データの消失、機器の損傷、ネットワーク障害、業務の中断、逸失利益、およびそれに起因する法的紛争を含むがこれらに限らない)について、その可能性を知らされていた場合であっても、一切責任を負いません。",
"disclaimerPrivacyTitle": "データ収集とプライバシー",
"disclaimerPrivacyContent": "本ソフトウェアは、サブスクリプション URL、ノード情報、設定内容、閲覧したウェブサイト、接続記録、通信内容、ログを収集またはアップロードしません。これらのデータは端末内にのみ保存され、開発者がアクセスすることはできません。\n\n本ソフトウェアが外部ネットワークにアクセスするのは、プロファイル更新時に指定されたサブスクリプション URL へアクセスする場合や、アップデート確認時に GitHub へアクセスする場合など、該当する機能を使用したときのみです。\n\n本ソフトウェアには、どのプラットフォームでも統計やクラッシュレポートのサービスは一切組み込まれていません。",
"disclaimerLicenseTitle": "オープンソースライセンス",
"disclaimerLicenseContent": "本ソフトウェアは GPL-3.0 ライセンスのもとでオープンソースとして公開されています。同ライセンスを遵守する限り、自由に使用、改変、再配布できますが、派生物も GPL-3.0 で公開し、原著作者の著作権表示を保持する必要があります。\n\n本ソフトウェアに含まれるサードパーティのコンポーネント(Clash.Meta コアを含む)は、それぞれのライセンスに従います。改変版や再配布版に起因する問題について、原著作者は責任を負いません。",
"disclaimerChangesTitle": "本声明の変更",
"disclaimerChangesContent": "開発者はバージョンの更新に伴い本声明を変更することがあり、変更内容は新しいバージョンの公開とともに効力を生じます。更新後も本ソフトウェアを引き続き使用した場合、変更後の声明に同意したものとみなされます。",
"disclaimerAcceptTitle": "声明への同意",
"disclaimerAcceptContent": "本ソフトウェアをインストール、複製または使用した時点で、本声明のすべての内容を読み、同意したものとみなされます。いずれかの条項に同意いただけない場合は、直ちに使用を中止し、本ソフトウェアをアンインストールしてください。",
"disclaimerRestateTitle": "声明の復唱",
"disclaimerRestateTip": "内容を理解したことを確認するため、以下の声明を下の入力欄に正確に入力してください:",
"disclaimerRestateHint": "上記の声明を入力",
"disclaimerRestateMismatch": "声明の内容と一致しません",
"disclaimerAcknowledgeTitle": "声明の確認",
"disclaimerAcknowledgeTip": "内容を理解したことを確認するため、以下の声明を読み、下のチェックボックスをオンにしてください:",
"disclaimerAcknowledgeLabel": "この声明を読み、理解しました",
"agree": "同意する",
"hotkeyManagement": "ホットキー管理",
"hotkeyManagementDesc": "キーボードでアプリを操作します",
"pressKeyboard": "キーを押してください",
"inputCorrectHotkey": "正しいホットキーを入力してください",
"hotkeyConflict": "ホットキーが競合しています",
"hotkeyDesc": "グローバルホットキーはウィンドウが非表示でも有効です。アクションをタップしてキーの組み合わせを記録します。",
"hotkeyNotSet": "未設定",
"pressKeyboard": "キーの組み合わせを押してください",
"hotkeyNeedsModifier": "{modifiers} のいずれかを含めてください",
"hotkeyConflictWith": "「{action}」で使用中です。保存するとこちらに移動します。",
"hotkeyUnavailable": "登録できませんでした。他のアプリが使用している可能性があります",
"remove": "削除",
"noHotKey": "ホットキーはまだありません",
"noNetwork": "ネットワークがありません",
"ipv6InboundDesc": "IPv6インバウンドを許可します",
"ipv6InboundDesc": "IPv6 を有効にすると、VPN で IPv6 インバウンドを許可します。",
"exportLogs": "ログをエクスポート",
"exportSuccess": "エクスポートが完了しました",
"iconStyle": "アイコンスタイル",
"onlyIcon": "アイコンのみ",
"iconStyleFilled": "背景あり",
"iconStylePlain": "背景なし",
"iconStyleHidden": "非表示",
"filter": "フィルター",
"hideTimeoutProxies": "タイムアウトしたプロキシを隠す",
"hideTimeoutProxiesDesc": "前回の遅延テストがタイムアウトしたプロキシを表示しない",
"stackMode": "スタックモード",
"network": "ネットワーク",
"networkDesc": "ネットワーク関連の設定を変更します",
"bypassDomain": "除外ドメイン",
"bypassDomainDesc": "システムプロキシが有効な場合のみ適用されます",
"bypassDomainDesc": "除外ドメインはシステムプロキシが有効な場合のみ適用されます。",
"resetTip": "リセットしてもよろしいですか?",
"icon": "アイコン",
"noData": "データがありません",
"fontFamily": "フォント",
"installedFonts": "インストール済みのフォント",
"toggle": "切り替え",
"system": "システム",
"routeMode": "ルートモード",
"routeModeBypassPrivate": "プライベートアドレスをバイパス",
"routeModeConfig": "設定を使用",
"routeAddress": "ルートアドレス",
"routeAddressDesc": "リッスンするルートアドレスを設定します",
"interfaceNameMode": "アウトバウンドインターフェース",
"interfaceNameModeClear": "クリア",
"interfaceNameModeFollow": "設定に従う",
"interfaceNameModeCustom": "カスタム",
"interfaceName": "インターフェース名",
"interfaceNameDesc": "アウトバウンド接続に使用するネットワークインターフェース名",
"interfaceNameDesc": "インターフェース名は、アウトバウンド接続に使用するネットワークインターフェースです。",
"copyEnvVar": "環境変数をコピー",
"memoryInfo": "メモリ情報",
"total": "合計",
"releaseMemory": "メモリを解放",
"memoryCoreHeapInuse": "使用中のヒープ",
"memoryCoreHeapIdle": "未使用のヒープ",
"memoryCoreStack": "ゴルーチンスタック",
"memoryCoreRuntime": "ランタイムのオーバーヘッド",
"memoryCoreNotRunning": "コアは実行されていません",
"memoryAppResident": "常駐メモリ",
"memoryAppShared": "アプリと共有",
"memoryEstimateDesc": "プロセスの常駐メモリからの推定値で、システムの表示とは異なる場合があります。",
"memoryEstimateSharedDesc": "コアはアプリと同じプロセスで動作します。コア分はランタイム統計から推定し、残りはアプリと共有メモリとして計上します。",
"memoryReleased": "メモリを解放しました",
"memoryReleasedSize": "{size} を解放しました",
"releaseMemoryFailed": "メモリの解放に失敗しました",
"cancel": "キャンセル",
"fileIsUpdate": "ファイルが変更されています。変更を保存しますか?",
"profileHasUpdate": "プロファイルが変更されています。自動更新を無効にしますか?",
@@ -305,38 +396,38 @@
"listen": "リッスン",
"undo": "元に戻す",
"redo": "やり直す",
"lineWrap": "折り返し",
"none": "なし",
"basicConfig": "基本設定",
"basicConfigDesc": "基本設定をグローバルに変更します",
"advancedConfig": "詳細設定",
"advancedConfigDesc": "多彩な設定項目を提供します",
"selectedCountTitle": "{count} 件選択中",
"advancedConfigDesc": "詳細設定には、ネットワーク、DNS、追加ルール、スクリプトがあります。",
"addRule": "ルールを追加",
"ruleName": "ルール名",
"content": "内容",
"subRule": "サブルール",
"ruleTarget": "ルールターゲット",
"matchTargetTitle": "マッチ先",
"matchTarget": "MATCH-TARGET",
"followProfile": "プロファイルに従う",
"matchTargetDesc": "MATCH-TARGET を対象にしたルールの行き先。既定ではこのプロファイル末尾の MATCH ルールのターゲットを使います。",
"selectMatchTarget": "MATCH-TARGET を選択",
"sourceIp": "送信元IP",
"noResolve": "IPを解決しない",
"saveChanges": "変更を保存しますか?",
"findProcessModeDesc": "有効にすると、パフォーマンスが多少低下します",
"tabAnimationDesc": "モバイル表示でのみ有効です",
"colorSchemes": "カラースキーム",
"palette": "パレット",
"tonalSpotScheme": "トーナルスポット",
"fidelityScheme": "フィデリティ",
"cannotSave": "保存できません",
"navigationBarStyle": "ボトムバー",
"floating": "フローティング",
"docked": "固定",
"slide": "スライド",
"fade": "フェード",
"pureBlack": "ピュアブラック",
"customColor": "カスタムカラー",
"colorStyle": "配色スタイル",
"grid": "グリッド",
"spectrum": "スペクトル",
"sliders": "スライダ",
"hue": "色相",
"saturation": "彩度",
"brightness": "明度",
"hex": "16進数",
"monochromeScheme": "モノクローム",
"neutralScheme": "ニュートラル",
"vibrantScheme": "ビブラント",
"expressiveScheme": "エクスプレッシブ",
"contentScheme": "コンテンツ",
"rainbowScheme": "レインボー",
"fruitSaladScheme": "フルーツサラダ",
"vividStyle": "ビビッド",
"tintedStyle": "ティント",
"softStyle": "ソフト",
"developerMode": "開発者モード",
"developerModeEnableTip": "開発者モードが有効になりました。",
"messageTest": "メッセージテスト",
@@ -348,6 +439,8 @@
"launchInterruptedTip": "前回、アプリは起動中に予期せず終了しました。今回の自動セットアップはスキップしました。手動で起動して再試行できます。",
"clearData": "データを消去",
"textScale": "テキストの拡大縮小",
"textScalePreview": "アプリ内の文字はこの大きさで表示されます",
"followSystem": "システムに従う",
"internet": "インターネット",
"systemApp": "システムアプリ",
"noNetworkApp": "ネットワーク不使用アプリ",
@@ -357,11 +450,23 @@
"logsTest": "ログテスト",
"emptyTip": "{label}は空にできません",
"urlTip": "{label}はURLである必要があります",
"invalidLinkTip": "サブスクリプションリンクまたはプロキシの共有リンクではありません",
"numberTip": "{label}は数値である必要があります",
"numberRangeTip": "{label}は{min}から{max}の範囲で指定してください",
"interval": "間隔",
"existsTip": "{label}はすでに存在します",
"maxLengthTip": "{label}は最大{max}文字です",
"multipleValuesTip": "複数の値はカンマで区切ってください",
"singleAdd": "個別追加",
"batchAdd": "一括追加",
"batchListInputTip": "1行に1項目、またはカンマ区切りで入力してください",
"batchMapInputTip": "1行に1件、キーと値はスペースで区切ってください",
"singleImport": "個別インポート",
"batchImport": "一括インポート",
"batchUrlInputTip": "1行に1つのURLを入力してください",
"batchLinkInputTip": "1行に1つのリンクを入力してください",
"batchPreviewTip": "{count}件を追加、{skipped}件は既存のためスキップ",
"lineIssueTip": "{line}行目:{message}",
"singleValueTip": "{label}は1項目のみ指定できます",
"deleteTip": "この{label}を削除してもよろしいですか?",
"deleteMultipTip": "選択した{label}を削除してもよろしいですか?",
"nullTip": "{label}はまだありません",
@@ -378,6 +483,7 @@
"import": "インポート",
"importFile": "ファイルからインポート",
"importUrl": "URLからインポート",
"startFromScratch": "最初から作成",
"autoSetSystemDns": "システムDNSを自動設定",
"details": "{label}の詳細",
"creationTime": "作成日時",
@@ -396,36 +502,44 @@
"request": "リクエスト",
"connected": "接続済み",
"disconnected": "切断済み",
"connecting": "接続中...",
"connecting": "接続中…",
"restartCoreTip": "コアを再起動してもよろしいですか?",
"forceRestartCoreTip": "コアを強制再起動してもよろしいですか?",
"dnsHijacking": "DNSハイジャック",
"coreStatus": "コアの状態",
"dataCollectionTip": "データ収集について",
"dataCollectionContent": "本アプリは、安定性向上のために Firebase Crashlytics を使用してクラッシュ情報を収集します。\n収集されるデータにはデバイス情報とクラッシュの詳細が含まれますが、個人の機密データは含まれません。\nこの機能は設定で無効にできます。",
"crashlytics": "クラッシュ分析",
"crashlyticsTip": "有効にすると、アプリのクラッシュ時に機密情報を含まないクラッシュログを自動的にアップロードします",
"appendSystemDns": "システムDNSを追加",
"appendSystemDnsTip": "設定にシステムDNSを強制的に追加します",
"editRule": "ルールを編集",
"overrideMode": "上書きモード",
"standardModeDesc": "標準モード:基本設定を上書きし、シンプルなルール追加機能を提供します",
"scriptModeDesc": "スクリプトモード:外部の拡張スクリプトを使用し、ワンクリックで設定を上書きします",
"extendMode": "拡張モード",
"standardModeDesc": "標準モード:プロファイルにルールを追加します",
"scriptModeDesc": "スクリプトモード:拡張スクリプトでプロファイル全体を書き換えます",
"addedRules": "追加ルール",
"controlGlobalAddedRules": "グローバル追加ルールを管理",
"overrideScript": "上書きスクリプト",
"quickActions": "クイック操作",
"rulePresetBlockQuic": "QUIC をブロック",
"rulePresetBlockStun": "STUN をブロック",
"rulePresetBlockDot": "DNS over TLS をブロック",
"rulePresetBlockLoopback": "プロキシのループバックを防止",
"rulePresetSystemServicesDirect": "Apple と Microsoft に直接接続",
"rulePresetBittorrentDirect": "BitTorrent を直接接続",
"rulePresetLanDirect": "LAN 直接接続",
"extendScript": "拡張スクリプト",
"goToConfigureScript": "スクリプト設定へ移動",
"goToConfigureAddedRules": "追加ルール設定へ移動",
"editGlobalRules": "グローバルルールを編集",
"externalFetch": "外部取得",
"confirmForceCrashCore": "コアを強制クラッシュさせてもよろしいですか?",
"confirmClearAllData": "すべてのデータを消去してもよろしいですか?",
"loading": "読み込み中...",
"loading": "読み込み中…",
"yearsAgo": "{count} 年前",
"monthsAgo": "{count} か月前",
"daysAgo": "{count} 日前",
"hoursAgo": "{count} 時間前",
"minutesAgo": "{count} 分前",
"justNow": "たった今",
"yearsAgoShort": "{count} 年前",
"monthsAgoShort": "{count} か月前",
"daysAgoShort": "{count} 日前",
"hoursAgoShort": "{count} 時間前",
"minutesAgoShort": "{count} 分前",
"noLongerRemind": "今後表示しない",
"accessControlSettings": "アクセス制御の設定",
"turnOn": "オンにする",
@@ -434,39 +548,45 @@
"restart": "再起動",
"speedStatistics": "速度統計",
"resetPageChangesTip": "このページには変更があります。リセットしてもよろしいですか?",
"overwriteTypeCustom": "カスタム",
"overwriteTypeCustomDesc": "カスタムモード:プロキシグループとルールを完全にカスタマイズできます",
"customProfile": "カスタム",
"customProfileDesc": "アプリのプロキシと他のプロファイルからプロキシグループとルールを組み立てます",
"customProfileResources": "カスタムプロファイルのリソース",
"customProfileResourcesDesc": "カスタムプロファイルのリソースには、プロキシ、ルールプロバイダー、フィルター、アイコンセットがあります。",
"unknownNetworkError": "不明なネットワークエラー",
"restoreException": "復元エラー",
"networkException": "ネットワークエラーです。接続を確認してから再試行してください",
"backupFromNewerVersion": "このバックアップは新しいバージョンのアプリで作成されています。アプリを更新してから復元してください",
"networkTimeoutError": "リクエストがタイムアウトしました。ネットワークまたはプロキシを確認してから再試行してください",
"networkHostLookupError": "サーバーのアドレスを解決できませんでした。URL が正しいこと、DNS が使えることを確認してください",
"networkConnectionError": "サーバーに接続できませんでした。ネットワーク接続またはプロキシ設定を確認してください",
"networkTlsError": "安全な接続に失敗しました。サーバー証明書が無効か、接続が傍受されている可能性があります",
"networkCancelledError": "リクエストはキャンセルされました",
"networkAccessDeniedError": "サーバーがアクセスを拒否しました(HTTP {code})。リンクの期限切れか、認証情報が誤っている可能性があります",
"networkNotFoundError": "このアドレスには何も見つかりませんでした(HTTP {code})。URL が正しいか確認してください",
"networkRateLimitedError": "リクエストが多すぎます(HTTP 429)。しばらく待ってから再試行してください",
"networkServerError": "サーバーで問題が発生しました(HTTP {code})。しばらくしてから再試行してください",
"networkBadResponseError": "サーバーがリクエストを拒否しました(HTTP {code})",
"networkRequestFailed": "ネットワークリクエストに失敗しました:{detail}",
"invalidBackupFile": "無効なバックアップファイル",
"pruneCache": "キャッシュを整理",
"backupAndRestore": "バックアップと復元",
"backupAndRestoreDesc": "WebDAVまたはファイルでデータを同期します",
"restore": "復元",
"restoreSuccess": "復元が完了しました",
"restoreFromWebDAVDesc": "WebDAVからデータを復元します",
"restoreFromFileDesc": "ファイルからデータを復元します",
"restoreOnlyConfig": "プロファイルのみ復元",
"restoreAllData": "すべてのデータを復元",
"addProfile": "プロファイルを追加",
"delayTest": "遅延テスト",
"proxyGroupEmpty": "プロキシグループが空です",
"proxyGroupNameEmpty": "プロキシグループ名は空にできません",
"proxyGroupNameDuplicate": "プロキシグループ名が重複しています",
"confirmExitWindow": "現在のウィンドウを閉じてもよろしいですか?",
"dataChangedSave": "データの変更を検出しました。保存しますか?",
"selectProxyProviders": "プロキシプロバイダーを選択",
"proxyFilter": "ノードフィルター",
"optional": "任意",
"maxFailedTimes": "最大失敗回数",
"testInterval": "テスト間隔",
"excludeProxyFilter": "除外ノードフィルター",
"excludeType": "除外タイプ",
"excludeFilter": "除外フィルター",
"expectedStatus": "期待するステータス",
"selectProxies": "プロキシを選択",
"strategy": "戦略",
"tolerance": "許容値",
"inputProxyGroupName": "プロキシグループ名を入力してください",
"helperCorruptTip": "Helper サービスが利用できないため、TUN モードを有効にできません。FlClash を再インストールしてください。",
"tunAuthorizationFailed": "TUN の権限を取得できなかったため、オフにしました。",
"sudoPasswordTitle": "sudo のパスワードを入力",
"coreBlockedByPolicyTip": "Windows が FlClashCore.exe の実行を拒否しました(エラー {code})。スマート アプリ コントロールや AppLocker などのアプリ制御ポリシーは未署名のプログラムをブロックします。ポリシーで FlClash を許可するか、ポリシーを無効にしてから再試行してください。",
"coreBlockedBySmartAppControlTip": "Windows のスマート アプリ コントロールが、署名されていない FlClashCore.exe をブロックしました。Windows セキュリティ → アプリとブラウザーの制御 → スマート アプリ コントロールの設定で「オフ」を選び、FlClash を再起動してください。一度オフにすると、Windows を再インストールしない限り再度オンにはできません。",
"hideFromList": "リストから隠す",
@@ -479,7 +599,6 @@
"selectRuleSet": "ルールセットを選択してください",
"splitStrategy": "振り分け戦略",
"selectSplitStrategy": "振り分け戦略を選択してください",
"selectSubRule": "サブルールを選択してください",
"noResolveHostname": "ホスト名を解決しない",
"matchSourceIp": "送信元IPにマッチ",
"basicInfo": "基本情報",
@@ -487,21 +606,11 @@
"proxyType": "プロキシタイプ",
"basicStrategy": "基本ポリシー",
"editProxy": "プロキシを編集",
"includeAllProxyProviders": "すべてのプロキシプロバイダーを含める",
"includeAllProxyProvidersTip": "有効にすると、取り込んだプロキシプロバイダーを上書きします",
"addProxyProviders": "プロキシプロバイダーを追加",
"includeAllProxies": "すべてのプロキシを含める",
"includeAllProxiesTip": "プロキシグループに属さないすべてのプロキシを取り込みます。下でプロキシグループを追加できます",
"proxiesEmpty": "プロキシが空です",
"addProxies": "プロキシを追加",
"addNodes": "ノードを追加",
"addProxyGroup": "プロキシグループを追加",
"editProxyGroup": "プロキシグループを編集",
"confirmOverwriteTip": "確定すると既存のデータを上書きします",
"configDataDetected": "設定内にデータが見つかりました",
"quickFill": "クイック入力",
"iconUrl": "アイコンURL",
"iconRecords": "アイコン履歴",
"noRecords": "記録がありません",
"custom": "カスタム",
"ruleActionDomainDesc": "完全なドメインにマッチ",
"ruleActionDomainSuffixDesc": "ドメインサフィックスにマッチ",
@@ -540,53 +649,51 @@
"ruleActionNotDesc": "論理ルール NOT",
"ruleActionSubRuleDesc": "サブルールへマッチします。括弧の使い方に注意してください",
"ruleActionMatchDesc": "すべてのリクエストにマッチ(条件不要)",
"subRuleEmpty": "サブルールが空です",
"proxyProvidersNotEmpty": "プロキシプロバイダーは空にできません",
"contentNotEmpty": "内容は空にできません",
"subRuleNotEmpty": "サブルールは空にできません",
"splitStrategyNotEmpty": "振り分け戦略は空にできません",
"proxyProvidersEmpty": "プロキシプロバイダーが空です",
"timeout": "タイムアウト",
"delayFailed": "失敗",
"invalidSubRule": "{subRule} は無効な SUB_RULE です",
"invalidPolicy": "{target} は無効なポリシーです",
"invalidProxyProvider": "{providerName} は無効なプロキシプロバイダーです",
"invalidRuleSet": "{ruleSet} は無効なルールセットです",
"invalidNetworkContent": "tcp または udp のみ対応しています",
"invalidRangeContent": "80 や 8000-9000 のような数値または範囲を / 区切りで入力してください",
"invalidDscpContent": "DSCP マークは 63 を超えられません",
"invalidCidrContent": "192.168.0.0/16 のような CIDR 形式の IP 範囲を入力してください",
"invalidPortRangeContent": "443 や 8000-9000 のようなポートまたは範囲を入力してください",
"invalidHostContent": "ドメインまたは IP アドレスを入力してください",
"invalidListenContent": "0.0.0.0:1053 のようなアドレスとポートを入力してください",
"invalidProxy": "{proxyName} は無効なプロキシです",
"proxyGroupDetectedAbnormal": "現在のプロキシグループに異常が見つかりました",
"proxyProviderDetectedAbnormal": "選択したプロキシプロバイダーに異常が見つかりました",
"proxyDetectedAbnormal": "選択したプロキシに異常が見つかりました",
"createProfile": "プロファイルを作成",
"locationPermissionRequired": "位置情報の権限が必要です",
"locationPermissionGuide": "1. システム設定 > プライバシーとセキュリティ を開く\n2. 位置情報サービス を選択\n3. リストで {appName} を見つけてチェックを入れる\n\n設定が完了したらアプリに戻ると、通常どおり使用できます。ご協力ありがとうございます。",
"prerequisites": "前提条件",
"ignoreBatteryOptimization": "電池の最適化を無視",
"batteryOptimizationDesc": "バックグラウンドでの動作を維持するため、このアプリの電池の最適化を無効にしてください。タップすると設定を開きます。",
"batteryOptimizationStatusTip": "システムの制限により、実行中は電池の最適化の状態を正しく取得できません",
"batteryOptimizationDesc": "電池の最適化を無視すると、アプリがバックグラウンドで動作し続けます。",
"locationPermission": "位置情報の権限",
"locationPermissionDesc": "システムの要件により、Wi-Fi 名の取得には位置情報の権限が必要です。Android では「常に許可」を選択してください。そうしないと、アプリがバックグラウンドにあるときに Wi-Fi 名を取得できません。",
"excludeSsids": "除外SSID",
"excludeSsidsDesc": "除外したSSIDのWi-Fiに接続すると、アプリの実行状態が自動的に切り替わります",
"excludeSsidsDesc": "除外した SSID の Wi-Fi に接続すると、アプリの実行状態が自動的に切り替わります。",
"ssidsEmpty": "SSIDが空です",
"onDemand": "オンデマンド",
"onDemandDesc": "特定のシナリオでのアプリの実行状態を設定します",
"onDemandDesc": "オンデマンドは、除外した Wi-Fi に接続している間プロキシを一時停止します。",
"locationPermissionDeniedMessage": "位置情報の権限が拒否されたため、現在の Wi-Fi 名を取得できません。システム設定で位置情報の権限を手動で有効にしてください。",
"addSsid": "SSIDを追加",
"editSsid": "SSIDを編集",
"authorized": "許可済み",
"tapToAuthorize": "タップして許可",
"suspended": "一時停止中...",
"geoOptions": "Geoオプション",
"geoAutoUpdate": "自動更新",
"geoAutoUpdateInterval": "自動更新間隔",
"geoAutoUpdateIntervalTip": "自動更新間隔は0より大きくしてください",
"suspended": "一時停止中…",
"resourceUpdateInterval": "自動更新間隔",
"resourceUpdateIntervalTip": "自動更新間隔は0より大きくしてください",
"hours": "時間",
"hoursCount": "{count} 時間",
"geoResources": "Geoリソース",
"minutes": "分",
"geoResources": "Geo",
"geoSkipped": "{name} はすでに最新です",
"geoUpdated": "{name} を更新しました",
"secondsCount": "{count} 秒",
"entriesCount": "{count} 件",
"proxiesCount": "プロキシ {count} 件",
"rulesCount": "ルール {count} 件",
"proxiesCount": "{count, plural, other{プロキシ {label} 件}}",
"rulesCount": "{count, plural, other{ルール {label} 件}}",
"proxyGroupsCount": "{count, plural, other{プロキシグループ {label} 件}}",
"changelogBreaking": "破壊的変更",
"changelogFeatures": "新機能",
"changelogFixes": "不具合修正",
@@ -600,7 +707,6 @@
"pinWindow": "最前面に固定",
"unpinWindow": "固定を解除",
"exitFullScreen": "全画面表示を終了",
"toggleLabel": "ラベルを切り替え",
"torch": "ライト",
"pickFromAlbum": "アルバムから選択",
"blockConnection": "接続をブロック",
@@ -611,11 +717,227 @@
"previousMatch": "前の一致",
"nextMatch": "次の一致",
"clearSearch": "検索をクリア",
"addWidget": "ウィジェットを追加",
"noSearchResults": "一致する結果はありません",
"showPassword": "パスワードを表示",
"hidePassword": "パスワードを隠す",
"authorize": "許可",
"installedAppsPermissionRequired": "アプリ一覧の権限が必要です",
"installedAppsPermissionDesc": "このシステムでは、許可するまでインストール済みアプリの一覧が提供されません。許可すると、アプリごとのプロキシを設定できます。",
"installedAppsPermissionDeniedMessage": "アプリ一覧の権限が拒否されたため、インストール済みアプリを取得できません。システム設定から手動で許可してください。"
"installedAppsPermissionDeniedMessage": "アプリ一覧の権限が拒否されたため、インストール済みアプリを取得できません。システム設定から手動で許可してください。",
"outboundIp": "アウトバウンド IP",
"ipAddress": "IP アドレス",
"ipQualityLevel": "レベル",
"ipQualityGood": "良好",
"ipQualityNormal": "普通",
"ipQualityRisky": "リスクあり",
"ipType": "タイプ",
"ipTypeResidential": "住宅",
"ipTypeMobile": "モバイル回線",
"ipTypeBusiness": "ビジネス",
"ipTypeHosting": "データセンター",
"ipFlags": "検出",
"ipFlagProxy": "プロキシ",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "不正利用の履歴",
"ipOrganization": "組織",
"ipAsn": "ASN",
"ipQualitySource": "採用したソース",
"ipQualitySources": "各ソース",
"ipQualityFailed": "IP タイプを判定できませんでした",
"ipQualityRetry": "再確認",
"ipSourceNoType": "判定不可",
"ipSourceRateLimited": "レート制限",
"ipSourceIpMismatch": "アウトバウンド IP が不一致",
"hideIp": "IP を隠す",
"editorUnavailable": "エディターを利用できません",
"replace": "置換",
"replaceAll": "すべて置換",
"cameraPermissionRequired": "カメラの権限が必要です",
"cameraPermissionDesc": "QRコードをスキャンするには、システム設定でカメラへのアクセスを許可するか、アルバムからQRコード画像を選択してください。",
"cameraUnavailable": "カメラを使用できません",
"retry": "再試行",
"invalidProfileQrcode": "このQRコードにはプロファイルのリンクが含まれていません",
"dnsQueries": "DNSクエリ",
"recordType": "レコードタイプ",
"answers": "応答",
"responseCode": "応答コード",
"error": "エラー",
"initiator": "発信元",
"cache": "キャッシュ",
"yes": "はい",
"no": "いいえ",
"nonTextProviderFile": "この外部リソースはテキストファイルではありません",
"addCustomProxy": "プロキシを追加",
"definitionNotMap": "設定は name と type を含む YAML マッピングである必要があります",
"ruleTextInvalid": "ルールは対応する種類で始めてください。例:DOMAIN,example.com,DIRECT",
"ruleListInvalid": "ルールは 1 項目に 1 つのルールを書いた YAML リストにしてください。例:- DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "リンクからプロキシを読み取れませんでした",
"shareLinkUnreadable": "{link} からプロキシを読み取れませんでした",
"proxiesProfileLabel": "{name} など{count}件のプロキシ",
"singleShareLinkOnly": "ここにはリンクを 1 つだけ入力してください。複数の場合は一覧のクイック編集を使ってください",
"username": "ユーザー名",
"emptyFallback": "空グループのフォールバック",
"defaultSelected": "デフォルトの選択",
"hashByInUser": "インバウンドユーザーでハッシュ",
"appProxiesEmpty": "ローカルプロキシがありません",
"nodes": "ノード",
"localProxies": "ローカルプロキシ",
"customIssueEmptyName": "名前が空です",
"customIssueReservedName": "{name} は組み込みポリシー名のため使用できません",
"customIssueDuplicateName": "名前 {name} は他のプロキシまたはプロキシグループで使用されています",
"customIssueCoreRejected": "コアがこのプロキシを解析できません:{message}",
"customIssueMissingProxies": "次のプロキシまたはポリシーは存在しません:{names}",
"customIssueMissingProviders": "次のプロキシプロバイダーは存在しません:{names}",
"customIssueNoProxySource": "プロキシもプロキシプロバイダーも選択されていないため、コアはこのグループを拒否します",
"customIssueGroupLoop": "プロキシグループが循環参照しています:{path}",
"customIssueInvalidEmptyFallback": "空グループのフォールバックはプロキシである必要がありますが、{name} は存在しないかプロキシグループです",
"customIssueInvalidFilter": "{name}は有効な正規表現ではありません:{message}",
"customIssueMissingDialer": "ダイヤラープロキシ {name} が存在しません",
"customIssueDialerLoop": "{proxy} は {target} 経由で接続しますが、{target} から再び {proxy} に戻るため、ループになります",
"dialerProxyLoopStopped": "{name} 経由の接続が使用元のプロキシに戻るため、接続を中止しました",
"customIssuesSummary": "{count} 件に問題があり、プロファイルの適用に失敗する可能性があります",
"providerInUse": "{label} は {profiles} でまだ使用されており、削除するとそれらが正常に動作しなくなります。削除してもよろしいですか?",
"proxyGroupInUse": "{label} はこのプロファイルのルール、プロキシグループ、ダイヤラープロキシ、DNS または NTP からまだ参照されており、削除するとそれらの参照は無効になります。削除してもよろしいですか?",
"proxiesRemovedTip": "次のプロキシが削除されます:{removed}。名前を変えた行は新しいプロキシとして扱われ、削除されたプロキシを参照するカスタムプロファイルでは見つからないと表示されます。適用しますか?",
"proxiesReplacedTip": "次のプロキシが削除されます:{removed}。次のプロキシが追加されます:{added}。名前を変えた行は新しいプロキシとして扱われ、削除されたプロキシを参照するカスタムプロファイルでは見つからないと表示されます。適用しますか?",
"fontSize": "サイズ",
"large": "大",
"extraLarge": "特大",
"switchProfile": "プロファイルを切り替え",
"lastUpdated": "最終更新",
"filters": "フィルター",
"label": "ラベル",
"regex": "正規表現",
"matchCase": "大文字と小文字を区別",
"iconSets": "アイコンセット",
"invalidIconSet": "有効なアイコンセットではありません",
"recommendedIcons": "おすすめ",
"localImage": "ローカル画像",
"iconSetsEmptyTip": "アイコンセットはまだありません。「カスタムプロファイルのリソース → アイコンセット」で追加できます",
"outbound": "アウトバウンド",
"strictRoute": "厳格なルーティング",
"congestionController": "TCP 輻輳制御",
"congestionControllerDesc": "TCP 輻輳制御は mips スタックでのみ有効です。",
"routeExcludeAddress": "ルート除外アドレス",
"keepAliveIdle": "TCP キープアライブ待機時間",
"disableKeepAlive": "TCP キープアライブを無効化",
"routingMark": "ルーティングマーク",
"strictRouteDesc": "厳格なルーティングは DNS などの通信が TUN を迂回するのを防ぎます。有効にすると他のデバイスからこの端末にアクセスできなくなります。",
"initFailed": "起動に失敗しました",
"initFailedTip": "FlClash の起動中にエラーが発生したため、続行できません。詳細をコピーして問題を報告できます。繰り返し発生する場合は、データを消去すると起動できるようになることがあります。",
"errorDetails": "エラーの詳細",
"stackTrace": "スタックトレース",
"clearDataAndExitTip": "すべてのプロファイル、設定、ローカルデータを削除してアプリを終了します。もう一度開くと最初から始められます。",
"clearDataFailed": "データを消去できませんでした:{detail}",
"bypassPrivateRoute": "プライベートアドレスをバイパス",
"bypassPrivateRouteDesc": "プライベートアドレスをバイパスすると、LAN・ループバック・マルチキャストのアドレスは TUN を通りません。",
"disableIcmpForwarding": "ICMP 転送を無効化",
"disableIcmpForwardingDesc": "ICMP 転送を無効にすると TUN が ping に直接応答するため、ping に実際の遅延が表示されなくなります。",
"excludeInterface": "除外インターフェース",
"excludeInterfaceDesc": "docker0 などの除外インターフェースから入る通信は TUN を通りません。",
"includeInterface": "対象インターフェース",
"includeInterfaceDesc": "対象インターフェースから入る通信だけが TUN を通り、除外インターフェースは無効になります。",
"includeUid": "対象ユーザー ID",
"includeUidDesc": "対象ユーザー ID のプロセスの通信だけが TUN を通ります。",
"excludeUid": "除外ユーザー ID",
"excludeUidDesc": "除外ユーザー ID のプロセスの通信は TUN を通りません。",
"excludeSrcPort": "除外送信元ポート",
"excludeSrcPortDesc": "除外送信元ポートからの通信は TUN を通りません。",
"excludeDstPort": "除外宛先ポート",
"excludeDstPortDesc": "除外宛先ポートへの通信は TUN を通りません。",
"udpTimeout": "UDP タイムアウト",
"udpTimeoutDesc": "UDP タイムアウトは秒単位で、gvisor・system・mixed スタックでのみ有効です。",
"dnsHijackDesc": "追加しない場合、ポート 53 宛てのすべての DNS 問い合わせをハイジャックします(any:53)。",
"tunOverrideDesc": "ここで追加した項目は、すべてのプロファイルの TUN 設定を上書きします。追加していない項目はプロファイル自身の設定のままです。カスタムプロファイル自身が追加した項目が優先されます。",
"profileTunSettingsDesc": "このプロファイルにのみ適用され、ネットワーク設定の TUN 上書き項目より優先されます。",
"settingsCount": "設定 {count} 件",
"sectionsInEffect": "{count} 件が有効",
"itemsCount": "{count} 件",
"additionalPrefix": "名前のプレフィックス",
"additionalSuffix": "名前のサフィックス",
"skipCertVerify": "証明書の検証をスキップ",
"ipVersion": "IP バージョン",
"addFilters": "フィルターを追加",
"failedItem": "{label}:{message}",
"failedCount": "{count} 件が失敗しました",
"importConfigInvalid": "このファイルはインポートできる設定ではありません",
"importConfigReplaceTip": "インポートすると、このプロファイルの現在のプロキシグループ、ルール、設定が置き換えられます",
"networkName": "ネットワーク名",
"networkSecret": "ネットワークシークレット",
"peers": "ピア",
"virtualIpv4": "仮想 IPv4",
"hostname": "ホスト名",
"exitNodes": "出口ノード",
"cipher": "暗号方式",
"protocol": "プロトコル",
"protocolParam": "プロトコルパラメータ",
"obfs": "難読化",
"obfsParam": "難読化パラメータ",
"obfsPassword": "難読化パスワード",
"obfsMode": "難読化モード",
"obfsHost": "難読化ホスト",
"plugin": "プラグイン",
"pluginMode": "プラグインモード",
"pluginHost": "プラグインホスト",
"privateKey": "秘密鍵",
"publicKey": "公開鍵",
"privateKeyPassphrase": "秘密鍵のパスフレーズ",
"preSharedKey": "事前共有鍵",
"secretKey": "キー",
"transport": "トランスポート",
"uploadBandwidth": "アップロード帯域幅",
"downloadBandwidth": "ダウンロード帯域幅",
"protocolVersion": "プロトコルバージョン",
"portHopping": "ポートホッピング",
"hopInterval": "ホップ間隔",
"congestionControl": "輻輳制御",
"udpRelayMode": "UDP リレーモード",
"allowedIps": "許可する IP",
"remoteDnsResolve": "リモート DNS 解決",
"clientFingerprint": "クライアントフィンガープリント",
"required": "必須",
"ageSecretKey": "age 秘密鍵",
"ageEncryptedProfile": "暗号化されたプロファイル",
"ageSecretKeyRequiredTip": "このプロファイルは age で暗号化されており、復号には age 秘密鍵が必要です",
"currentOutboundDesc": "起動ボタンがあるため削除できません",
"running": "実行中",
"notRunning": "停止中",
"trafficPeak": "ピーク {value}",
"trafficTotal": "累計 {value}",
"ipQualityShort": "品質",
"ipTypeUnknown": "タイプ不明",
"tapIconToRetry": "アイコンをタップして再試行",
"networkTakeover": "トラフィックの引き継ぎ",
"portValue": "ポート {port}",
"serviceAbnormalCount": "{count} 件に問題",
"serviceAllAvailable": "すべて利用可能",
"serviceShownOnDashboard": "ダッシュボードに表示",
"groupTimeoutCount": "{count} 件タイムアウト",
"groupCountUnit": "個",
"groupCount": "{count} 個",
"proxyGroupsAfterStart": "起動するとここにグループが表示されます",
"usedShort": "使用済み",
"profileUpdatedAt": "{time}に更新",
"profileExpiresIn": "{date} に期限切れ、残り {days} 日",
"profileDaysLeft": "残り {days} 日",
"target": "宛先",
"outboundExit": "出口",
"duration": "経過時間",
"traffic": "通信量",
"totalDownload": "ダウンロード量",
"totalUpload": "アップロード量",
"initiatorShort": "発信元",
"result": "結果",
"elapsed": "所要時間",
"connectionStats": "接続統計",
"connectionStatsDesc": "ライブ接続、最近のリクエスト、DNS クエリ",
"memoryInfoDesc": "アプリとコアが使用するメモリ",
"editDashboard": "ダッシュボードを編集",
"done": "完了",
"dashboardAddableSection": "追加できるカード",
"customImportDroppedSubRules": "{count, plural, other{カスタム設定はサブルールに対応していないため、SUB-RULE ルール {label} 件を破棄しました}}",
"autoInclude": "自動で含める",
"allReferencedProxies": "参照されているすべてのプロキシ",
"allReferencedProxyProviders": "参照されているすべてのプロキシプロバイダー"
}
+943
View File
@@ -0,0 +1,943 @@
{
"rule": "규칙",
"rules": "규칙",
"global": "전역",
"direct": "직접",
"dashboard": "대시보드",
"proxies": "프록시",
"profile": "프로필",
"profiles": "프로필",
"tools": "도구",
"logs": "로그",
"resources": "리소스",
"trafficUsage": "트래픽 사용량",
"networkSpeed": "네트워크 속도",
"outboundMode": "아웃바운드 모드",
"serviceStatus": "서비스 상태",
"serviceRestricted": "접근 제한됨",
"serviceAvailable": "사용 가능",
"serviceUnavailable": "사용 불가",
"serviceFailed": "확인 실패",
"serviceDisallowedIsp": "허용되지 않는 ISP",
"serviceBlocked": "차단됨",
"serviceUnsupportedRegion": "지원하지 않는 지역",
"serviceOriginalsOnly": "오리지널 콘텐츠만",
"serviceComingSoon": "출시 예정",
"servicePending": "확인 전",
"serviceManage": "서비스 관리",
"serviceCheck": "확인",
"serviceCheckAll": "모두 확인",
"serviceCheckedAt": "확인 시각: {time}",
"upload": "업로드",
"download": "다운로드",
"usedTraffic": "사용한 트래픽",
"totalTraffic": "전체 트래픽",
"expireTime": "만료 시간",
"nullProfileDesc": "프로필을 추가하여 시작하세요",
"settings": "설정",
"language": "언어",
"defaultText": "기본값",
"more": "더 보기",
"other": "기타",
"about": "정보",
"en": "영어",
"es": "스페인어",
"fr": "프랑스어",
"ja": "일본어",
"ko": "한국어",
"ru": "러시아어",
"zhCN": "중국어 간체",
"theme": "테마",
"extend": "확장",
"runtimeConfig": "런타임 구성",
"allowLan": "LAN 허용",
"tun": "TUN",
"tunDesc": "TUN은 관리자 모드에서만 작동합니다.",
"minimizeOnExit": "종료 시 최소화",
"autoLaunch": "자동 실행",
"silentLaunch": "백그라운드 실행",
"silentLaunchDesc": "백그라운드 실행은 창을 표시하지 않고 시작합니다.",
"autoRun": "자동 시작",
"autoRunDesc": "자동 시작은 앱을 열 때 프록시를 시작합니다.",
"logcat": "Logcat",
"logcatDesc": "Logcat을 끄면 로그 항목이 숨겨집니다.",
"autoCheckUpdate": "업데이트 자동 확인",
"checkCertificate": "TLS 인증서 검증",
"checkCertificateDesc": "TLS 검증을 끄면 구독과 백업이 중간자 공격에 노출됩니다.",
"accessControl": "접근 제어",
"accessControlDesc": "접근 제어는 프록시를 사용할 앱을 선택합니다.",
"edit": "편집",
"confirm": "확인",
"update": "업데이트",
"add": "추가",
"save": "저장",
"delete": "삭제",
"seconds": "초",
"qrcode": "QR 코드",
"qrcodeDesc": "QR 코드를 스캔하여 프로필 가져오기",
"url": "URL",
"link": "링크",
"linkDesc": "구독 링크 또는 프록시 공유 링크로 프로필 가져오기",
"file": "파일",
"fileDesc": "프로필 파일을 직접 업로드",
"name": "이름",
"profileNameNullValidationDesc": "프로필 이름을 입력하세요",
"rename": "이름 변경",
"profileUrlNullValidationDesc": "프로필 URL을 입력하세요",
"profileUrlInvalidValidationDesc": "올바른 프로필 URL을 입력하세요",
"autoUpdate": "자동 업데이트",
"autoUpdateInterval": "자동 업데이트 간격",
"profileAutoUpdateIntervalNullValidationDesc": "자동 업데이트 간격을 입력하세요",
"profileAutoUpdateIntervalInvalidValidationDesc": "올바른 간격을 입력하세요",
"themeMode": "테마 모드",
"themeColor": "테마 색상",
"preview": "미리보기",
"auto": "자동",
"light": "라이트",
"dark": "다크",
"importFromLink": "링크에서 가져오기",
"submit": "제출",
"createProfileFromUrlTip": "{url}에서 프로필을 만드시겠습니까?",
"create": "만들기",
"pleaseUploadValidQrcode": "올바른 QR 코드를 업로드하세요",
"blacklistMode": "블랙리스트 모드",
"whitelistMode": "화이트리스트 모드",
"selectAll": "모두 선택",
"cancelSelectAll": "모두 선택 해제",
"appAccessControl": "앱 접근 제어",
"accessControlAllowDesc": "선택한 앱만 VPN을 거칩니다",
"accessControlNotAllowDesc": "선택한 앱은 VPN에서 제외됩니다",
"accessControlDisabledDesc": "앱 접근 제어가 꺼져 있습니다",
"selected": "선택됨",
"port": "포트",
"logLevel": "로그 수준",
"show": "표시",
"exit": "종료",
"systemProxy": "시스템 프록시",
"safeMode": "안전 모드",
"safeModeAppTitle": "{appName} (안전 모드)",
"project": "프로젝트",
"core": "코어",
"tabAnimation": "탭 애니메이션",
"desc": "ClashMeta 기반의 멀티 플랫폼 프록시 클라이언트로, 간편하게 사용할 수 있으며 오픈 소스이고 광고가 없습니다.",
"startVpn": "VPN 시작 중…",
"stopVpn": "VPN 중지 중…",
"localNetworkDeniedTip": "로컬 네트워크 권한이 거부되었습니다. gvisor 스택을 사용하므로 LAN에 연결할 수 없습니다.",
"compatible": "호환 모드",
"notSelectedTip": "현재 프록시 그룹은 선택할 수 없습니다",
"changeProxyFailedTip": "프록시를 전환하지 못해 이전 선택을 복원했습니다",
"databaseWriteFailedTip": "변경 사항을 저장하지 못해 되돌렸습니다",
"tip": "안내",
"account": "계정",
"backup": "백업",
"backupSuccess": "백업 완료",
"noInfo": "정보 없음",
"pleaseBindWebDAV": "WebDAV를 연결하세요",
"bind": "연결",
"connectivity": "연결 상태: ",
"webDAVConfiguration": "WebDAV 구성",
"address": "주소",
"addressHelp": "WebDAV 서버 주소",
"addressTip": "올바른 WebDAV 주소를 입력하세요",
"password": "비밀번호",
"authentication": "인증",
"authenticationDesc": "인증을 켜면 로컬 프록시 포트에서 사용자 이름과 비밀번호를 요구합니다.",
"authenticationSystemProxyDesc": "인증이 켜져 있는 동안에는 시스템 프록시가 적용되지 않습니다.",
"checkUpdate": "업데이트 확인",
"discoverNewVersion": "새 버전 발견",
"checkUpdateError": "이미 최신 버전입니다",
"goDownload": "다운로드",
"unknown": "알 수 없음",
"country": "지역",
"search": "검색",
"allowBypass": "앱의 VPN 우회 허용",
"externalController": "외부 컨트롤러",
"externalControllerDesc": "외부 컨트롤러를 사용하면 9090 포트로 코어를 제어할 수 있습니다.",
"externalControllerSecret": "외부 컨트롤러 시크릿",
"ipv6Desc": "IPv6 트래픽은 IPv6가 켜져 있을 때만 수신됩니다.",
"app": "앱",
"general": "일반",
"startupAndBackground": "시작 및 백그라운드",
"requestsAndUpdates": "요청 및 업데이트",
"logsAndDiagnostics": "로그 및 진단",
"userAgent": "User-Agent",
"unifiedDelay": "통합 지연",
"tcpConcurrent": "TCP 동시 연결",
"geodataLoader": "Geo 저메모리 모드",
"requests": "요청",
"recentRequests": "최근 요청",
"findProcessMode": "프로세스 찾기",
"init": "초기화",
"infiniteTime": "만료되지 않음",
"connections": "연결",
"liveConnections": "실시간 연결",
"inbound": "인바운드",
"intranetIP": "내부망 IP",
"view": "보기",
"cut": "잘라내기",
"copy": "복사",
"clipboardWriteFailed": "클립보드에 복사하지 못했습니다. 선택 영역이 너무 클 수 있습니다",
"paste": "붙여넣기",
"testUrl": "테스트 URL",
"sync": "동기화",
"exclude": "최근 작업에서 숨기기",
"compact": "간결하게",
"tab": "탭",
"list": "목록",
"delay": "지연",
"style": "스타일",
"size": "크기",
"sort": "정렬",
"columns": "열",
"proxyGroup": "프록시 그룹",
"go": "이동",
"externalLink": "외부 링크",
"otherContributors": "기타 기여자",
"appIconDesign": "앱 아이콘 디자인",
"autoCloseConnections": "연결 자동 종료",
"autoCloseConnectionsDesc": "연결 자동 종료는 프록시를 전환할 때 열려 있는 연결을 끊습니다.",
"onlyStatisticsProxy": "프록시 트래픽만 집계",
"showNotificationStopAction": "알림에 중지 버튼 표시",
"pureBlackMode": "퓨어 블랙 모드",
"sidebarBlur": "사이드바 블러",
"sidebarBlurDesc": "사이드바를 통해 창 뒤의 흐릿한 바탕화면을 표시합니다",
"liquidGlass": "Liquid Glass",
"keepAliveIntervalDesc": "TCP 킵얼라이브 간격",
"entries": "개 항목",
"local": "로컬",
"remote": "원격",
"mode": "모드",
"time": "시간",
"source": "소스",
"action": "동작",
"intelligentSelected": "스마트 선택",
"clipboardImport": "클립보드에서 가져오기",
"clipboardExport": "클립보드로 내보내기",
"layout": "레이아웃",
"tight": "좁게",
"standard": "표준",
"ultra": "최고",
"loose": "넓게",
"profilesSort": "프로필 정렬",
"start": "시작",
"runTime": "실행 시간",
"stop": "중지",
"key": "키",
"value": "값",
"vpnEnableDesc": "VPN은 VpnService를 통해 모든 시스템 트래픽을 자동으로 라우팅합니다.",
"options": "옵션",
"enabled": "사용",
"disabled": "사용 안 함",
"loopback": "UWP 루프백 예외",
"providers": "외부 리소스",
"proxyProviders": "프록시 공급자",
"ruleProviders": "규칙 공급자",
"providerUrlTip": "원격 공급자만 지원합니다",
"ruleSetEmptyTip": "규칙 세트에 사용할 수 있는 항목이 없습니다",
"ruleSetMixedTip": "규칙 세트에 도메인과 IP 범위가 섞여 있어 유형을 판별할 수 없습니다",
"ruleSetInvalidTip": "이 mrs 파일을 읽을 수 없습니다",
"subscriptionInfo": "구독 정보",
"ntpStatusDesc": "NTP는 시스템 시계 대신 NTP 서버에서 시간을 가져옵니다.",
"server": "서버",
"ntpInterval": "동기화 간격(분)",
"dialerProxy": "다이얼러 프록시",
"dialerProxyDesc": "다이얼러 프록시는 NTP 서버에 연결할 때 사용하는 아웃바운드입니다.",
"writeToSystem": "시스템에 기록",
"writeToSystemDesc": "시스템에 기록하면 시스템 시계도 설정합니다. Android에서는 무시됩니다.",
"sniffer": "스니퍼",
"snifferStatusDesc": "스니퍼는 TLS, HTTP, QUIC 트래픽에서 도메인을 읽어 규칙이 일치할 수 있게 합니다.",
"overrideDestination": "대상 덮어쓰기",
"overrideDestinationDesc": "대상 덮어쓰기는 원래 주소 대신 스니핑한 도메인으로 연결합니다.",
"forceDnsMapping": "DNS 매핑 강제",
"forceDnsMappingDesc": "DNS 매핑 강제는 DNS 매핑에서 도메인을 얻은 연결도 스니핑합니다.",
"parsePureIp": "순수 IP 분석",
"parsePureIpDesc": "순수 IP 분석은 IP 주소만 있는 연결을 스니핑합니다.",
"forceDomain": "강제 도메인",
"forceDomainDesc": "강제 도메인은 항상 스니핑합니다. 이미 도메인이 있는 다른 연결은 그대로 둡니다.",
"skipDomain": "건너뛸 도메인",
"skipDomainDesc": "건너뛸 도메인 목록에 있는 스니핑된 도메인은 사용하지 않습니다.",
"skipSrcAddress": "건너뛸 출발지 주소",
"skipDstAddress": "건너뛸 목적지 주소",
"sniffProtocols": "프로토콜",
"ports": "포트",
"overrideEntries": "덮어쓰기 항목",
"addOverrideEntry": "덮어쓰기 항목 추가",
"dnsOverrideDesc": "여기에 추가한 항목은 모든 URL 및 파일 프로필의 DNS를 대체합니다. 추가하지 않은 항목은 프로필 자체 설정을 유지합니다. 사용자 지정 프로필은 내용에서 DNS를 설정합니다.",
"settingEntries": "설정",
"addSettingEntry": "설정 추가",
"profileSettingsDesc": "이 프로필에만 적용되며, 추가하지 않은 항목은 기본값을 사용합니다.",
"healthCheck": "상태 확인",
"discardChanges": "변경 사항을 취소하시겠습니까?",
"quickEdit": "빠른 편집",
"status": "상태",
"statusDesc": "DNS를 끄면 시스템 DNS를 사용합니다.",
"respectRules": "규칙 준수",
"respectRulesDesc": "규칙 준수는 DNS 연결을 규칙에 따라 라우팅하며 Proxy Server Nameserver가 필요합니다.",
"dnsMode": "DNS 모드",
"fakeipRange": "Fake-IP 범위",
"fakeipFilter": "Fake-IP 필터",
"useHosts": "hosts 사용",
"useSystemHosts": "시스템 hosts 사용",
"fallbackFilter": "폴백 필터",
"domain": "도메인",
"listenRoutingMark": "수신 라우팅 마크",
"listenRoutingMarkDesc": "수신 라우팅 마크는 Linux에서만 작동합니다.",
"ipv6Timeout": "IPv6 타임아웃(ms)",
"cacheAlgorithm": "캐시 알고리즘",
"cacheMaxSize": "캐시 크기",
"fakeipRange6": "Fake-IP 범위(IPv6)",
"fakeipFilterMode": "Fake-IP 필터 모드",
"fakeipFilterModeDesc": "Fake-IP 필터 모드에서 블랙리스트는 일치 항목을 제외하고, 화이트리스트는 일치 항목에만 가짜 IP를 사용하며, 규칙은 규칙에 따라 일치시킵니다.",
"fakeipTtl": "Fake-IP TTL",
"reset": "초기화",
"actionView": "표시/숨기기",
"actionStart": "시작/중지",
"actionMode": "모드 전환",
"actionProxy": "시스템 프록시",
"actionTun": "TUN",
"actionRuleMode": "규칙 모드",
"actionGlobalMode": "전역 모드",
"actionDirectMode": "직접 모드",
"actionDelayTest": "모든 지연 테스트",
"actionUpdateProfiles": "프로필 업데이트",
"disclaimer": "면책 조항",
"disclaimerDesc": "FlClash(이하 “본 소프트웨어”)를 사용하기 전에 이 문서를 주의 깊게 읽고 모든 내용을 이해했는지 확인하세요. “동의”를 누르면 아래 모든 조항을 읽고 이해했으며 수락한 것으로 간주합니다. 동의하지 않으면 “종료”를 누르고 본 소프트웨어의 사용을 중단하세요.",
"disclaimerSoftwareTitle": "소프트웨어의 성격",
"disclaimerSoftwareContent": "본 소프트웨어는 Clash.Meta(mihomo) 코어를 기반으로 한 오픈 소스 네트워크 프록시 클라이언트입니다. 구성 관리, 규칙 기반 라우팅, 트래픽 전달과 같은 로컬 도구만 제공합니다.",
"disclaimerNoServiceStatement": "본 소프트웨어 자체는 프록시 서버, 노드, 구독, 네트워크 접속 서비스를 일절 제공하지 않으며, 이러한 서비스의 어떤 제공자와도 제휴, 대리 또는 보증 관계가 없습니다.",
"disclaimerUsageTitle": "사용 제한",
"disclaimerUsageContent": "본 소프트웨어는 학습, 교류, 기술 연구와 같은 비상업적 용도로만 제공됩니다. 유료 배포, 끼워 팔기, 상업 서비스의 일부로 사용, 본 소프트웨어의 이름을 내세운 영업 등 어떠한 상업적 목적의 사용도 엄격히 금지되며, 이에 국한되지 않습니다. 모든 상업 활동은 본 소프트웨어 및 개발자와 무관합니다.\n\n거주 국가 또는 지역의 법령을 위반하는 용도로 본 소프트웨어를 사용하는 것은 엄격히 금지됩니다. 여기에는 합법적으로 부과된 네트워크 접속 제한 우회, 불법 콘텐츠 유포, 네트워크 공격, 타인의 합법적 권리 침해가 포함되며 이에 국한되지 않습니다.",
"disclaimerResponsibilityTitle": "사용자의 책임",
"disclaimerResponsibilityContent": "본 소프트웨어의 사용이 거주지에서 적법한지 확인할 책임은 사용자에게 있으며, 본 소프트웨어로 한 모든 행위와 그 결과에 대한 법적 책임은 사용자 본인이 부담합니다.\n\n가져오는 구독, 노드, 구성은 사용자 본인의 선택입니다. 출처의 적법성, 콘텐츠의 안전성, 서비스의 신뢰성은 사용자와 해당 제공자 사이의 문제입니다.",
"disclaimerThirdPartyTitle": "제3자 콘텐츠",
"disclaimerThirdPartyContent": "구독 링크, 구성 파일, 규칙 세트, 스크립트, 외부 리소스, 외부 링크는 모두 제3자가 제공합니다. 개발자는 이들의 적법성, 정확성, 안전성, 가용성을 심사하거나 보증할 수 없으며 보증하지 않습니다.\n\n제3자 콘텐츠로 인한 데이터 유출, 금전적 손실, 계정 정지 및 기타 손해는 사용자와 해당 제3자 사이에서 해결해야 하며, 개발자는 이에 대해 책임을 지지 않습니다.",
"disclaimerWarrantyTitle": "보증 부인",
"disclaimerWarrantyContent": "본 소프트웨어는 “있는 그대로”, “이용 가능한 상태로” 제공되며, 상품성, 특정 목적에의 적합성, 비침해성, 중단 없는 이용 가능성, 오류 없음, 보안 취약점 없음에 대한 보증을 포함하되 이에 국한되지 않고 명시적이든 묵시적이든 어떠한 종류의 보증도 하지 않습니다.\n\n개발자는 본 소프트웨어가 사용자의 요구를 충족하거나 중단 및 오류 없이 실행될 것임을 보증하지 않습니다.",
"disclaimerLiabilityTitle": "책임의 제한",
"disclaimerLiabilityContent": "관련 법률이 허용하는 최대 범위에서, 개발자와 기여자는 본 소프트웨어의 사용 또는 사용 불능으로 발생하는 직접, 간접, 부수적, 특별, 징벌적 또는 결과적 손해에 대해 책임을 지지 않습니다. 여기에는 데이터 손실, 기기 손상, 네트워크 장애, 사업 중단, 이익 손실 및 그로 인한 법적 분쟁이 포함되며 이에 국한되지 않고, 그러한 손해의 가능성을 사전에 통지받은 경우에도 마찬가지입니다.",
"disclaimerPrivacyTitle": "데이터 수집 및 개인정보 보호",
"disclaimerPrivacyContent": "본 소프트웨어는 구독 URL, 노드 정보, 구성 내용, 방문한 웹사이트, 연결 기록, 트래픽 내용, 로그를 수집하거나 업로드하지 않습니다. 이 데이터는 사용자의 기기에만 저장되며 개발자는 접근할 수 없습니다.\n\n본 소프트웨어는 프로필을 업데이트할 때 사용자가 입력한 구독 URL을 가져오거나 업데이트를 확인할 때 GitHub에 접속하는 등 기능이 필요할 때만 네트워크에 접속합니다.\n\n본 소프트웨어에는 어떤 플랫폼에서도 분석 또는 비정상 종료 보고 서비스가 포함되어 있지 않습니다.",
"disclaimerLicenseTitle": "오픈 소스 라이선스",
"disclaimerLicenseContent": "본 소프트웨어는 GPL-3.0 라이선스로 공개된 오픈 소스입니다. 파생 저작물도 GPL-3.0으로 배포하고 원저작권 고지를 유지해야 하는 라이선스 조건을 준수하는 한 자유롭게 사용, 수정, 배포할 수 있습니다.\n\nClash.Meta 코어를 포함한 본 소프트웨어의 제3자 구성 요소는 각자의 라이선스를 따릅니다. 원저작자는 수정 또는 재배포된 버전에서 발생하는 어떠한 문제에도 책임을 지지 않습니다.",
"disclaimerChangesTitle": "본 문서의 변경",
"disclaimerChangesContent": "개발자는 릴리스와 함께 본 문서를 개정할 수 있으며, 개정 내용은 해당 릴리스가 공개되는 시점에 효력이 발생합니다. 업데이트 후에도 본 소프트웨어를 계속 사용하면 개정된 문서를 수락한 것으로 간주합니다.",
"disclaimerAcceptTitle": "동의",
"disclaimerAcceptContent": "본 소프트웨어를 설치, 복사 또는 사용하면 이 문서 전체를 읽고 동의한 것으로 간주합니다. 어느 조항이라도 동의하지 않는 경우 즉시 본 소프트웨어의 사용을 중단하고 삭제하세요.",
"disclaimerRestateTitle": "문구 다시 입력",
"disclaimerRestateTip": "내용을 이해했는지 확인하기 위해 아래 입력란에 다음 문구를 그대로 입력하세요.",
"disclaimerRestateHint": "위 문구를 입력하세요",
"disclaimerRestateMismatch": "문구와 일치하지 않습니다",
"disclaimerAcknowledgeTitle": "문구 확인",
"disclaimerAcknowledgeTip": "내용을 이해했는지 확인하기 위해 다음 문구를 읽고 아래 항목에 체크하세요.",
"disclaimerAcknowledgeLabel": "이 문구를 읽고 이해했습니다",
"agree": "동의",
"hotkeyManagement": "단축키 관리",
"hotkeyDesc": "전역 단축키는 창이 숨겨져 있어도 작동합니다. 동작을 눌러 키 조합을 기록하세요.",
"hotkeyNotSet": "설정 안 됨",
"pressKeyboard": "키 조합을 누르세요",
"hotkeyNeedsModifier": "{modifiers} 중 하나 이상을 포함하세요",
"hotkeyConflictWith": "이미 “{action}”에서 사용 중입니다. 저장하면 이쪽으로 옮겨집니다.",
"hotkeyUnavailable": "등록되지 않음: 다른 앱이 사용 중일 수 있습니다",
"remove": "제거",
"noNetwork": "네트워크 없음",
"ipv6InboundDesc": "IPv6를 켜면 VPN이 IPv6 인바운드를 허용합니다.",
"exportLogs": "로그 내보내기",
"exportSuccess": "내보내기 완료",
"iconStyle": "아이콘 스타일",
"iconStyleFilled": "채움",
"iconStylePlain": "기본",
"iconStyleHidden": "숨김",
"filter": "필터",
"hideTimeoutProxies": "시간 초과된 프록시 숨기기",
"hideTimeoutProxiesDesc": "마지막 지연 테스트에서 시간 초과된 프록시를 제외합니다",
"stackMode": "스택 모드",
"network": "네트워크",
"bypassDomain": "우회 도메인",
"bypassDomainDesc": "우회 도메인은 시스템 프록시가 켜져 있을 때만 적용됩니다.",
"resetTip": "초기화하시겠습니까?",
"icon": "아이콘",
"noData": "데이터 없음",
"fontFamily": "글꼴",
"installedFonts": "설치된 글꼴",
"toggle": "전환",
"system": "시스템",
"routeAddress": "라우팅 주소",
"interfaceNameMode": "아웃바운드 인터페이스",
"interfaceNameModeClear": "지정 안 함",
"interfaceNameModeFollow": "구성 따르기",
"interfaceNameModeCustom": "사용자 지정",
"interfaceName": "인터페이스 이름",
"interfaceNameDesc": "인터페이스 이름은 아웃바운드 연결이 사용하는 네트워크 인터페이스입니다.",
"copyEnvVar": "환경 변수 복사",
"memoryInfo": "메모리 정보",
"total": "합계",
"releaseMemory": "메모리 해제",
"memoryCoreHeapInuse": "사용 중인 힙",
"memoryCoreHeapIdle": "유휴 힙",
"memoryCoreStack": "고루틴 스택",
"memoryCoreRuntime": "런타임 오버헤드",
"memoryCoreNotRunning": "코어가 실행 중이 아닙니다",
"memoryAppResident": "상주 메모리",
"memoryAppShared": "앱 및 공유",
"memoryEstimateDesc": "프로세스 상주 메모리로 추정한 값이며 시스템에 표시되는 값과 다를 수 있습니다.",
"memoryEstimateSharedDesc": "코어는 앱 프로세스 안에서 실행됩니다. 코어의 점유량은 런타임 통계로 추정하며, 나머지는 앱 및 공유 메모리로 집계합니다.",
"memoryReleased": "메모리를 해제했습니다",
"memoryReleasedSize": "{size} 해제됨",
"releaseMemoryFailed": "메모리를 해제하지 못했습니다",
"cancel": "취소",
"fileIsUpdate": "파일이 수정되었습니다. 변경 사항을 저장하시겠습니까?",
"profileHasUpdate": "프로필이 수정되었습니다. 자동 업데이트를 끄시겠습니까?",
"hasCacheChange": "변경 사항을 캐시하시겠습니까?",
"copySuccess": "복사했습니다",
"copyLink": "링크 복사",
"exportFile": "파일 내보내기",
"cacheCorrupt": "캐시가 손상되었습니다. 지우시겠습니까?",
"detectionTip": "서드파티 API에 의존하므로 참고용으로만 사용하세요",
"listen": "수신",
"undo": "실행 취소",
"redo": "다시 실행",
"lineWrap": "자동 줄 바꿈",
"none": "없음",
"advancedConfig": "고급 구성",
"advancedConfigDesc": "고급 구성에는 네트워크, DNS, 추가한 규칙, 스크립트가 포함됩니다.",
"addRule": "규칙 추가",
"content": "내용",
"ruleTarget": "규칙 대상",
"matchTarget": "MATCH-TARGET",
"followProfile": "프로필 따르기",
"sourceIp": "출발지 IP",
"noResolve": "IP 확인 안 함",
"saveChanges": "변경 사항을 저장하시겠습니까?",
"cannotSave": "저장할 수 없음",
"navigationBarStyle": "하단 바",
"floating": "플로팅",
"docked": "고정",
"slide": "슬라이드",
"fade": "페이드",
"pureBlack": "퓨어 블랙",
"customColor": "사용자 지정 색상",
"colorStyle": "색상 스타일",
"grid": "격자",
"spectrum": "스펙트럼",
"sliders": "슬라이더",
"hue": "색조",
"saturation": "채도",
"brightness": "밝기",
"hex": "16진수",
"monochromeScheme": "모노크롬",
"vividStyle": "선명",
"tintedStyle": "틴트",
"softStyle": "부드러움",
"developerMode": "개발자 모드",
"developerModeEnableTip": "개발자 모드가 켜졌습니다.",
"messageTest": "메시지 테스트",
"messageTestTip": "이것은 메시지입니다.",
"crashTest": "비정상 종료 테스트",
"crashDetected": "비정상 종료 감지됨",
"crashDetectedTip": "앱이 두 번 연속 시작을 완료하지 못했습니다. 반복을 끊기 위해 프로필 {name}의 선택을 해제하고 자동 설정을 건너뛰었습니다. 언제든지 다시 선택할 수 있습니다.",
"launchInterrupted": "시작이 완료되지 않음",
"launchInterruptedTip": "지난번 시작하는 중에 앱이 예기치 않게 종료되었습니다. 이번 실행에서는 자동 설정을 건너뛰었으니 직접 시작하여 다시 시도하세요.",
"clearData": "데이터 지우기",
"textScale": "글자 크기 조절",
"textScalePreview": "앱의 글자가 이렇게 표시됩니다",
"followSystem": "시스템 설정 따르기",
"internet": "인터넷",
"systemApp": "시스템 앱",
"noNetworkApp": "네트워크 미사용 앱",
"restoreStrategy": "복원 방식",
"restoreStrategyOverride": "덮어쓰기",
"restoreStrategyCompatible": "호환",
"logsTest": "로그 테스트",
"emptyTip": "{label}을(를) 입력하세요",
"urlTip": "{label}은(는) URL이어야 합니다",
"invalidLinkTip": "구독 링크 또는 프록시 공유 링크가 아닙니다",
"numberTip": "{label}은(는) 숫자여야 합니다",
"numberRangeTip": "{label}은(는) {min}에서 {max} 사이여야 합니다",
"interval": "간격",
"existsTip": "{label}이(가) 이미 있습니다",
"maxLengthTip": "{label}은(는) 최대 {max}자까지 입력할 수 있습니다",
"singleAdd": "개별 추가",
"batchAdd": "일괄 추가",
"batchListInputTip": "한 줄에 하나씩 또는 쉼표로 구분하여 입력하세요",
"batchMapInputTip": "한 줄에 하나씩, 키, 공백, 값 순서로 입력하세요",
"singleImport": "개별 가져오기",
"batchImport": "일괄 가져오기",
"batchUrlInputTip": "한 줄에 URL 하나씩",
"batchLinkInputTip": "한 줄에 링크 하나씩",
"batchPreviewTip": "추가 {count}개, 이미 있어서 건너뜀 {skipped}개",
"lineIssueTip": "{line}행: {message}",
"singleValueTip": "{label}은(는) 하나만 입력해야 합니다",
"deleteTip": "이 {label}을(를) 삭제하시겠습니까?",
"deleteMultipTip": "선택한 {label}을(를) 삭제하시겠습니까?",
"nullTip": "{label}이(가) 아직 없습니다",
"script": "스크립트",
"color": "색상",
"unnamed": "이름 없음",
"pleaseEnterScriptName": "스크립트 이름을 입력하세요",
"mixedPort": "혼합 포트",
"socksPort": "SOCKS 포트",
"redirPort": "Redir 포트",
"tproxyPort": "TProxy 포트",
"portTip": "{label}은(는) 1024에서 49151 사이여야 합니다",
"portConflictTip": "다른 포트를 입력하세요",
"import": "가져오기",
"importFile": "파일에서 가져오기",
"importUrl": "URL에서 가져오기",
"startFromScratch": "처음부터 시작",
"autoSetSystemDns": "시스템 DNS 자동 설정",
"details": "{label} 상세 정보",
"creationTime": "생성 시간",
"process": "프로세스",
"host": "호스트",
"destination": "목적지",
"destinationGeoIP": "목적지 GeoIP",
"destinationIPASN": "목적지 IP ASN",
"specialProxy": "특수 프록시",
"specialRules": "특수 규칙",
"remoteDestination": "원격 목적지",
"networkType": "네트워크 유형",
"proxyChains": "프록시 체인",
"log": "로그",
"connection": "연결",
"request": "요청",
"connected": "연결됨",
"disconnected": "연결 끊김",
"connecting": "연결 중…",
"restartCoreTip": "코어를 다시 시작하시겠습니까?",
"forceRestartCoreTip": "코어를 강제로 다시 시작하시겠습니까?",
"dnsHijacking": "DNS 하이재킹",
"coreStatus": "코어 상태",
"appendSystemDns": "시스템 DNS 추가",
"editRule": "규칙 편집",
"extendMode": "확장 모드",
"standardModeDesc": "표준 모드: 프로필 위에 규칙을 추가합니다",
"scriptModeDesc": "스크립트 모드: 확장 스크립트로 프로필 전체를 다시 작성합니다",
"addedRules": "추가한 규칙",
"controlGlobalAddedRules": "전역 추가 규칙 관리",
"quickActions": "빠른 작업",
"rulePresetBlockQuic": "QUIC 차단",
"rulePresetBlockStun": "STUN 차단",
"rulePresetBlockDot": "DNS over TLS 차단",
"rulePresetBlockLoopback": "프록시 루프백 차단",
"rulePresetSystemServicesDirect": "Apple 및 Microsoft 직접 연결",
"rulePresetBittorrentDirect": "BitTorrent 직접 연결",
"rulePresetLanDirect": "LAN 직접 연결",
"extendScript": "확장 스크립트",
"goToConfigureScript": "스크립트 구성으로 이동",
"goToConfigureAddedRules": "추가한 규칙 구성으로 이동",
"editGlobalRules": "전역 규칙 편집",
"confirmForceCrashCore": "코어를 강제로 비정상 종료시키시겠습니까?",
"confirmClearAllData": "모든 데이터를 지우시겠습니까?",
"loading": "로드 중…",
"yearsAgo": "{count, plural, other{{count}년 전}}",
"monthsAgo": "{count, plural, other{{count}개월 전}}",
"daysAgo": "{count, plural, other{{count}일 전}}",
"hoursAgo": "{count, plural, other{{count}시간 전}}",
"minutesAgo": "{count, plural, other{{count}분 전}}",
"justNow": "방금",
"yearsAgoShort": "{count, plural, other{{count}년 전}}",
"monthsAgoShort": "{count, plural, other{{count}개월 전}}",
"daysAgoShort": "{count, plural, other{{count}일 전}}",
"hoursAgoShort": "{count, plural, other{{count}시간 전}}",
"minutesAgoShort": "{count, plural, other{{count}분 전}}",
"noLongerRemind": "다시 알리지 않음",
"accessControlSettings": "접근 제어 설정",
"turnOn": "켜기",
"turnOff": "끄기",
"vpnConfigChangeDetected": "VPN 관련 구성 변경이 감지되었습니다",
"restart": "다시 시작",
"speedStatistics": "속도 통계",
"resetPageChangesTip": "이 페이지에 변경 사항이 있습니다. 초기화하시겠습니까?",
"customProfile": "사용자 지정",
"customProfileDesc": "앱 프록시와 다른 프로필로 프록시 그룹과 규칙을 조합합니다",
"customProfileResources": "사용자 지정 프로필 리소스",
"customProfileResourcesDesc": "사용자 지정 프로필 리소스에는 프록시, 규칙 공급자, 필터, 아이콘 세트가 포함됩니다.",
"unknownNetworkError": "알 수 없는 네트워크 오류",
"backupFromNewerVersion": "이 백업은 더 최신 버전의 앱에서 만들어졌습니다. 복원하기 전에 앱을 업데이트하세요",
"networkTimeoutError": "요청 시간이 초과되었습니다. 네트워크나 프록시를 확인한 후 다시 시도하세요",
"networkHostLookupError": "서버 주소를 확인할 수 없습니다. URL이 올바른지, DNS가 정상인지 확인하세요",
"networkConnectionError": "서버에 연결할 수 없습니다. 네트워크 연결이나 프록시 설정을 확인하세요",
"networkTlsError": "보안 연결에 실패했습니다. 서버 인증서가 올바르지 않거나 연결이 가로채이고 있을 수 있습니다",
"networkCancelledError": "요청이 취소되었습니다",
"networkAccessDeniedError": "서버가 접근을 거부했습니다(HTTP {code}). 링크가 만료되었거나 자격 증명이 잘못되었을 수 있습니다",
"networkNotFoundError": "이 주소에서 아무것도 찾지 못했습니다(HTTP {code}). URL이 올바른지 확인하세요",
"networkRateLimitedError": "요청이 너무 많습니다(HTTP 429). 잠시 후 다시 시도하세요",
"networkServerError": "서버에 문제가 발생했습니다(HTTP {code}). 나중에 다시 시도하세요",
"networkBadResponseError": "서버가 요청을 거부했습니다(HTTP {code})",
"networkRequestFailed": "네트워크 요청 실패: {detail}",
"invalidBackupFile": "올바르지 않은 백업 파일",
"pruneCache": "캐시 정리",
"backupAndRestore": "백업 및 복원",
"restore": "복원",
"restoreSuccess": "복원 완료",
"restoreOnlyConfig": "프로필만 복원",
"restoreAllData": "모든 데이터 복원",
"addProfile": "프로필 추가",
"delayTest": "지연 테스트",
"proxyGroupEmpty": "프록시 그룹이 비어 있습니다",
"confirmExitWindow": "현재 창을 닫으시겠습니까?",
"optional": "선택 사항",
"maxFailedTimes": "최대 실패 횟수",
"testInterval": "테스트 간격",
"excludeFilter": "제외 필터",
"expectedStatus": "예상 상태",
"strategy": "전략",
"tolerance": "허용 오차",
"inputProxyGroupName": "프록시 그룹 이름을 입력하세요",
"helperCorruptTip": "헬퍼 서비스를 사용할 수 없어 TUN 모드를 켤 수 없습니다. FlClash를 다시 설치하여 복구하세요.",
"tunAuthorizationFailed": "TUN 권한을 얻지 못해 끄기로 되돌렸습니다.",
"sudoPasswordTitle": "sudo 비밀번호를 입력하세요",
"coreBlockedByPolicyTip": "Windows에서 FlClashCore.exe 실행을 거부했습니다(오류 {code}). Smart App Control이나 AppLocker와 같은 앱 제어 정책이 서명되지 않은 프로그램을 차단합니다. 해당 정책에서 FlClash를 허용하거나 정책을 끈 후 다시 시도하세요.",
"coreBlockedBySmartAppControlTip": "서명되지 않아 Windows Smart App Control이 FlClashCore.exe를 차단했습니다. Windows 보안 → 앱 및 브라우저 컨트롤 → Smart App Control 설정에서 끔을 선택한 후 FlClash를 다시 시작하세요. Smart App Control은 Windows를 다시 설치하지 않으면 다시 켤 수 없습니다.",
"hideFromList": "목록에서 숨기기",
"testWhenUsed": "사용할 때 테스트",
"disableUDP": "UDP 사용 안 함",
"confirmDeleteProxyGroup": "이 프록시 그룹을 삭제하시겠습니까?",
"ruleEmpty": "규칙이 비어 있습니다",
"inputRuleContent": "규칙 내용을 입력하세요",
"ruleSet": "규칙 세트",
"selectRuleSet": "규칙 세트를 선택하세요",
"splitStrategy": "분할 전략",
"selectSplitStrategy": "분할 전략을 선택하세요",
"noResolveHostname": "호스트 이름 확인 안 함",
"matchSourceIp": "출발지 IP 일치",
"basicInfo": "기본 정보",
"additionalParameters": "추가 매개변수",
"proxyType": "프록시 유형",
"basicStrategy": "기본 전략",
"editProxy": "프록시 편집",
"addNodes": "노드 추가",
"addProxyGroup": "프록시 그룹 추가",
"editProxyGroup": "프록시 그룹 편집",
"iconUrl": "아이콘 URL",
"iconRecords": "아이콘 기록",
"custom": "사용자 지정",
"ruleActionDomainDesc": "전체 도메인 일치",
"ruleActionDomainSuffixDesc": "도메인 접미사 일치",
"ruleActionDomainKeywordDesc": "도메인 키워드 일치",
"ruleActionDomainRegexDesc": "도메인 정규식 일치",
"ruleActionDomainWildcardDesc": "와일드카드 일치, *와 ?만 지원",
"ruleActionGeositeDesc": "Geosite의 도메인 일치",
"ruleActionIpCidrDesc": "IP 주소 범위 일치",
"ruleActionIpCidr6Desc": "IP 주소 범위 일치, IP-CIDR6는 별칭일 뿐입니다",
"ruleActionIpSuffixDesc": "IP 접미사 범위 일치",
"ruleActionIpAsnDesc": "IP의 ASN 일치",
"ruleActionGeoipDesc": "IP의 국가 코드 일치",
"ruleActionSrcGeoipDesc": "출발지 IP의 국가 코드 일치",
"ruleActionSrcIpAsnDesc": "출발지 IP의 ASN 일치",
"ruleActionSrcIpCidrDesc": "출발지 IP 주소 범위 일치",
"ruleActionSrcIpSuffixDesc": "출발지 IP 접미사 범위 일치",
"ruleActionDstPortDesc": "목적지 포트 범위 일치",
"ruleActionSrcPortDesc": "출발지 포트 범위 일치",
"ruleActionInPortDesc": "인바운드 포트 일치",
"ruleActionInTypeDesc": "인바운드 유형 일치",
"ruleActionInUserDesc": "인바운드 사용자 이름 일치, 여러 사용자 이름은 /로 구분",
"ruleActionInNameDesc": "인바운드 이름 일치",
"ruleActionRematchNameDesc": "리매치 이름 일치, 여러 이름은 /로 구분",
"ruleActionProcessPathDesc": "전체 프로세스 경로로 일치",
"ruleActionProcessPathRegexDesc": "프로세스 경로 정규식으로 일치",
"ruleActionProcessPathWildcardDesc": "프로세스 경로 와일드카드로 일치, *와 ?만 지원",
"ruleActionProcessNameDesc": "프로세스 이름으로 일치, Android에서는 패키지 이름과 일치",
"ruleActionProcessNameRegexDesc": "프로세스 이름 정규식으로 일치, Android에서는 패키지 이름과 일치",
"ruleActionProcessNameWildcardDesc": "프로세스 이름 와일드카드로 일치, *와 ?만 지원",
"ruleActionUidDesc": "Linux 사용자 ID 일치",
"ruleActionNetworkDesc": "TCP 또는 UDP 일치",
"ruleActionDscpDesc": "DSCP 마크 일치(tproxy UDP 인바운드 전용)",
"ruleActionRuleSetDesc": "규칙 세트 참조, rule-providers 필요",
"ruleActionAndDesc": "논리 규칙 AND",
"ruleActionOrDesc": "논리 규칙 OR",
"ruleActionNotDesc": "논리 규칙 NOT",
"ruleActionSubRuleDesc": "하위 규칙으로 일치, 괄호에 유의",
"ruleActionMatchDesc": "모든 요청과 일치, 조건 불필요",
"contentNotEmpty": "내용을 입력하세요",
"splitStrategyNotEmpty": "분할 전략을 입력하세요",
"timeout": "시간 초과",
"delayFailed": "실패",
"invalidSubRule": "올바르지 않은 SUB_RULE: {subRule}",
"invalidPolicy": "올바르지 않은 정책: {target}",
"invalidProxyProvider": "올바르지 않은 프록시 공급자: {providerName}",
"invalidRuleSet": "올바르지 않은 규칙 세트: {ruleSet}",
"invalidNetworkContent": "tcp 또는 udp만 지원합니다",
"invalidRangeContent": "80 또는 8000-9000과 같은 숫자나 범위를 /로 구분하여 입력하세요",
"invalidDscpContent": "DSCP 마크는 63을 넘을 수 없습니다",
"invalidCidrContent": "192.168.0.0/16과 같은 CIDR 형식의 IP 범위를 입력하세요",
"invalidPortRangeContent": "443 또는 8000-9000과 같은 포트나 범위를 입력하세요",
"invalidHostContent": "도메인 또는 IP 주소를 입력하세요",
"invalidListenContent": "0.0.0.0:1053과 같은 주소와 포트를 입력하세요",
"invalidProxy": "올바르지 않은 프록시: {proxyName}",
"locationPermissionRequired": "위치 권한 필요",
"locationPermissionGuide": "1. 시스템 설정 > 개인정보 보호 및 보안을 엽니다\n2. 위치 서비스를 선택합니다\n3. 목록에서 {appName}을(를) 찾아 체크합니다\n\n완료되면 앱으로 돌아와 계속하세요. 협조해 주셔서 감사합니다.",
"prerequisites": "사전 요건",
"ignoreBatteryOptimization": "배터리 최적화 무시",
"batteryOptimizationDesc": "배터리 최적화를 무시하면 앱이 백그라운드에서 계속 실행됩니다.",
"locationPermission": "위치 권한",
"locationPermissionDesc": "시스템에서 Wi-Fi 이름을 읽으려면 위치 권한이 필요합니다. Android에서는 “항상 허용”을 선택해야 하며, 그렇지 않으면 앱이 백그라운드에 있을 때 Wi-Fi 이름을 읽을 수 없습니다.",
"excludeSsids": "제외할 SSID",
"excludeSsidsDesc": "제외 SSID의 Wi-Fi에 연결되면 앱의 실행 상태가 자동으로 전환됩니다.",
"ssidsEmpty": "SSID가 비어 있습니다",
"onDemand": "필요 시 실행",
"onDemandDesc": "필요 시 실행은 제외된 Wi-Fi에서 프록시를 일시 중지합니다.",
"locationPermissionDeniedMessage": "위치 권한이 거부되어 현재 Wi-Fi 이름을 읽을 수 없습니다. 시스템 설정에서 위치 권한을 직접 켜 주세요.",
"addSsid": "SSID 추가",
"editSsid": "SSID 편집",
"authorized": "허용됨",
"tapToAuthorize": "눌러서 허용",
"suspended": "일시 중지됨…",
"resourceUpdateInterval": "자동 업데이트 간격",
"resourceUpdateIntervalTip": "자동 업데이트 간격은 0보다 커야 합니다",
"hours": "시간",
"minutes": "분",
"geoResources": "Geo",
"geoSkipped": "{name}: 이미 최신 상태입니다",
"geoUpdated": "{name}: 업데이트됨",
"secondsCount": "{count, plural, other{{count}초}}",
"proxiesCount": "{count, plural, other{프록시 {label}개}}",
"rulesCount": "{count, plural, other{규칙 {label}개}}",
"proxyGroupsCount": "{count, plural, other{프록시 그룹 {label}개}}",
"changelogBreaking": "호환성 변경",
"changelogFeatures": "새로운 기능",
"changelogFixes": "버그 수정",
"changelogPerformance": "성능",
"changelogReverts": "되돌림",
"close": "닫기",
"back": "뒤로",
"minimize": "최소화",
"maximize": "최대화",
"unmaximize": "이전 크기로 복원",
"pinWindow": "창 고정",
"unpinWindow": "창 고정 해제",
"exitFullScreen": "전체 화면 종료",
"torch": "손전등",
"pickFromAlbum": "앨범에서 선택",
"blockConnection": "연결 차단",
"closeConnections": "연결 종료",
"scrollToSelected": "선택 항목으로 스크롤",
"showMore": "펼치기",
"showLess": "접기",
"previousMatch": "이전 결과",
"nextMatch": "다음 결과",
"clearSearch": "검색 지우기",
"noSearchResults": "일치하는 결과 없음",
"showPassword": "비밀번호 표시",
"hidePassword": "비밀번호 숨기기",
"authorize": "허용",
"installedAppsPermissionRequired": "앱 목록 권한 필요",
"installedAppsPermissionDesc": "이 시스템은 권한을 허용하기 전까지 설치된 앱 목록을 숨깁니다. 앱별 프록시를 구성하려면 권한을 허용하세요.",
"installedAppsPermissionDeniedMessage": "앱 목록 권한이 거부되어 설치된 앱을 표시할 수 없습니다. 시스템 설정에서 직접 허용해 주세요.",
"outboundIp": "아웃바운드 IP",
"ipAddress": "IP 주소",
"ipQualityLevel": "등급",
"ipQualityGood": "양호",
"ipQualityNormal": "보통",
"ipQualityRisky": "위험",
"ipType": "유형",
"ipTypeResidential": "가정용",
"ipTypeMobile": "모바일 네트워크",
"ipTypeBusiness": "기업용",
"ipTypeHosting": "데이터 센터",
"ipFlags": "플래그",
"ipFlagProxy": "프록시",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "악용 이력",
"ipOrganization": "조직",
"ipAsn": "ASN",
"ipQualitySource": "응답 출처",
"ipQualitySources": "출처",
"ipQualityFailed": "IP 유형을 확인할 수 없습니다",
"ipQualityRetry": "다시 확인",
"ipSourceNoType": "유형 없음",
"ipSourceRateLimited": "요청 제한됨",
"ipSourceIpMismatch": "아웃바운드 IP가 다름",
"hideIp": "IP 숨기기",
"editorUnavailable": "편집기를 사용할 수 없음",
"replace": "바꾸기",
"replaceAll": "모두 바꾸기",
"cameraPermissionRequired": "카메라 권한 필요",
"cameraPermissionDesc": "QR 코드를 스캔하려면 시스템 설정에서 카메라 접근을 허용하거나, 앨범에서 QR 코드 이미지를 선택하세요.",
"cameraUnavailable": "카메라를 사용할 수 없음",
"retry": "다시 시도",
"invalidProfileQrcode": "이 QR 코드에는 프로필 링크가 없습니다",
"dnsQueries": "DNS 쿼리",
"recordType": "레코드 유형",
"answers": "응답",
"responseCode": "응답 코드",
"error": "오류",
"initiator": "요청자",
"cache": "캐시",
"yes": "예",
"no": "아니요",
"nonTextProviderFile": "이 외부 리소스는 텍스트 파일이 아닙니다",
"addCustomProxy": "프록시 추가",
"definitionNotMap": "구성은 이름과 유형이 있는 YAML 매핑이어야 합니다",
"ruleTextInvalid": "규칙은 DOMAIN,example.com,DIRECT처럼 지원하는 유형으로 시작해야 합니다",
"ruleListInvalid": "규칙은 항목마다 규칙 하나인 YAML 목록이어야 합니다. 예: - DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "링크에서 읽을 수 있는 프록시가 없습니다",
"shareLinkUnreadable": "읽을 수 있는 프록시가 없습니다: {link}",
"proxiesProfileLabel": "{name} 외 (프록시 {count}개)",
"singleShareLinkOnly": "여기에는 링크를 하나만 입력하세요. 여러 개는 목록의 빠른 편집에서 편집하세요",
"username": "사용자 이름",
"emptyFallback": "빈 폴백",
"defaultSelected": "기본 선택",
"hashByInUser": "인바운드 사용자별 해시",
"appProxiesEmpty": "로컬 프록시 없음",
"nodes": "노드",
"localProxies": "로컬 프록시",
"customIssueEmptyName": "이름이 비어 있습니다",
"customIssueReservedName": "내장 정책 이름이라 여기에서 사용할 수 없습니다: {name}",
"customIssueDuplicateName": "다른 프록시 또는 프록시 그룹이 이미 사용 중인 이름입니다: {name}",
"customIssueCoreRejected": "코어가 이 프록시를 해석할 수 없습니다: {message}",
"customIssueMissingProxies": "존재하지 않는 프록시 또는 정책: {names}",
"customIssueMissingProviders": "존재하지 않는 프록시 공급자: {names}",
"customIssueNoProxySource": "선택한 프록시나 프록시 공급자가 없어 코어가 이 그룹을 거부합니다",
"customIssueGroupLoop": "프록시 그룹이 서로를 순환 참조합니다: {path}",
"customIssueInvalidEmptyFallback": "빈 폴백은 프록시여야 하지만 존재하지 않거나 프록시 그룹입니다: {name}",
"customIssueInvalidFilter": "올바른 정규식이 아닙니다({name}): {message}",
"customIssueMissingDialer": "존재하지 않는 다이얼러 프록시: {name}",
"customIssueDialerLoop": "{proxy}이(가) {target}을(를) 거쳐 다시 {proxy}(으)로 돌아오므로 다이얼이 순환합니다",
"dialerProxyLoopStopped": "{name}을(를) 거치는 다이얼이 이를 사용하는 프록시로 되돌아와 연결을 중단했습니다",
"customIssuesSummary": "문제가 있는 항목 {count}개, 이 프로필을 적용하면 실패할 수 있습니다",
"providerInUse": "{label}은(는) 아직 {profiles}에서 사용 중이며, 삭제하면 해당 프로필이 제대로 작동하지 않습니다. 삭제하시겠습니까?",
"proxyGroupInUse": "{label}은(는) 이 프로필의 규칙, 프록시 그룹, 다이얼러 프록시, DNS 또는 NTP에서 아직 참조 중이며, 삭제하면 작동하지 않게 됩니다. 삭제하시겠습니까?",
"proxiesRemovedTip": "다음 프록시가 삭제됩니다: {removed}. 이름이 바뀐 줄은 새 프록시로 간주되며, 삭제된 프록시를 지정한 사용자 지정 프로필에는 누락으로 표시됩니다. 그래도 적용하시겠습니까?",
"proxiesReplacedTip": "다음 프록시가 삭제됩니다: {removed}. 다음 프록시가 추가됩니다: {added}. 이름이 바뀐 줄은 새 프록시로 간주되며, 삭제된 프록시를 지정한 사용자 지정 프로필에는 누락으로 표시됩니다. 그래도 적용하시겠습니까?",
"fontSize": "크기",
"large": "크게",
"extraLarge": "매우 크게",
"switchProfile": "프로필 전환",
"lastUpdated": "마지막 업데이트",
"filters": "필터",
"label": "레이블",
"regex": "정규식",
"matchCase": "대/소문자 구분",
"iconSets": "아이콘 세트",
"invalidIconSet": "올바른 아이콘 세트가 아닙니다",
"recommendedIcons": "추천",
"localImage": "로컬 이미지",
"iconSetsEmptyTip": "아이콘 세트가 아직 없습니다. 사용자 지정 프로필 리소스 → 아이콘 세트에서 추가하세요",
"outbound": "아웃바운드",
"strictRoute": "엄격한 라우팅",
"congestionController": "TCP 혼잡 제어",
"congestionControllerDesc": "TCP 혼잡 제어는 mips 스택에서만 작동합니다.",
"routeExcludeAddress": "라우팅 제외 주소",
"keepAliveIdle": "TCP 킵얼라이브 유휴 시간",
"disableKeepAlive": "TCP 킵얼라이브 사용 안 함",
"routingMark": "라우팅 마크",
"strictRouteDesc": "엄격한 라우팅은 DNS 등의 트래픽이 TUN을 우회하지 못하게 하며, 다른 기기는 이 기기에 접속할 수 없게 됩니다.",
"initFailed": "시작 실패",
"initFailedTip": "FlClash를 시작하는 중 오류가 발생하여 계속할 수 없습니다. 세부 정보를 복사하여 문제를 보고하세요. 문제가 계속되면 데이터를 지우면 대개 앱이 다시 시작됩니다.",
"errorDetails": "오류 세부 정보",
"stackTrace": "스택 트레이스",
"clearDataAndExitTip": "모든 프로필, 설정, 로컬 데이터를 삭제한 후 앱을 종료합니다. 앱을 다시 열면 처음부터 시작합니다.",
"clearDataFailed": "데이터를 지우지 못했습니다: {detail}",
"bypassPrivateRoute": "사설 주소 우회",
"bypassPrivateRouteDesc": "사설 주소 우회를 켜면 LAN, 루프백, 멀티캐스트 주소는 TUN을 거치지 않습니다.",
"disableIcmpForwarding": "ICMP 전달 사용 안 함",
"disableIcmpForwardingDesc": "ICMP 전달을 끄면 TUN이 ping에 직접 응답하므로 ping에 실제 지연 시간이 표시되지 않습니다.",
"excludeInterface": "제외할 인터페이스",
"excludeInterfaceDesc": "docker0와 같이 제외할 인터페이스로 들어오는 트래픽은 TUN을 거치지 않습니다.",
"includeInterface": "포함할 인터페이스",
"includeInterfaceDesc": "포함할 인터페이스로 들어오는 트래픽만 TUN을 거치며, 제외할 인터페이스는 더 이상 적용되지 않습니다.",
"includeUid": "포함할 사용자 ID",
"includeUidDesc": "포함할 사용자 ID의 프로세스 트래픽만 TUN을 거칩니다.",
"excludeUid": "제외할 사용자 ID",
"excludeUidDesc": "제외할 사용자 ID의 프로세스 트래픽은 TUN을 거치지 않습니다.",
"excludeSrcPort": "제외할 소스 포트",
"excludeSrcPortDesc": "제외할 소스 포트에서 나가는 트래픽은 TUN을 거치지 않습니다.",
"excludeDstPort": "제외할 대상 포트",
"excludeDstPortDesc": "제외할 대상 포트로 가는 트래픽은 TUN을 거치지 않습니다.",
"udpTimeout": "UDP 시간 제한",
"udpTimeoutDesc": "UDP 시간 제한은 초 단위이며 gvisor, system, mixed 스택에서만 작동합니다.",
"dnsHijackDesc": "추가하지 않으면 53번 포트로 가는 모든 DNS 쿼리를 하이재킹합니다(any:53).",
"tunOverrideDesc": "여기에 추가한 항목은 모든 프로필의 TUN 설정을 대체합니다. 추가하지 않은 항목은 프로필 자체 설정을 유지합니다. 사용자 지정 프로필이 직접 추가한 항목이 우선합니다.",
"profileTunSettingsDesc": "이 프로필에만 적용되며 네트워크 설정의 TUN 덮어쓰기 항목보다 우선합니다.",
"settingsCount": "{count, plural, other{설정 {count}개}}",
"sectionsInEffect": "{count}개 적용 중",
"itemsCount": "{count, plural, other{항목 {count}개}}",
"additionalPrefix": "이름 접두사",
"additionalSuffix": "이름 접미사",
"skipCertVerify": "인증서 검증 건너뛰기",
"ipVersion": "IP 버전",
"addFilters": "필터 추가",
"failedItem": "{label}: {message}",
"failedCount": "{count}개 항목 실패",
"importConfigInvalid": "가져올 수 있는 구성 파일이 아닙니다",
"importConfigReplaceTip": "가져오면 이 프로필의 현재 프록시 그룹, 규칙, 설정이 대체됩니다",
"networkName": "네트워크 이름",
"networkSecret": "네트워크 시크릿",
"peers": "피어",
"virtualIpv4": "가상 IPv4",
"hostname": "호스트 이름",
"exitNodes": "출구 노드",
"cipher": "암호화 방식",
"protocol": "프로토콜",
"protocolParam": "프로토콜 매개변수",
"obfs": "난독화",
"obfsParam": "난독화 매개변수",
"obfsPassword": "난독화 비밀번호",
"obfsMode": "난독화 모드",
"obfsHost": "난독화 호스트",
"plugin": "플러그인",
"pluginMode": "플러그인 모드",
"pluginHost": "플러그인 호스트",
"privateKey": "개인 키",
"publicKey": "공개 키",
"privateKeyPassphrase": "개인 키 암호",
"preSharedKey": "사전 공유 키",
"secretKey": "키",
"transport": "전송",
"uploadBandwidth": "업로드 대역폭",
"downloadBandwidth": "다운로드 대역폭",
"protocolVersion": "프로토콜 버전",
"portHopping": "포트 호핑",
"hopInterval": "호핑 간격",
"congestionControl": "혼잡 제어",
"udpRelayMode": "UDP 릴레이 모드",
"allowedIps": "허용 IP",
"remoteDnsResolve": "원격 DNS 확인",
"clientFingerprint": "클라이언트 핑거프린트",
"required": "필수",
"ageSecretKey": "age 비밀 키",
"ageEncryptedProfile": "암호화된 프로필",
"ageSecretKeyRequiredTip": "이 프로필은 age로 암호화되어 있어 복호화하려면 age 비밀 키가 필요합니다",
"currentOutboundDesc": "시작 버튼이 있어 제거할 수 없습니다",
"running": "실행 중",
"notRunning": "실행 중 아님",
"trafficPeak": "최고 {value}",
"trafficTotal": "합계 {value}",
"ipQualityShort": "품질",
"ipTypeUnknown": "알 수 없는 유형",
"tapIconToRetry": "아이콘을 눌러 다시 시도",
"networkTakeover": "트래픽 가로채기",
"portValue": "포트 {port}",
"serviceAbnormalCount": "{count, plural, other{문제 {count}건}}",
"serviceAllAvailable": "모두 사용 가능",
"serviceShownOnDashboard": "대시보드에 표시됨",
"groupTimeoutCount": "{count}개 시간 초과",
"groupCountUnit": "개",
"groupCount": "{count, plural, other{그룹 {count}개}}",
"proxyGroupsAfterStart": "코어가 시작되면 그룹이 여기에 표시됩니다",
"usedShort": "사용",
"profileUpdatedAt": "{time} 업데이트",
"profileExpiresIn": "{date} 만료, {days, plural, other{{days}일}} 남음",
"profileDaysLeft": "{days, plural, other{{days}일 남음}}",
"target": "대상",
"outboundExit": "출구",
"duration": "소요 시간",
"traffic": "트래픽",
"totalDownload": "다운로드량",
"totalUpload": "업로드량",
"initiatorShort": "출처",
"result": "결과",
"elapsed": "소요",
"connectionStats": "연결 통계",
"connectionStatsDesc": "실시간 연결, 최근 요청, DNS 쿼리",
"memoryInfoDesc": "앱과 코어가 사용하는 메모리",
"editDashboard": "대시보드 편집",
"done": "완료",
"dashboardAddableSection": "추가 가능",
"customImportDroppedSubRules": "{count, plural, other{사용자 지정 프로필은 하위 규칙을 지원하지 않아 SUB-RULE 규칙 {label}개를 제외했습니다}}",
"autoInclude": "자동 포함",
"allReferencedProxies": "참조된 모든 프록시",
"allReferencedProxyProviders": "참조된 모든 프록시 공급자"
}
+490 -168
View File
@@ -9,19 +9,31 @@
"profiles": "Профили",
"tools": "Инструменты",
"logs": "Логи",
"logsDesc": "Записи захваченных логов",
"resources": "Ресурсы",
"resourcesDesc": "Сведения о внешних ресурсах",
"trafficUsage": "Статистика трафика",
"networkSpeed": "Скорость сети",
"outboundMode": "Режим исходящего трафика",
"networkDetection": "Проверка сети",
"serviceStatus": "Состояние сервисов",
"serviceRestricted": "Доступ ограничен",
"serviceAvailable": "Доступен",
"serviceUnavailable": "Недоступен",
"serviceFailed": "Ошибка проверки",
"serviceDisallowedIsp": "Недопустимый провайдер",
"serviceBlocked": "Заблокировано",
"serviceUnsupportedRegion": "Регион не поддерживается",
"serviceOriginalsOnly": "Только оригиналы",
"serviceComingSoon": "Скоро появится",
"servicePending": "Не проверено",
"serviceManage": "Управление сервисами",
"serviceCheck": "Проверить",
"serviceCheckAll": "Проверить все",
"serviceCheckedAt": "Проверено в {time}",
"upload": "Отдача",
"download": "Загрузка",
"usedTraffic": "Использованный трафик",
"totalTraffic": "Общий трафик",
"expireTime": "Срок действия",
"nullProfileDesc": "Профилей пока нет. Сначала добавьте профиль",
"nullProfileDesc": "Добавьте профиль, чтобы начать",
"settings": "Настройки",
"language": "Язык",
"defaultText": "По умолчанию",
@@ -29,34 +41,31 @@
"other": "Другое",
"about": "О программе",
"en": "Английский",
"es": "Испанский",
"fr": "Французский",
"ja": "Японский",
"ko": "Корейский",
"ru": "Русский",
"zhCN": "Упрощённый китайский",
"theme": "Тема",
"themeDesc": "Тёмный режим и настройка цветов",
"override": "Переопределение",
"extend": "Расширение",
"runtimeConfig": "Конфигурация выполнения",
"allowLan": "Разрешить LAN",
"allowLanDesc": "Разрешить доступ к прокси из локальной сети",
"tun": "TUN",
"tunDesc": "Работает только в режиме администратора",
"tunDesc": "TUN работает только в режиме администратора.",
"minimizeOnExit": "Сворачивать при выходе",
"minimizeOnExitDesc": "Изменяет стандартное поведение при выходе",
"autoLaunch": "Автозапуск",
"autoLaunchDesc": "Запускаться автоматически при старте системы",
"silentLaunch": "Тихий запуск",
"silentLaunchDesc": "Запускаться в фоновом режиме",
"silentLaunchDesc": "Тихий запуск не показывает окно.",
"autoRun": "Автовключение",
"autoRunDesc": "Включаться автоматически при открытии приложения",
"autoRunDesc": "Автовключение запускает прокси при открытии приложения.",
"logcat": "Захват логов",
"logcatDesc": "При отключении раздел логов будет скрыт",
"logcatDesc": "При отключении захвата логов раздел логов скрывается.",
"autoCheckUpdate": "Автопроверка обновлений",
"autoCheckUpdateDesc": "Автоматически проверять обновления при запуске приложения",
"checkCertificate": "Проверять TLS-сертификаты",
"checkCertificateDesc": "Отклонять недоверенные сертификаты. Отключение подвергает подписки и резервные копии атаке «человек посередине»",
"checkCertificateDesc": "Без проверки TLS-сертификатов подписки и резервные копии уязвимы для атаки «человек посередине».",
"accessControl": "Контроль доступа",
"accessControlDesc": "Выбор приложений, использующих прокси",
"application": "Приложение",
"applicationDesc": "Настройки, связанные с приложением",
"accessControlDesc": "Контроль доступа выбирает приложения, использующие прокси.",
"edit": "Редактировать",
"confirm": "Подтвердить",
"update": "Обновить",
@@ -67,15 +76,17 @@
"qrcode": "QR-код",
"qrcodeDesc": "Сканируйте QR-код, чтобы получить профиль",
"url": "URL",
"urlDesc": "Получить профиль по URL",
"link": "Ссылка",
"linkDesc": "Получить профиль по ссылке на подписку или на прокси",
"file": "Файл",
"fileDesc": "Загрузить файл профиля напрямую",
"name": "Название",
"profileNameNullValidationDesc": "Введите название профиля",
"rename": "Переименовать",
"profileUrlNullValidationDesc": "Введите URL профиля",
"profileUrlInvalidValidationDesc": "Введите корректный URL профиля",
"autoUpdate": "Автообновление",
"autoUpdateInterval": "Интервал автообновления (минуты)",
"autoUpdateInterval": "Интервал автообновления",
"profileAutoUpdateIntervalNullValidationDesc": "Введите интервал автообновления",
"profileAutoUpdateIntervalInvalidValidationDesc": "Введите корректный интервал",
"themeMode": "Режим темы",
@@ -84,7 +95,7 @@
"auto": "Авто",
"light": "Светлая",
"dark": "Тёмная",
"importFromURL": "Импорт из URL",
"importFromLink": "Импорт по ссылке",
"submit": "Отправить",
"createProfileFromUrlTip": "Создать профиль по ссылке {url}?",
"create": "Создать",
@@ -103,12 +114,15 @@
"show": "Показать",
"exit": "Выход",
"systemProxy": "Системный прокси",
"safeMode": "Безопасный режим",
"safeModeAppTitle": "{appName} (Безопасный режим)",
"project": "Проект",
"core": "Ядро",
"tabAnimation": "Анимация вкладок",
"desc": "Многоплатформенный прокси-клиент на основе ClashMeta: простой и удобный, с открытым исходным кодом и без рекламы.",
"startVpn": "Запуск VPN...",
"stopVpn": "Остановка VPN...",
"startVpn": "Запуск VPN…",
"stopVpn": "Остановка VPN…",
"localNetworkDeniedTip": "Доступ к локальной сети запрещён: используется стек gvisor, локальная сеть недоступна.",
"compatible": "Режим совместимости",
"notSelectedTip": "Текущую группу прокси нельзя выбрать",
"changeProxyFailedTip": "Не удалось переключить прокси; восстановлен предыдущий выбор",
@@ -127,8 +141,8 @@
"addressTip": "Введите корректный адрес WebDAV",
"password": "Пароль",
"authentication": "Аутентификация",
"authenticationDesc": "Требовать учётные данные для локального порта прокси, чтобы другие приложения не могли использовать его",
"authenticationSystemProxyDesc": "Не применяется, пока включена аутентификация",
"authenticationDesc": "Аутентификация требует логин и пароль для локального порта прокси.",
"authenticationSystemProxyDesc": "Пока включена аутентификация, системный прокси не применяется.",
"checkUpdate": "Проверить обновления",
"discoverNewVersion": "Доступна новая версия",
"checkUpdateError": "У вас уже последняя версия",
@@ -137,40 +151,37 @@
"country": "Регион",
"search": "Поиск",
"allowBypass": "Разрешить приложениям обходить VPN",
"allowBypassDesc": "При включении некоторые приложения смогут обходить VPN",
"externalController": "Внешний контроллер",
"externalControllerDesc": "При включении ядром Clash можно управлять через порт 9090",
"ipv6Desc": "При включении можно принимать трафик IPv6",
"externalControllerDesc": "Внешний контроллер позволяет управлять ядром через порт 9090.",
"externalControllerSecret": "Секрет внешнего контроллера",
"ipv6Desc": "Трафик IPv6 принимается, только когда IPv6 включён.",
"app": "Приложение",
"general": "Общие",
"systemProxyDesc": "Настроить системный прокси",
"startupAndBackground": "Запуск и фоновая работа",
"requestsAndUpdates": "Запросы и обновления",
"logsAndDiagnostics": "Логи и диагностика",
"userAgent": "User-Agent",
"unifiedDelay": "Единая задержка",
"unifiedDelayDesc": "Убирает лишние задержки, например рукопожатие",
"tcpConcurrent": "Параллельный TCP",
"tcpConcurrentDesc": "При включении разрешает параллельные TCP-подключения",
"geodataLoader": "Geo: экономия памяти",
"geodataLoaderDesc": "При включении используется Geo-загрузчик с низким потреблением памяти",
"requests": "Запросы",
"requestsDesc": "Просмотр последних запросов",
"recentRequests": "Последние запросы",
"findProcessMode": "Поиск процесса",
"init": "Инициализация",
"infiniteTime": "Бессрочно",
"connections": "Соединения",
"liveConnections": "Активные соединения",
"inbound": "Входящие",
"connectionsDesc": "Просмотр данных о текущих соединениях",
"intranetIP": "Внутренний IP",
"view": "Просмотр",
"cut": "Вырезать",
"copy": "Копировать",
"clipboardWriteFailed": "Не удалось скопировать в буфер обмена. Возможно, выделение слишком велико",
"paste": "Вставить",
"testUrl": "URL для теста",
"sync": "Синхронизация",
"exclude": "Скрыть из недавних задач",
"excludeDesc": "Скрывать приложение из недавних задач, когда оно в фоне",
"expand": "Стандартный",
"shrink": "Компактный",
"min": "Минимальный",
"compact": "Компактный",
"tab": "Вкладки",
"list": "Список",
"delay": "Задержка",
@@ -182,19 +193,19 @@
"go": "Перейти",
"externalLink": "Внешняя ссылка",
"otherContributors": "Другие участники",
"appIconDesign": "Дизайн значка приложения",
"autoCloseConnections": "Автозакрытие соединений",
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
"onlyStatisticsProxy": "Учитывать только прокси",
"onlyStatisticsProxyDesc": "При включении учитывается только трафик через прокси",
"autoCloseConnectionsDesc": "Автозакрытие соединений обрывает открытые соединения при смене прокси.",
"onlyStatisticsProxy": "Учитывать только трафик прокси",
"showNotificationStopAction": "Кнопка остановки в уведомлении",
"showNotificationStopActionDesc": "Показывать кнопку остановки в постоянном уведомлении. Отключите, если из-за неё система всегда разворачивает уведомление",
"pureBlackMode": "Чисто чёрный режим",
"sidebarBlur": "Размытие боковой панели",
"sidebarBlurDesc": "Показывать сквозь боковую панель размытый рабочий стол за окном",
"liquidGlass": "Жидкое стекло",
"keepAliveIntervalDesc": "Интервал TCP keep-alive",
"entries": " записей",
"local": "Локально",
"remote": "Удалённо",
"remoteBackupDesc": "Резервное копирование данных в WebDAV",
"localBackupDesc": "Резервное копирование данных локально",
"mode": "Режим",
"time": "Время",
"source": "Источник",
@@ -205,94 +216,174 @@
"layout": "Макет",
"tight": "Плотный",
"standard": "Стандартный",
"ultra": "Максимум",
"loose": "Свободный",
"profilesSort": "Сортировка профилей",
"start": "Старт",
"runTime": "Время работы",
"stop": "Стоп",
"dnsDesc": "Настройки, связанные с DNS",
"key": "Ключ",
"value": "Значение",
"hostsDesc": "Добавить записи hosts",
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
"vpnEnableDesc": "VPN автоматически направляет весь системный трафик через VpnService.",
"options": "Опции",
"loopback": "Инструмент разблокировки loopback",
"loopbackDesc": "Для снятия ограничения loopback у UWP-приложений",
"enabled": "Включено",
"disabled": "Выключено",
"loopback": "Снятие ограничения loopback для UWP",
"providers": "Внешние ресурсы",
"proxyProviders": "Провайдеры прокси",
"ruleProviders": "Провайдеры правил",
"providerUrlTip": "Поддерживаются только удалённые ресурсы",
"ruleSetEmptyTip": "В наборе правил нет пригодных записей",
"ruleSetMixedTip": "Набор правил смешивает домены и диапазоны IP, поэтому его тип не определить",
"ruleSetInvalidTip": "Не удаётся прочитать этот файл mrs",
"subscriptionInfo": "Информация о подписке",
"overrideDns": "Переопределить DNS",
"overrideDnsDesc": "При включении настройки DNS профиля переопределяются",
"ntpStatusDesc": "NTP берёт время с NTP-сервера, а не из системных часов.",
"server": "Сервер",
"ntpInterval": "Интервал синхронизации (минуты)",
"dialerProxy": "Прокси для подключения",
"dialerProxyDesc": "Прокси для подключения — исход, через который идёт обращение к NTP-серверу.",
"writeToSystem": "Записывать в систему",
"writeToSystemDesc": "«Записывать в систему» также устанавливает системные часы; Android это игнорирует.",
"sniffer": "Сниффер",
"snifferStatusDesc": "Сниффер определяет домен по трафику TLS, HTTP и QUIC, чтобы правила могли его сопоставить.",
"overrideDestination": "Подменять адрес назначения",
"overrideDestinationDesc": "«Подменять адрес назначения» подключается к обнаруженному домену вместо исходного адреса.",
"forceDnsMapping": "Принудительно для DNS-сопоставлений",
"forceDnsMappingDesc": "«Принудительно для DNS-сопоставлений» анализирует и соединения, домен которых получен из DNS-сопоставления.",
"parsePureIp": "Разбирать чистые IP",
"parsePureIpDesc": "«Разбирать чистые IP» анализирует соединения, у которых есть только IP-адрес.",
"forceDomain": "Принудительные домены",
"forceDomainDesc": "Принудительные домены тоже анализируются; остальные соединения с доменом не затрагиваются.",
"skipDomain": "Пропускаемые домены",
"skipDomainDesc": "Обнаруженный домен из пропускаемых доменов не используется.",
"skipSrcAddress": "Пропускаемые адреса источника",
"skipDstAddress": "Пропускаемые адреса назначения",
"sniffProtocols": "Протоколы",
"ports": "Порты",
"overrideEntries": "Переопределяемые параметры",
"addOverrideEntry": "Добавить параметр",
"dnsOverrideDesc": "Добавленные здесь параметры заменяют DNS всех профилей из URL и файлов; остальные остаются такими, как в профиле. DNS пользовательского профиля задаётся в его содержимом.",
"settingEntries": "параметры",
"addSettingEntry": "Добавить параметр",
"profileSettingsDesc": "Действует только для этого профиля; не добавленные параметры используют значения по умолчанию.",
"healthCheck": "Проверка состояния",
"discardChanges": "Отменить изменения?",
"quickEdit": "Быстрое редактирование",
"status": "Статус",
"statusDesc": "При отключении используется системный DNS",
"preferH3Desc": "Предпочитать HTTP/3 для DoH",
"statusDesc": "При отключённом DNS используется системный DNS.",
"respectRules": "Соблюдать правила",
"respectRulesDesc": "DNS-соединения следуют правилам; требуется настроить proxy-server-nameserver",
"respectRulesDesc": "«Соблюдать правила» направляет DNS-соединения по правилам; нужен Proxy Server Nameserver.",
"dnsMode": "Режим DNS",
"fakeipRange": "Диапазон Fake-IP",
"fakeipFilter": "Фильтр Fake-IP",
"defaultNameserver": "DNS-сервер по умолчанию",
"defaultNameserverDesc": "Используется для разрешения адресов DNS-серверов",
"nameserver": "DNS-сервер",
"nameserverDesc": "Используется для разрешения доменов",
"useHosts": "Использовать hosts",
"useSystemHosts": "Использовать системный hosts",
"nameserverPolicy": "Политика DNS-серверов",
"nameserverPolicyDesc": "Задать политику DNS-серверов для доменов",
"proxyNameserver": "DNS-сервер для прокси",
"proxyNameserverDesc": "Используется для разрешения доменов прокси-узлов",
"fallback": "Fallback",
"fallbackDesc": "Обычно зарубежный DNS",
"fallbackFilter": "Фильтр fallback",
"geoipCode": "Код GeoIP",
"ipcidr": "IP/CIDR",
"domain": "Домен",
"listenRoutingMark": "Метка маршрутизации",
"listenRoutingMarkDesc": "Метка маршрутизации действует только в Linux.",
"ipv6Timeout": "Тайм-аут IPv6 (мс)",
"cacheAlgorithm": "Алгоритм кэша",
"cacheMaxSize": "Размер кэша",
"fakeipRange6": "Диапазон Fake-IP (IPv6)",
"fakeipFilterMode": "Режим фильтра Fake-IP",
"fakeipFilterModeDesc": "В режиме фильтра Fake-IP blacklist исключает совпадения, whitelist применяется только к ним, rule — по правилам.",
"fakeipTtl": "TTL Fake-IP",
"reset": "Сброс",
"actionView": "Показать/Скрыть",
"actionStart": "Старт/Стоп",
"actionMode": "Переключить режим",
"actionProxy": "Системный прокси",
"actionTun": "TUN",
"actionRuleMode": "Режим правил",
"actionGlobalMode": "Глобальный режим",
"actionDirectMode": "Прямой режим",
"actionDelayTest": "Проверить все задержки",
"actionUpdateProfiles": "Обновить профили",
"disclaimer": "Отказ от ответственности",
"disclaimerDesc": "Это программное обеспечение предназначено только для некоммерческого использования: обучения, обмена опытом и научных исследований. Коммерческое использование строго запрещено; любая коммерческая деятельность не имеет отношения к этому программному обеспечению.",
"disclaimerDesc": "Перед использованием FlClash (далее — «Программа») внимательно прочитайте это заявление и убедитесь, что понимаете его полностью. Нажимая «Согласен», вы подтверждаете, что прочитали, поняли и принимаете все приведённые ниже условия. Если вы не согласны, нажмите «Выход» и прекратите использование Программы.",
"disclaimerSoftwareTitle": "Характер программы",
"disclaimerSoftwareContent": "Программа — это клиент сетевого прокси с открытым исходным кодом на основе ядра Clash.Meta (mihomo). Она предоставляет только локальные инструменты: управление конфигурациями, маршрутизацию по правилам и пересылку трафика.",
"disclaimerNoServiceStatement": "Сама Программа не предоставляет прокси-серверы, узлы, подписки или услуги доступа к сети и не состоит в партнёрских, агентских или гарантийных отношениях с поставщиками таких услуг.",
"disclaimerUsageTitle": "Ограничения использования",
"disclaimerUsageContent": "Программа предназначена только для некоммерческого использования: обучения, обмена опытом и технических исследований. Любое коммерческое использование строго запрещено, включая, помимо прочего, платное распространение, продажу в комплекте, использование в составе коммерческого сервиса или ведение деятельности от имени Программы. Любая коммерческая деятельность не имеет отношения к Программе и её разработчикам.\n\nСтрого запрещено использовать Программу для действий, нарушающих законы вашей страны или региона, включая, помимо прочего, обход законно установленных ограничений доступа к сети, распространение незаконной информации, сетевые атаки и нарушение законных прав других лиц.",
"disclaimerResponsibilityTitle": "Ваша ответственность",
"disclaimerResponsibilityContent": "Вы самостоятельно убеждаетесь, что использование Программы законно в вашей стране или регионе, и единолично несёте юридическую ответственность за все действия с ней и их последствия.\n\nПодписки, узлы и конфигурации, которые вы импортируете, выбираете вы сами. Законность их источника, безопасность содержимого и надёжность сервиса — вопрос между вами и их поставщиками.",
"disclaimerThirdPartyTitle": "Сторонний контент",
"disclaimerThirdPartyContent": "Ссылки на подписки, файлы конфигурации, наборы правил, скрипты, внешние ресурсы и внешние ссылки предоставляются третьими лицами. Разработчики не могут проверять и не проверяют их законность, точность, безопасность и доступность и не дают на них никаких гарантий.\n\nУтечка данных, финансовые потери, блокировка аккаунта или иной ущерб, вызванные сторонним контентом, урегулируются между вами и третьим лицом; разработчики не несут за это никакой ответственности.",
"disclaimerWarrantyTitle": "Отсутствие гарантий",
"disclaimerWarrantyContent": "Программа предоставляется «как есть» и «по мере доступности», без каких-либо явных или подразумеваемых гарантий, включая, помимо прочего, гарантии товарной пригодности, пригодности для определённой цели, ненарушения прав, бесперебойной работы, отсутствия ошибок и уязвимостей.\n\nРазработчики не гарантируют, что Программа будет соответствовать вашим потребностям или работать без сбоев и ошибок.",
"disclaimerLiabilityTitle": "Ограничение ответственности",
"disclaimerLiabilityContent": "В максимальной степени, допустимой применимым законодательством, ни разработчики, ни участники проекта не несут ответственности за любой прямой, косвенный, случайный, особый, штрафной или последующий ущерб, возникший в результате использования или невозможности использования Программы, включая, помимо прочего, потерю данных, повреждение устройства, сбои сети, прерывание деятельности, упущенную выгоду или связанные с этим правовые споры, даже если они были предупреждены о возможности такого ущерба.",
"disclaimerPrivacyTitle": "Сбор данных и конфиденциальность",
"disclaimerPrivacyContent": "Программа не собирает и не отправляет адреса ваших подписок, сведения об узлах, содержимое конфигураций, посещённые сайты, записи о подключениях, содержимое трафика и журналы. Эти данные хранятся только на вашем устройстве, и у разработчиков нет к ним доступа.\n\nПрограмма обращается к сети только при использовании соответствующих функций, например загружает указанный вами адрес подписки при обновлении профиля или обращается к GitHub при проверке обновлений.\n\nПрограмма не содержит никаких сервисов статистики или отчётов о сбоях ни на одной платформе.",
"disclaimerLicenseTitle": "Лицензия с открытым исходным кодом",
"disclaimerLicenseContent": "Программа распространяется с открытым исходным кодом по лицензии GPL-3.0. Вы можете свободно использовать, изменять и распространять её при соблюдении этой лицензии: производные работы также должны распространяться по GPL-3.0 с сохранением уведомлений об авторских правах.\n\nСторонние компоненты Программы, включая ядро Clash.Meta, распространяются по своим собственным лицензиям. Авторы оригинала не несут ответственности за проблемы, возникшие в изменённых или распространяемых третьими лицами версиях.",
"disclaimerChangesTitle": "Изменения заявления",
"disclaimerChangesContent": "Разработчики могут изменять это заявление в любом выпуске; изменения вступают в силу с момента публикации выпуска. Продолжая пользоваться Программой после обновления, вы принимаете изменённое заявление.",
"disclaimerAcceptTitle": "Принятие условий",
"disclaimerAcceptContent": "Устанавливая, копируя или используя Программу, вы подтверждаете, что прочитали и приняли это заявление полностью. Если вы не согласны с каким-либо его условием, немедленно прекратите использование и удалите Программу.",
"disclaimerRestateTitle": "Повторите заявление",
"disclaimerRestateTip": "Чтобы подтвердить, что вы ознакомились с ним, полностью введите следующее заявление в поле ниже:",
"disclaimerRestateHint": "Введите заявление выше",
"disclaimerRestateMismatch": "Не совпадает с текстом заявления",
"disclaimerAcknowledgeTitle": "Подтвердите заявление",
"disclaimerAcknowledgeTip": "Чтобы подтвердить, что вы ознакомились с ним, прочитайте следующее заявление и отметьте пункт ниже:",
"disclaimerAcknowledgeLabel": "Я прочитал(а) и понимаю это заявление",
"agree": "Согласен",
"hotkeyManagement": "Горячие клавиши",
"hotkeyManagementDesc": "Управление приложением с клавиатуры",
"pressKeyboard": "Нажмите клавишу",
"inputCorrectHotkey": "Введите корректную горячую клавишу",
"hotkeyConflict": "Конфликт горячих клавиш",
"hotkeyDesc": "Глобальные горячие клавиши работают, даже когда окно скрыто. Нажмите на действие, чтобы записать сочетание клавиш.",
"hotkeyNotSet": "Не задано",
"pressKeyboard": "Нажмите сочетание клавиш",
"hotkeyNeedsModifier": "Добавьте хотя бы одну из клавиш: {modifiers}",
"hotkeyConflictWith": "Уже используется для «{action}». При сохранении будет перенесено сюда.",
"hotkeyUnavailable": "Не зарегистрировано: сочетание может быть занято другим приложением",
"remove": "Убрать",
"noHotKey": "Горячих клавиш пока нет",
"noNetwork": "Нет сети",
"ipv6InboundDesc": "Разрешить входящий IPv6",
"ipv6InboundDesc": "При включённом IPv6 VPN принимает входящий IPv6.",
"exportLogs": "Экспорт логов",
"exportSuccess": "Экспорт выполнен",
"iconStyle": "Стиль значков",
"onlyIcon": "Только значок",
"iconStyleFilled": "С подложкой",
"iconStylePlain": "Без подложки",
"iconStyleHidden": "Скрыто",
"filter": "Фильтр",
"hideTimeoutProxies": "Скрывать прокси с таймаутом",
"hideTimeoutProxiesDesc": "Не показывать прокси, у которых последний тест задержки завершился таймаутом",
"stackMode": "Режим стека",
"network": "Сеть",
"networkDesc": "Настройки, связанные с сетью",
"bypassDomain": "Исключённые домены",
"bypassDomainDesc": "Действует только при включённом системном прокси",
"bypassDomainDesc": "Исключённые домены действуют только при включённом системном прокси.",
"resetTip": "Вы уверены, что хотите выполнить сброс?",
"icon": "Значок",
"noData": "Нет данных",
"fontFamily": "Шрифт",
"installedFonts": "Установленные шрифты",
"toggle": "Переключить",
"system": "Система",
"routeMode": "Режим маршрутизации",
"routeModeBypassPrivate": "Обходить частные адреса",
"routeModeConfig": "Использовать конфигурацию",
"routeAddress": "Адреса маршрутов",
"routeAddressDesc": "Настроить прослушиваемые адреса маршрутов",
"interfaceNameMode": "Исходящий интерфейс",
"interfaceNameModeClear": "Очистить",
"interfaceNameModeFollow": "Как в конфигурации",
"interfaceNameModeCustom": "Вручную",
"interfaceName": "Имя интерфейса",
"interfaceNameDesc": "Сетевой интерфейс для исходящих соединений",
"interfaceNameDesc": "Имя интерфейса задаёт сетевой интерфейс для исходящих соединений.",
"copyEnvVar": "Копировать переменные окружения",
"memoryInfo": "Память",
"total": "Всего",
"releaseMemory": "Освободить память",
"memoryCoreHeapInuse": "Используемая куча",
"memoryCoreHeapIdle": "Свободная куча",
"memoryCoreStack": "Стеки горутин",
"memoryCoreRuntime": "Накладные расходы среды",
"memoryCoreNotRunning": "Ядро не запущено",
"memoryAppResident": "Резидентная память",
"memoryAppShared": "Приложение и общая",
"memoryEstimateDesc": "Оценка по резидентной памяти процессов; может отличаться от данных системы.",
"memoryEstimateSharedDesc": "Ядро работает в процессе приложения. Его доля оценивается по статистике среды выполнения, остальное относится к приложению и общей памяти.",
"memoryReleased": "Память освобождена",
"memoryReleasedSize": "Освобождено {size}",
"releaseMemoryFailed": "Не удалось освободить память",
"cancel": "Отмена",
"fileIsUpdate": "Файл изменён. Сохранить изменения?",
"profileHasUpdate": "Профиль изменён. Отключить автообновление?",
@@ -305,38 +396,38 @@
"listen": "Прослушивание",
"undo": "Отменить",
"redo": "Повторить",
"lineWrap": "Перенос строк",
"none": "Нет",
"basicConfig": "Базовая конфигурация",
"basicConfigDesc": "Глобальное изменение базовой конфигурации",
"advancedConfig": "Расширенная конфигурация",
"advancedConfigDesc": "Разнообразные параметры конфигурации",
"selectedCountTitle": "Выбрано: {count}",
"advancedConfigDesc": "Расширенная конфигурация включает сеть, DNS, добавленные правила и скрипты.",
"addRule": "Добавить правило",
"ruleName": "Название правила",
"content": "Содержимое",
"subRule": "Подправило",
"ruleTarget": "Цель правила",
"matchTargetTitle": "Цель MATCH",
"matchTarget": "MATCH-TARGET",
"followProfile": "Как в профиле",
"matchTargetDesc": "Куда направляются правила с целью MATCH-TARGET. По умолчанию — цель последнего правила MATCH этого профиля.",
"selectMatchTarget": "Выбрать MATCH-TARGET",
"sourceIp": "IP источника",
"noResolve": "Не разрешать IP",
"saveChanges": "Сохранить изменения?",
"findProcessModeDesc": "При включении возможна небольшая потеря производительности",
"tabAnimationDesc": "Действует только в мобильном виде",
"colorSchemes": "Цветовые схемы",
"palette": "Палитра",
"tonalSpotScheme": "Тональный акцент",
"fidelityScheme": "Точная передача",
"cannotSave": "Не удалось сохранить",
"navigationBarStyle": "Нижняя панель",
"floating": "Плавающая",
"docked": "Закреплённая",
"slide": "Сдвиг",
"fade": "Растворение",
"pureBlack": "Чисто чёрный",
"customColor": "Свой цвет",
"colorStyle": "Стиль палитры",
"grid": "Сетка",
"spectrum": "Спектр",
"sliders": "Ползунки",
"hue": "Оттенок",
"saturation": "Насыщенность",
"brightness": "Яркость",
"hex": "Hex",
"monochromeScheme": "Монохром",
"neutralScheme": "Нейтральная",
"vibrantScheme": "Яркая",
"expressiveScheme": "Экспрессивная",
"contentScheme": "Контентная",
"rainbowScheme": "Радуга",
"fruitSaladScheme": "Фруктовый микс",
"vividStyle": "Яркий",
"tintedStyle": "Тонированный",
"softStyle": "Мягкий",
"developerMode": "Режим разработчика",
"developerModeEnableTip": "Режим разработчика включён.",
"messageTest": "Тест сообщения",
@@ -348,6 +439,8 @@
"launchInterruptedTip": "В прошлый раз приложение неожиданно завершилось во время запуска. Автоматическая настройка для этого запуска пропущена; вы можете запустить её вручную.",
"clearData": "Очистить данные",
"textScale": "Масштаб текста",
"textScalePreview": "Так будет выглядеть текст в приложении",
"followSystem": "Как в системе",
"internet": "Интернет",
"systemApp": "Системные приложения",
"noNetworkApp": "Приложения без сети",
@@ -357,11 +450,23 @@
"logsTest": "Тест логов",
"emptyTip": "Поле «{label}» не может быть пустым",
"urlTip": "Значение «{label}» должно быть URL",
"invalidLinkTip": "Это не ссылка на подписку и не ссылка на прокси",
"numberTip": "Значение «{label}» должно быть числом",
"numberRangeTip": "Значение «{label}» должно быть от {min} до {max}",
"interval": "Интервал",
"existsTip": "«{label}» уже существует",
"maxLengthTip": "«{label}» — не более {max} символов",
"multipleValuesTip": "Разделяйте несколько значений запятыми",
"singleAdd": "По одному",
"batchAdd": "Массовое добавление",
"batchListInputTip": "По одному значению на строку или через запятую",
"batchMapInputTip": "По одной записи на строку: ключ, пробел, значение",
"singleImport": "По одному",
"batchImport": "Массовый импорт",
"batchUrlInputTip": "По одному URL на строку",
"batchLinkInputTip": "По одной ссылке на строку",
"batchPreviewTip": "Будет добавлено: {count}, пропущено (уже есть): {skipped}",
"lineIssueTip": "Строка {line}: {message}",
"singleValueTip": "«{label}» — только одно значение",
"deleteTip": "Вы уверены, что хотите удалить «{label}»?",
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные элементы ({label})?",
"nullTip": "Пока нет: {label}",
@@ -378,6 +483,7 @@
"import": "Импорт",
"importFile": "Импорт из файла",
"importUrl": "Импорт по URL",
"startFromScratch": "С нуля",
"autoSetSystemDns": "Автонастройка системного DNS",
"details": "Сведения: {label}",
"creationTime": "Время создания",
@@ -396,36 +502,44 @@
"request": "Запрос",
"connected": "Подключено",
"disconnected": "Отключено",
"connecting": "Подключение...",
"connecting": "Подключение…",
"restartCoreTip": "Вы уверены, что хотите перезапустить ядро?",
"forceRestartCoreTip": "Вы уверены, что хотите принудительно перезапустить ядро?",
"dnsHijacking": "Перехват DNS",
"coreStatus": "Статус ядра",
"dataCollectionTip": "Уведомление о сборе данных",
"dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях, чтобы повысить стабильность.\nСобираемые данные включают сведения об устройстве и подробности сбоя и не содержат личных конфиденциальных данных.\nЭту функцию можно отключить в настройках.",
"crashlytics": "Аналитика сбоев",
"crashlyticsTip": "При включении в случае сбоя приложения автоматически загружаются логи сбоя без конфиденциальной информации",
"appendSystemDns": "Добавлять системный DNS",
"appendSystemDnsTip": "Принудительно добавлять системный DNS в конфигурацию",
"editRule": "Редактировать правило",
"overrideMode": "Режим переопределения",
"standardModeDesc": "Стандартный режим: переопределяет базовую конфигурацию и позволяет просто добавлять правила",
"scriptModeDesc": "Режим скрипта: использует внешние скрипты-расширения для переопределения конфигурации в один клик",
"extendMode": "Режим расширения",
"standardModeDesc": "Стандартный режим: добавляет правила поверх профиля",
"scriptModeDesc": "Режим скрипта: переписывает весь профиль скриптом расширения",
"addedRules": "Добавленные правила",
"controlGlobalAddedRules": "Управление глобальными добавленными правилами",
"overrideScript": "Скрипт переопределения",
"quickActions": "Быстрые действия",
"rulePresetBlockQuic": "Блокировать QUIC",
"rulePresetBlockStun": "Блокировать STUN",
"rulePresetBlockDot": "Блокировать DNS over TLS",
"rulePresetBlockLoopback": "Защита от петли прокси",
"rulePresetSystemServicesDirect": "Apple и Microsoft напрямую",
"rulePresetBittorrentDirect": "BitTorrent напрямую",
"rulePresetLanDirect": "Локальная сеть напрямую",
"extendScript": "Скрипт расширения",
"goToConfigureScript": "Перейти к настройке скрипта",
"goToConfigureAddedRules": "Перейти к настройке добавленных правил",
"editGlobalRules": "Редактировать глобальные правила",
"externalFetch": "Внешнее получение",
"confirmForceCrashCore": "Вы уверены, что хотите принудительно завершить ядро со сбоем?",
"confirmClearAllData": "Вы уверены, что хотите удалить все данные?",
"loading": "Загрузка...",
"loading": "Загрузка…",
"yearsAgo": "{count, plural, one{{count} год назад} few{{count} года назад} many{{count} лет назад} other{{count} года назад}}",
"monthsAgo": "{count, plural, one{{count} месяц назад} few{{count} месяца назад} many{{count} месяцев назад} other{{count} месяца назад}}",
"daysAgo": "{count, plural, one{{count} день назад} few{{count} дня назад} many{{count} дней назад} other{{count} дня назад}}",
"hoursAgo": "{count, plural, one{{count} час назад} few{{count} часа назад} many{{count} часов назад} other{{count} часа назад}}",
"minutesAgo": "{count, plural, one{{count} минуту назад} few{{count} минуты назад} many{{count} минут назад} other{{count} минуты назад}}",
"justNow": "Только что",
"yearsAgoShort": "{count, plural, one{{count} г. назад} few{{count} г. назад} many{{count} л. назад} other{{count} г. назад}}",
"monthsAgoShort": "{count} мес. назад",
"daysAgoShort": "{count} дн. назад",
"hoursAgoShort": "{count} ч назад",
"minutesAgoShort": "{count} мин назад",
"noLongerRemind": "Больше не напоминать",
"accessControlSettings": "Настройки контроля доступа",
"turnOn": "Включить",
@@ -434,39 +548,45 @@
"restart": "Перезапустить",
"speedStatistics": "Статистика скорости",
"resetPageChangesTip": "На этой странице есть изменения. Вы уверены, что хотите выполнить сброс?",
"overwriteTypeCustom": "Пользовательский",
"overwriteTypeCustomDesc": "Пользовательский режим: полная настройка групп прокси и правил",
"customProfile": "Пользовательский",
"customProfileDesc": "Группы прокси и правила из прокси приложения и других профилей",
"customProfileResources": "Ресурсы пользовательских профилей",
"customProfileResourcesDesc": "Ресурсы пользовательских профилей включают прокси, провайдеры правил, фильтры и наборы значков.",
"unknownNetworkError": "Неизвестная сетевая ошибка",
"restoreException": "Ошибка восстановления",
"networkException": "Ошибка сети. Проверьте подключение и повторите попытку",
"backupFromNewerVersion": "Резервная копия создана более новой версией приложения. Обновите приложение перед восстановлением",
"networkTimeoutError": "Время ожидания запроса истекло. Проверьте сеть или прокси и повторите попытку",
"networkHostLookupError": "Не удалось определить адрес сервера. Проверьте правильность URL и работу DNS",
"networkConnectionError": "Не удалось подключиться к серверу. Проверьте подключение к сети или настройки прокси",
"networkTlsError": "Не удалось установить защищённое соединение. Сертификат сервера может быть недействителен, или соединение перехватывается",
"networkCancelledError": "Запрос отменён",
"networkAccessDeniedError": "Сервер запретил доступ (HTTP {code}). Возможно, ссылка устарела или учётные данные неверны",
"networkNotFoundError": "По этому адресу ничего не найдено (HTTP {code}). Проверьте правильность URL",
"networkRateLimitedError": "Слишком много запросов (HTTP 429). Подождите немного и повторите попытку",
"networkServerError": "На сервере произошла ошибка (HTTP {code}). Повторите попытку позже",
"networkBadResponseError": "Сервер отклонил запрос (HTTP {code})",
"networkRequestFailed": "Сетевой запрос не выполнен: {detail}",
"invalidBackupFile": "Недопустимый файл резервной копии",
"pruneCache": "Очистить кэш",
"backupAndRestore": "Резервное копирование и восстановление",
"backupAndRestoreDesc": "Синхронизация данных через WebDAV или файлы",
"restore": "Восстановить",
"restoreSuccess": "Восстановление выполнено",
"restoreFromWebDAVDesc": "Восстановить данные из WebDAV",
"restoreFromFileDesc": "Восстановить данные из файла",
"restoreOnlyConfig": "Восстановить только профили",
"restoreAllData": "Восстановить все данные",
"addProfile": "Добавить профиль",
"delayTest": "Тест задержки",
"proxyGroupEmpty": "Группа прокси пуста",
"proxyGroupNameEmpty": "Название группы прокси не может быть пустым",
"proxyGroupNameDuplicate": "Название группы прокси уже используется",
"confirmExitWindow": "Вы уверены, что хотите закрыть текущее окно?",
"dataChangedSave": "Обнаружены изменения данных. Сохранить их?",
"selectProxyProviders": "Выбрать провайдеров прокси",
"proxyFilter": "Фильтр узлов",
"optional": "Необязательно",
"maxFailedTimes": "Макс. число неудач",
"testInterval": "Интервал тестирования",
"excludeProxyFilter": "Фильтр исключения узлов",
"excludeType": "Исключаемые типы",
"excludeFilter": "Фильтр исключения",
"expectedStatus": "Ожидаемый статус",
"selectProxies": "Выбрать прокси",
"strategy": "Стратегия",
"tolerance": "Допуск",
"inputProxyGroupName": "Введите название группы прокси",
"helperCorruptTip": "Служба Helper недоступна, поэтому TUN-режим включить нельзя. Переустановите FlClash.",
"tunAuthorizationFailed": "Не удалось получить права для TUN, поэтому он выключен.",
"sudoPasswordTitle": "Введите пароль для sudo",
"coreBlockedByPolicyTip": "Windows отказалась запускать FlClashCore.exe (ошибка {code}). Политики контроля приложений, такие как Smart App Control или AppLocker, блокируют неподписанные программы; разрешите FlClash в этой политике или отключите её и повторите попытку.",
"coreBlockedBySmartAppControlTip": "Smart App Control в Windows заблокировал неподписанный FlClashCore.exe. Откройте Безопасность Windows → Управление приложениями и браузером → Параметры Smart App Control, выберите «Выкл.» и снова запустите FlClash. Повторно включить Smart App Control без переустановки Windows нельзя.",
"hideFromList": "Скрыть из списка",
@@ -479,7 +599,6 @@
"selectRuleSet": "Выберите набор правил",
"splitStrategy": "Стратегия распределения",
"selectSplitStrategy": "Выберите стратегию распределения",
"selectSubRule": "Выберите подправило",
"noResolveHostname": "Не разрешать имя хоста",
"matchSourceIp": "Сопоставлять IP источника",
"basicInfo": "Основная информация",
@@ -487,21 +606,11 @@
"proxyType": "Тип прокси",
"basicStrategy": "Базовые политики",
"editProxy": "Редактировать прокси",
"includeAllProxyProviders": "Включить всех провайдеров прокси",
"includeAllProxyProvidersTip": "При включении переопределяет подключённых провайдеров прокси",
"addProxyProviders": "Добавить провайдеров прокси",
"includeAllProxies": "Включить все прокси",
"includeAllProxiesTip": "Подключает все прокси вне групп; ниже можно добавить дополнительные группы прокси",
"proxiesEmpty": "Список прокси пуст",
"addProxies": "Добавить прокси",
"addNodes": "Добавить узлы",
"addProxyGroup": "Добавить группу прокси",
"editProxyGroup": "Редактировать группу прокси",
"confirmOverwriteTip": "После подтверждения существующие данные будут перезаписаны",
"configDataDetected": "В конфигурации обнаружены данные",
"quickFill": "Быстрое заполнение",
"iconUrl": "URL значка",
"iconRecords": "История значков",
"noRecords": "Записей пока нет",
"custom": "Вручную",
"ruleActionDomainDesc": "Сопоставить полный домен",
"ruleActionDomainSuffixDesc": "Сопоставить суффикс домена",
@@ -540,53 +649,51 @@
"ruleActionNotDesc": "Логическое правило NOT",
"ruleActionSubRuleDesc": "Переход к подправилу; обратите внимание на скобки",
"ruleActionMatchDesc": "Сопоставляет все запросы, условия не нужны",
"subRuleEmpty": "Подправило пусто",
"proxyProvidersNotEmpty": "Провайдеры прокси не могут быть пустыми",
"contentNotEmpty": "Содержимое не может быть пустым",
"subRuleNotEmpty": "Подправило не может быть пустым",
"splitStrategyNotEmpty": "Стратегия распределения не может быть пустой",
"proxyProvidersEmpty": "Список провайдеров прокси пуст",
"timeout": "Тайм-аут",
"delayFailed": "Сбой",
"invalidSubRule": "{subRule} — недопустимый SUB_RULE",
"invalidPolicy": "{target} — недопустимая политика",
"invalidProxyProvider": "{providerName} — недопустимый провайдер прокси",
"invalidRuleSet": "{ruleSet} — недопустимый набор правил",
"invalidNetworkContent": "Поддерживаются только tcp и udp",
"invalidRangeContent": "Введите числа или диапазоны, например 80 или 8000-9000, через /",
"invalidDscpContent": "Метка DSCP не может превышать 63",
"invalidCidrContent": "Введите диапазон IP в формате CIDR, например 192.168.0.0/16",
"invalidPortRangeContent": "Введите порт или диапазон, например 443 или 8000-9000",
"invalidHostContent": "Введите домен или IP-адрес",
"invalidListenContent": "Введите адрес и порт, например 0.0.0.0:1053",
"invalidProxy": "{proxyName} — недопустимый прокси",
"proxyGroupDetectedAbnormal": "Обнаружены отклонения в текущей группе прокси",
"proxyProviderDetectedAbnormal": "Обнаружены отклонения в выбранных провайдерах прокси",
"proxyDetectedAbnormal": "Обнаружены отклонения в выбранных прокси",
"createProfile": "Создать профиль",
"locationPermissionRequired": "Требуется разрешение на геолокацию",
"locationPermissionGuide": "1. Откройте Системные настройки > Конфиденциальность и безопасность\n2. Выберите Службы геолокации\n3. Найдите и отметьте {appName} в списке\n\nПосле настройки вернитесь в приложение и продолжайте работу. Спасибо за сотрудничество.",
"prerequisites": "Предварительные условия",
"ignoreBatteryOptimization": "Игнорировать оптимизацию батареи",
"batteryOptimizationDesc": "Чтобы приложение работало в фоне, отключите для него оптимизацию батареи. Нажмите, чтобы перейти к настройкам.",
"batteryOptimizationStatusTip": "Из-за системных ограничений во время работы невозможно корректно получить статус оптимизации батареи",
"batteryOptimizationDesc": "Игнорирование оптимизации батареи позволяет приложению работать в фоне.",
"locationPermission": "Разрешение на геолокацию",
"locationPermissionDesc": "По требованию системы для получения имени сети Wi-Fi необходимо разрешение на геолокацию. На Android выберите «Разрешить всегда», иначе имя сети Wi-Fi нельзя получить, пока приложение в фоне.",
"excludeSsids": "Исключённые SSID",
"excludeSsidsDesc": "При подключении к Wi-Fi с исключённым SSID состояние работы приложения переключается автоматически",
"excludeSsidsDesc": "При подключении к Wi-Fi с исключённым SSID состояние работы приложения переключается автоматически.",
"ssidsEmpty": "Список SSID пуст",
"onDemand": "По условию",
"onDemandDesc": "Настройте состояние работы приложения для определённых сценариев",
"onDemandDesc": "«По условию» приостанавливает прокси в исключённых сетях Wi-Fi.",
"locationPermissionDeniedMessage": "Разрешение на геолокацию отклонено, поэтому невозможно получить имя текущей сети Wi-Fi. Включите разрешение на геолокацию вручную в системных настройках.",
"addSsid": "Добавить SSID",
"editSsid": "Изменить SSID",
"authorized": "Разрешено",
"tapToAuthorize": "Нажмите, чтобы разрешить",
"suspended": "Приостановлено...",
"geoOptions": "Настройки Geo",
"geoAutoUpdate": "Автообновление",
"geoAutoUpdateInterval": "Интервал автообновления",
"geoAutoUpdateIntervalTip": "Интервал автообновления должен быть больше 0",
"suspended": "Приостановлено…",
"resourceUpdateInterval": "Интервал автообновления",
"resourceUpdateIntervalTip": "Интервал автообновления должен быть больше 0",
"hours": "часов",
"hoursCount": "{count, plural, one{{count} час} few{{count} часа} many{{count} часов} other{{count} часа}}",
"geoResources": "Ресурсы Geo",
"minutes": "минут",
"geoResources": "Geo",
"geoSkipped": "{name}: уже последняя версия",
"geoUpdated": "{name}: обновлено",
"secondsCount": "{count, plural, one{{count} секунда} few{{count} секунды} many{{count} секунд} other{{count} секунды}}",
"entriesCount": "{count, plural, one{{count} запись} few{{count} записи} many{{count} записей} other{{count} записи}}",
"proxiesCount": "{count} прокси",
"rulesCount": "{count, plural, one{{count} правило} few{{count} правила} many{{count} правил} other{{count} правила}}",
"proxiesCount": "{count, plural, other{{label} прокси}}",
"rulesCount": "{count, plural, one{{label} правило} few{{label} правила} many{{label} правил} other{{label} правила}}",
"proxyGroupsCount": "{count, plural, one{{label} группа прокси} few{{label} группы прокси} many{{label} групп прокси} other{{label} группы прокси}}",
"changelogBreaking": "Важные изменения",
"changelogFeatures": "Новые функции",
"changelogFixes": "Исправления",
@@ -600,7 +707,6 @@
"pinWindow": "Закрепить поверх всех окон",
"unpinWindow": "Открепить окно",
"exitFullScreen": "Выйти из полноэкранного режима",
"toggleLabel": "Переключить подписи",
"torch": "Фонарик",
"pickFromAlbum": "Выбрать из галереи",
"blockConnection": "Заблокировать соединение",
@@ -611,11 +717,227 @@
"previousMatch": "Предыдущее совпадение",
"nextMatch": "Следующее совпадение",
"clearSearch": "Очистить поиск",
"addWidget": "Добавить виджет",
"noSearchResults": "Ничего не найдено",
"showPassword": "Показать пароль",
"hidePassword": "Скрыть пароль",
"authorize": "Разрешить",
"installedAppsPermissionRequired": "Требуется разрешение на список приложений",
"installedAppsPermissionDesc": "Эта система не выдаёт список установленных приложений без разрешения. Предоставьте его, чтобы настроить прокси для отдельных приложений.",
"installedAppsPermissionDeniedMessage": "Разрешение на список приложений отклонено, поэтому установленные приложения недоступны. Предоставьте его вручную в системных настройках."
"installedAppsPermissionDeniedMessage": "Разрешение на список приложений отклонено, поэтому установленные приложения недоступны. Предоставьте его вручную в системных настройках.",
"outboundIp": "Исходящий IP",
"ipAddress": "IP-адрес",
"ipQualityLevel": "Уровень",
"ipQualityGood": "Хороший",
"ipQualityNormal": "Обычный",
"ipQualityRisky": "Рискованный",
"ipType": "Тип",
"ipTypeResidential": "Домашний",
"ipTypeMobile": "Мобильная сеть",
"ipTypeBusiness": "Бизнес",
"ipTypeHosting": "Дата-центр",
"ipFlags": "Метки",
"ipFlagProxy": "Прокси",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "Злоупотребления",
"ipOrganization": "Организация",
"ipAsn": "ASN",
"ipQualitySource": "Источник ответа",
"ipQualitySources": "Источники",
"ipQualityFailed": "Не удалось определить тип IP",
"ipQualityRetry": "Проверить снова",
"ipSourceNoType": "Тип не определён",
"ipSourceRateLimited": "Лимит запросов",
"ipSourceIpMismatch": "Другой исходящий IP",
"hideIp": "Скрыть IP",
"editorUnavailable": "Редактор недоступен",
"replace": "Заменить",
"replaceAll": "Заменить все",
"cameraPermissionRequired": "Требуется доступ к камере",
"cameraPermissionDesc": "Разрешите доступ к камере в системных настройках, чтобы сканировать QR-коды, или выберите изображение QR-кода из галереи.",
"cameraUnavailable": "Камера недоступна",
"retry": "Повторить",
"invalidProfileQrcode": "Этот QR-код не содержит ссылку на профиль",
"dnsQueries": "DNS-запросы",
"recordType": "Тип записи",
"answers": "Ответы",
"responseCode": "Код ответа",
"error": "Ошибка",
"initiator": "Инициатор",
"cache": "Кэш",
"yes": "Да",
"no": "Нет",
"nonTextProviderFile": "Этот внешний ресурс не является текстовым файлом",
"addCustomProxy": "Добавить прокси",
"definitionNotMap": "Конфигурация должна быть YAML-словарём с полями name и type",
"ruleTextInvalid": "Правило должно начинаться с поддерживаемого типа, например DOMAIN,example.com,DIRECT",
"ruleListInvalid": "Правила должны быть YAML-списком, по одному правилу в элементе, например - DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "Не удалось прочитать прокси из ссылок",
"shareLinkUnreadable": "Не удалось прочитать прокси из {link}",
"proxiesProfileLabel": "{name} и другие ({count} прокси)",
"singleShareLinkOnly": "Здесь укажите одну ссылку, несколько можно задать в быстром редактировании списка",
"username": "Имя пользователя",
"emptyFallback": "Резерв для пустой группы",
"defaultSelected": "Выбор по умолчанию",
"hashByInUser": "Хешировать по пользователю входящего подключения",
"appProxiesEmpty": "Нет локальных прокси",
"nodes": "Узлы",
"localProxies": "Локальные прокси",
"customIssueEmptyName": "Имя не задано",
"customIssueReservedName": "{name} — встроенное имя политики, его нельзя использовать",
"customIssueDuplicateName": "Имя {name} уже занято другим прокси или группой прокси",
"customIssueCoreRejected": "Ядро не может разобрать этот прокси: {message}",
"customIssueMissingProxies": "Эти прокси или политики не существуют: {names}",
"customIssueMissingProviders": "Эти провайдеры прокси не существуют: {names}",
"customIssueNoProxySource": "Не выбраны ни прокси, ни провайдеры прокси, поэтому ядро отклонит эту группу",
"customIssueGroupLoop": "Группы прокси ссылаются друг на друга по кругу: {path}",
"customIssueInvalidEmptyFallback": "Резервом для пустой группы может быть только прокси, а {name} не существует или является группой прокси",
"customIssueInvalidFilter": "{name}: недопустимое регулярное выражение: {message}",
"customIssueMissingDialer": "Прокси для подключения {name} не существует",
"customIssueDialerLoop": "{proxy} подключается через {target}, а {target} снова ведёт к {proxy}, поэтому подключение зацикливается",
"dialerProxyLoopStopped": "Подключение через {name} ведёт обратно к прокси, который его использует, поэтому соединение остановлено",
"customIssuesSummary": "Проблем: {count}, применение профиля может завершиться ошибкой",
"providerInUse": "«{label}» всё ещё используется профилями: {profiles}. После удаления они будут работать неправильно. Вы уверены, что хотите удалить его?",
"proxyGroupInUse": "На «{label}» всё ещё ссылаются правила, группы прокси, прокси для подключения, DNS или NTP этого профиля, и после удаления эти ссылки перестанут работать. Вы уверены, что хотите удалить её?",
"proxiesRemovedTip": "Будут удалены прокси: {removed}. Строка с изменённым именем считается новым прокси, а пользовательские профили, ссылающиеся на удалённые, пометят их как отсутствующие. Применить?",
"proxiesReplacedTip": "Будут удалены прокси: {removed}. Будут добавлены: {added}. Строка с изменённым именем считается новым прокси, а пользовательские профили, ссылающиеся на удалённые, пометят их как отсутствующие. Применить?",
"fontSize": "Размер",
"large": "Крупный",
"extraLarge": "Очень крупный",
"switchProfile": "Сменить профиль",
"lastUpdated": "Последнее обновление",
"filters": "Фильтры",
"label": "Метка",
"regex": "Регулярное выражение",
"matchCase": "Учитывать регистр",
"iconSets": "Наборы значков",
"invalidIconSet": "Это не набор значков",
"recommendedIcons": "Рекомендуемые",
"localImage": "Локальное изображение",
"iconSetsEmptyTip": "Наборов значков пока нет. Добавьте их в разделе «Ресурсы пользовательских профилей → Наборы значков»",
"outbound": "Исходящие",
"strictRoute": "Строгая маршрутизация",
"congestionController": "Управление перегрузкой TCP",
"congestionControllerDesc": "Управление перегрузкой TCP действует только со стеком mips.",
"routeExcludeAddress": "Исключённые адреса маршрутов",
"keepAliveIdle": "Простой TCP keep-alive",
"disableKeepAlive": "Отключить TCP keep-alive",
"routingMark": "Метка маршрутизации",
"strictRouteDesc": "Строгая маршрутизация не даёт трафику вроде DNS обходить TUN, но другие устройства перестают видеть это устройство.",
"initFailed": "Не удалось запустить",
"initFailedTip": "При запуске FlClash произошла ошибка, и работа не может быть продолжена. Скопируйте подробности, чтобы сообщить о проблеме. Если ошибка повторяется, очистка данных обычно позволяет снова запустить приложение.",
"errorDetails": "Подробности ошибки",
"stackTrace": "Трассировка стека",
"clearDataAndExitTip": "Все профили, настройки и локальные данные будут удалены, после чего приложение закроется. Откройте его снова, чтобы начать заново.",
"clearDataFailed": "Не удалось очистить данные: {detail}",
"bypassPrivateRoute": "Обходить частные адреса",
"bypassPrivateRouteDesc": "При обходе частных адресов адреса LAN, loopback и multicast идут в обход TUN.",
"disableIcmpForwarding": "Отключить пересылку ICMP",
"disableIcmpForwardingDesc": "При отключённой пересылке ICMP TUN сам отвечает на ping, поэтому ping больше не показывает реальную задержку.",
"excludeInterface": "Исключённые интерфейсы",
"excludeInterfaceDesc": "Трафик с исключённых интерфейсов, например с docker0, идёт в обход TUN.",
"includeInterface": "Включённые интерфейсы",
"includeInterfaceDesc": "Через TUN идёт только трафик с включённых интерфейсов, а исключённые интерфейсы больше не действуют.",
"includeUid": "Включённые ID пользователей",
"includeUidDesc": "Через TUN идёт только трафик процессов включённых ID пользователей.",
"excludeUid": "Исключённые ID пользователей",
"excludeUidDesc": "Трафик процессов исключённых ID пользователей идёт в обход TUN.",
"excludeSrcPort": "Исключённые исходные порты",
"excludeSrcPortDesc": "Трафик с исключённых исходных портов идёт в обход TUN.",
"excludeDstPort": "Исключённые порты назначения",
"excludeDstPortDesc": "Трафик на исключённые порты назначения идёт в обход TUN.",
"udpTimeout": "Тайм-аут UDP",
"udpTimeoutDesc": "Тайм-аут UDP задаётся в секундах и действует только со стеками gvisor, system и mixed.",
"dnsHijackDesc": "Без этого параметра TUN перехватывает все DNS-запросы на порт 53 (any:53).",
"tunOverrideDesc": "Добавленные здесь параметры заменяют настройки TUN всех профилей; остальные остаются такими, как в профиле. Параметры, добавленные в самом пользовательском профиле, имеют приоритет.",
"profileTunSettingsDesc": "Действует только для этого профиля и имеет приоритет над переопределяемыми параметрами TUN в настройках сети.",
"settingsCount": "{count, plural, one{{count} настройка} few{{count} настройки} many{{count} настроек} other{{count} настройки}}",
"sectionsInEffect": "Действуют: {count}",
"itemsCount": "{count, plural, one{{count} элемент} few{{count} элемента} many{{count} элементов} other{{count} элемента}}",
"additionalPrefix": "Префикс имени",
"additionalSuffix": "Суффикс имени",
"skipCertVerify": "Пропускать проверку сертификата",
"ipVersion": "Версия IP",
"addFilters": "Добавить фильтры",
"failedItem": "{label}: {message}",
"failedCount": "Ошибок: {count}",
"importConfigInvalid": "Этот файл не является конфигурацией, которую можно импортировать",
"importConfigReplaceTip": "Импорт заменит текущие группы прокси, правила и настройки этого профиля",
"networkName": "Имя сети",
"networkSecret": "Секрет сети",
"peers": "Пиры",
"virtualIpv4": "Виртуальный IPv4",
"hostname": "Имя хоста",
"exitNodes": "Выходные узлы",
"cipher": "Шифр",
"protocol": "Протокол",
"protocolParam": "Параметр протокола",
"obfs": "Обфускация",
"obfsParam": "Параметр обфускации",
"obfsPassword": "Пароль обфускации",
"obfsMode": "Режим обфускации",
"obfsHost": "Хост обфускации",
"plugin": "Плагин",
"pluginMode": "Режим плагина",
"pluginHost": "Хост плагина",
"privateKey": "Закрытый ключ",
"publicKey": "Открытый ключ",
"privateKeyPassphrase": "Пароль закрытого ключа",
"preSharedKey": "Общий ключ",
"secretKey": "Ключ",
"transport": "Транспорт",
"uploadBandwidth": "Скорость отдачи",
"downloadBandwidth": "Скорость загрузки",
"protocolVersion": "Версия протокола",
"portHopping": "Перескок портов",
"hopInterval": "Интервал перескока",
"congestionControl": "Управление перегрузкой",
"udpRelayMode": "Режим ретрансляции UDP",
"allowedIps": "Разрешённые IP",
"remoteDnsResolve": "Удалённое разрешение DNS",
"clientFingerprint": "Отпечаток клиента",
"required": "Обязательно",
"ageSecretKey": "Секретный ключ age",
"ageEncryptedProfile": "Зашифрованный профиль",
"ageSecretKeyRequiredTip": "Профиль зашифрован с помощью age, для расшифровки нужен секретный ключ age",
"currentOutboundDesc": "Содержит кнопку запуска, удалить нельзя",
"running": "Работает",
"notRunning": "Не запущено",
"trafficPeak": "Пик {value}",
"trafficTotal": "Всего {value}",
"ipQualityShort": "Качество",
"ipTypeUnknown": "Тип неизвестен",
"tapIconToRetry": "Нажмите на значок, чтобы повторить",
"networkTakeover": "Перехват трафика",
"portValue": "Порт {port}",
"serviceAbnormalCount": "Проблем: {count}",
"serviceAllAvailable": "Все доступны",
"serviceShownOnDashboard": "Показан на панели",
"groupTimeoutCount": "Тайм-аут: {count}",
"groupCountUnit": "групп",
"groupCount": "Групп: {count}",
"proxyGroupsAfterStart": "Группы появятся здесь после запуска ядра",
"usedShort": "использовано",
"profileUpdatedAt": "Обновлено {time}",
"profileExpiresIn": "Истекает {date}, осталось дней: {days}",
"profileDaysLeft": "Осталось дней: {days}",
"target": "Цель",
"outboundExit": "Выход",
"duration": "Длительность",
"traffic": "Трафик",
"totalDownload": "Загружено",
"totalUpload": "Отдано",
"initiatorShort": "Источник",
"result": "Результат",
"elapsed": "Время",
"connectionStats": "Статистика подключений",
"connectionStatsDesc": "Подключения, запросы и DNS-запросы",
"memoryInfoDesc": "Память приложения и ядра",
"editDashboard": "Изменить панель",
"done": "Готово",
"dashboardAddableSection": "Можно добавить",
"customImportDroppedSubRules": "{count, plural, one{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правило SUB-RULE} few{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правила SUB-RULE} many{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правил SUB-RULE} other{Пользовательские профили не поддерживают подправила, поэтому отброшено {label} правила SUB-RULE}}",
"autoInclude": "Автоматическое включение",
"allReferencedProxies": "Все используемые прокси",
"allReferencedProxyProviders": "Все используемые провайдеры прокси"
}
+499 -177
View File
@@ -9,19 +9,31 @@
"profiles": "配置",
"tools": "工具",
"logs": "日志",
"logsDesc": "日志捕获记录",
"resources": "资源",
"resourcesDesc": "外部资源相关信息",
"trafficUsage": "流量统计",
"networkSpeed": "网络速度",
"outboundMode": "出站模式",
"networkDetection": "网络检测",
"serviceStatus": "服务状态",
"serviceRestricted": "访问受限",
"serviceAvailable": "可用",
"serviceUnavailable": "不可用",
"serviceFailed": "检测失败",
"serviceDisallowedIsp": "不允许的 ISP",
"serviceBlocked": "已被封禁",
"serviceUnsupportedRegion": "地区不支持",
"serviceOriginalsOnly": "仅限自制内容",
"serviceComingSoon": "即将上线",
"servicePending": "待检测",
"serviceManage": "管理服务",
"serviceCheck": "检测",
"serviceCheckAll": "全部检测",
"serviceCheckedAt": "检测于 {time}",
"upload": "上传",
"download": "下载",
"usedTraffic": "已用流量",
"totalTraffic": "总流量",
"expireTime": "到期时间",
"nullProfileDesc": "没有配置文件,请先添加配置文件",
"nullProfileDesc": "添加一个配置文件后即可开始使用",
"settings": "设置",
"language": "语言",
"defaultText": "默认",
@@ -29,34 +41,31 @@
"other": "其他",
"about": "关于",
"en": "英语",
"es": "西班牙语",
"fr": "法语",
"ja": "日语",
"ko": "韩语",
"ru": "俄语",
"zhCN": "中文简体",
"theme": "主题",
"themeDesc": "设置深色模式,调整色彩",
"override": "覆写",
"extend": "扩展",
"runtimeConfig": "运行时配置",
"allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理",
"tun": "虚拟网卡",
"tunDesc": "仅在管理员模式生效",
"tunDesc": "虚拟网卡仅在管理员模式生效。",
"minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件",
"autoLaunch": "自启动",
"autoLaunchDesc": "跟随系统自启动",
"silentLaunch": "静默启动",
"silentLaunchDesc": "后台启动",
"silentLaunchDesc": "静默启动时不显示窗口。",
"autoRun": "自动运行",
"autoRunDesc": "应用打开时自动运行",
"autoRunDesc": "自动运行会在打开应用时启动代理。",
"logcat": "日志捕获",
"logcatDesc": "禁用将会隐藏日志入口",
"logcatDesc": "关闭日志捕获会隐藏日志入口。",
"autoCheckUpdate": "自动检查更新",
"autoCheckUpdateDesc": "应用启动时自动检查更新",
"checkCertificate": "校验 TLS 证书",
"checkCertificateDesc": "拒绝不受信任的证书。关闭后订阅和备份将暴露于中间人攻击",
"checkCertificateDesc": "关闭证书校验后,订阅和备份可能遭中间人攻击。",
"accessControl": "访问控制",
"accessControlDesc": "配置应用访问代理",
"application": "应用程序",
"applicationDesc": "修改应用程序相关设置",
"accessControlDesc": "访问控制决定哪些应用使用代理。",
"edit": "编辑",
"confirm": "确定",
"update": "更新",
@@ -67,15 +76,17 @@
"qrcode": "二维码",
"qrcodeDesc": "扫描二维码获取配置文件",
"url": "URL",
"urlDesc": "通过URL获取配置文件",
"link": "链接",
"linkDesc": "通过订阅链接或代理分享链接获取配置文件",
"file": "文件",
"fileDesc": "直接上传配置文件",
"name": "名称",
"profileNameNullValidationDesc": "请输入配置名称",
"rename": "重命名",
"profileUrlNullValidationDesc": "请输入配置URL",
"profileUrlInvalidValidationDesc": "请输入有效配置URL",
"autoUpdate": "自动更新",
"autoUpdateInterval": "自动更新间隔(分钟)",
"autoUpdateInterval": "自动更新间隔",
"profileAutoUpdateIntervalNullValidationDesc": "请输入自动更新间隔时间",
"profileAutoUpdateIntervalInvalidValidationDesc": "请输入有效间隔时间格式",
"themeMode": "主题模式",
@@ -84,7 +95,7 @@
"auto": "自动",
"light": "浅色",
"dark": "深色",
"importFromURL": "从URL导入",
"importFromLink": "从链接导入",
"submit": "提交",
"createProfileFromUrlTip": "是否要通过 {url} 创建配置?",
"create": "创建",
@@ -103,12 +114,15 @@
"show": "显示",
"exit": "退出",
"systemProxy": "系统代理",
"safeMode": "安全模式",
"safeModeAppTitle": "{appName}(安全模式)",
"project": "项目",
"core": "内核",
"tabAnimation": "选项卡动画",
"desc": "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。",
"startVpn": "正在启动VPN...",
"stopVpn": "正在停止VPN...",
"startVpn": "正在启动VPN…",
"stopVpn": "正在停止VPN…",
"localNetworkDeniedTip": "本地网络权限被拒绝:已改用 gvisor 栈,局域网无法访问。",
"compatible": "兼容模式",
"notSelectedTip": "当前代理组无法选中",
"changeProxyFailedTip": "切换代理失败,已恢复上一次的选择",
@@ -127,8 +141,8 @@
"addressTip": "请输入有效的WebDAV地址",
"password": "密码",
"authentication": "认证",
"authenticationDesc": "为本地代理端口启用认证,防止本机其他应用擅自使用",
"authenticationSystemProxyDesc": "认证启用时不生效",
"authenticationDesc": "认证开启后,使用本地代理端口需要账号密码。",
"authenticationSystemProxyDesc": "认证启用时,系统代理不生效。",
"checkUpdate": "检查更新",
"discoverNewVersion": "发现新版本",
"checkUpdateError": "当前应用已经是最新版了",
@@ -137,40 +151,37 @@
"country": "区域",
"search": "搜索",
"allowBypass": "允许应用绕过VPN",
"allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
"ipv6Desc": "开启后将可以接收IPv6流量",
"externalControllerDesc": "外部控制器开启后,可通过 9090 端口控制内核。",
"externalControllerSecret": "外部控制器密钥",
"ipv6Desc": "开启 IPv6 后才会接收 IPv6 流量。",
"app": "应用",
"general": "常规",
"systemProxyDesc": "设置系统代理",
"startupAndBackground": "启动与后台",
"requestsAndUpdates": "请求与更新",
"logsAndDiagnostics": "日志与诊断",
"userAgent": "用户代理",
"unifiedDelay": "统一延迟",
"unifiedDelayDesc": "去除握手等额外延迟",
"tcpConcurrent": "TCP并发",
"tcpConcurrentDesc": "开启后允许TCP并发",
"geodataLoader": "Geo低内存模式",
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求",
"requestsDesc": "查看最近请求记录",
"recentRequests": "最近请求",
"findProcessMode": "查找进程",
"init": "初始化",
"infiniteTime": "长期有效",
"connections": "连接",
"liveConnections": "实时连接",
"inbound": "入站",
"connectionsDesc": "查看当前连接数据",
"intranetIP": "内网 IP",
"view": "查看",
"cut": "剪切",
"copy": "复制",
"clipboardWriteFailed": "无法复制到剪贴板,所选内容可能过大",
"paste": "粘贴",
"testUrl": "测速链接",
"sync": "同步",
"exclude": "从最近任务中隐藏",
"excludeDesc": "应用在后台时,从最近任务中隐藏应用",
"expand": "标准",
"shrink": "紧凑",
"min": "最小",
"compact": "紧凑",
"tab": "标签页",
"list": "列表",
"delay": "延迟",
@@ -182,19 +193,19 @@
"go": "前往",
"externalLink": "外部链接",
"otherContributors": "其他贡献者",
"appIconDesign": "应用图标设计",
"autoCloseConnections": "自动关闭连接",
"autoCloseConnectionsDesc": "切换节点后自动关闭连接",
"onlyStatisticsProxy": "仅统计代理",
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
"autoCloseConnectionsDesc": "自动关闭连接会在切换代理后断开已有连接。",
"onlyStatisticsProxy": "仅统计代理流量",
"showNotificationStopAction": "通知栏显示停止按钮",
"showNotificationStopActionDesc": "在常驻通知上显示停止按钮。若系统因此总是展开通知,可关闭",
"pureBlackMode": "纯黑模式",
"sidebarBlur": "侧边栏背景模糊",
"sidebarBlurDesc": "侧边栏透出窗口后方模糊的桌面",
"liquidGlass": "液态玻璃",
"keepAliveIntervalDesc": "TCP保持活动间隔",
"entries": "个条目",
"local": "本地",
"remote": "远程",
"remoteBackupDesc": "备份数据到WebDAV",
"localBackupDesc": "备份数据到本地",
"mode": "模式",
"time": "时间",
"source": "来源",
@@ -205,97 +216,177 @@
"layout": "布局",
"tight": "紧凑",
"standard": "标准",
"ultra": "极致",
"loose": "宽松",
"profilesSort": "配置排序",
"start": "启动",
"runTime": "运行时长",
"stop": "暂停",
"dnsDesc": "更新DNS相关设置",
"key": "键",
"value": "值",
"hostsDesc": "追加Hosts",
"vpnTip": "重启VPN后改变生效",
"vpnEnableDesc": "通过VpnService自动路由系统所有流量",
"vpnEnableDesc": "VPN 通过 VpnService 自动路由系统所有流量。",
"options": "选项",
"loopback": "回环解锁工具",
"loopbackDesc": "用于UWP回环解锁",
"enabled": "已开启",
"disabled": "已关闭",
"loopback": "UWP 回环解锁",
"providers": "外部资源",
"proxyProviders": "代理集",
"ruleProviders": "规则集",
"providerUrlTip": "仅支持远程资源",
"ruleSetEmptyTip": "规则集中没有可用的条目",
"ruleSetMixedTip": "规则集同时包含域名和 IP 段,无法判断类型",
"ruleSetInvalidTip": "无法读取该 mrs 文件",
"subscriptionInfo": "订阅信息",
"overrideDns": "覆写DNS",
"overrideDnsDesc": "开启后将覆盖配置中的DNS选项",
"ntpStatusDesc": "开启 NTP 后从 NTP 服务器获取时间,而非系统时钟。",
"server": "服务器",
"ntpInterval": "同步间隔(分钟)",
"dialerProxy": "拨号代理",
"dialerProxyDesc": "拨号代理为连接 NTP 服务器使用的出站。",
"writeToSystem": "写入系统",
"writeToSystemDesc": "写入系统会同时设置系统时钟,Android 上不生效。",
"sniffer": "域名嗅探",
"snifferStatusDesc": "域名嗅探从 TLS、HTTP、QUIC 流量中识别域名,供规则匹配。",
"overrideDestination": "覆盖目标地址",
"overrideDestinationDesc": "覆盖目标地址使用嗅探到的域名替换原目标地址发起连接。",
"forceDnsMapping": "强制DNS映射",
"forceDnsMappingDesc": "强制 DNS 映射对通过 DNS 映射得到域名的连接也进行嗅探。",
"parsePureIp": "解析纯IP",
"parsePureIpDesc": "解析纯 IP 会嗅探只有 IP 地址的连接。",
"forceDomain": "强制嗅探域名",
"forceDomainDesc": "强制嗅探域名中的域名同样会被嗅探,其余已有域名的连接不嗅探。",
"skipDomain": "跳过域名",
"skipDomainDesc": "嗅探到跳过域名中的域名时不予采用。",
"skipSrcAddress": "跳过来源地址",
"skipDstAddress": "跳过目标地址",
"sniffProtocols": "协议",
"ports": "端口",
"overrideEntries": "覆写项",
"addOverrideEntry": "添加覆写项",
"dnsOverrideDesc": "添加的项会覆写所有 URL 和文件配置的 DNS,未添加的项沿用配置自身的设置。自定义配置的 DNS 在其内容中设置。",
"settingEntries": "设置项",
"addSettingEntry": "添加设置项",
"profileSettingsDesc": "仅作用于当前配置,未添加的项使用默认值。",
"healthCheck": "健康检查",
"discardChanges": "是否放弃更改?",
"quickEdit": "快速编辑",
"status": "状态",
"statusDesc": "关闭后将使用系统DNS",
"preferH3Desc": "优先使用DOH的http/3",
"statusDesc": "关闭 DNS 后将使用系统 DNS。",
"respectRules": "遵守规则",
"respectRulesDesc": "DNS连接跟随rules,需配置proxy-server-nameserver",
"respectRulesDesc": "遵守规则使 DNS 连接遵守规则,需配置 Proxy Server Nameserver。",
"dnsMode": "DNS模式",
"fakeipRange": "Fakeip范围",
"fakeipFilter": "Fakeip过滤",
"defaultNameserver": "默认域名服务器",
"defaultNameserverDesc": "用于解析DNS服务器",
"nameserver": "域名服务器",
"nameserverDesc": "用于解析域名",
"fakeipRange": "Fake-IP范围",
"fakeipFilter": "Fake-IP过滤",
"useHosts": "使用Hosts",
"useSystemHosts": "使用系统Hosts",
"nameserverPolicy": "域名服务器策略",
"nameserverPolicyDesc": "指定对应域名服务器策略",
"proxyNameserver": "代理域名服务器",
"proxyNameserverDesc": "用于解析代理节点的域名",
"fallback": "Fallback",
"fallbackDesc": "一般情况下使用境外DNS",
"fallbackFilter": "Fallback过滤",
"geoipCode": "Geoip代码",
"ipcidr": "IP/掩码",
"domain": "域名",
"listenRoutingMark": "监听路由标记",
"listenRoutingMarkDesc": "监听路由标记仅在 Linux 生效。",
"ipv6Timeout": "IPv6超时(毫秒)",
"cacheAlgorithm": "缓存算法",
"cacheMaxSize": "缓存大小",
"fakeipRange6": "Fake-IP范围(IPv6)",
"fakeipFilterMode": "Fake-IP过滤模式",
"fakeipFilterModeDesc": "Fake-IP 过滤模式中,blacklist 排除匹配项,whitelist 仅用于匹配项,rule 按规则匹配。",
"fakeipTtl": "Fake-IP TTL",
"reset": "重置",
"actionView": "显示/隐藏",
"actionStart": "启动/停止",
"actionMode": "切换模式",
"actionProxy": "系统代理",
"actionTun": "虚拟网卡",
"actionRuleMode": "规则模式",
"actionGlobalMode": "全局模式",
"actionDirectMode": "直连模式",
"actionDelayTest": "测试全部延迟",
"actionUpdateProfiles": "更新全部配置",
"disclaimer": "免责声明",
"disclaimerDesc": "本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
"disclaimerDesc": "在使用 FlClash(以下简称“本软件”)前,请仔细阅读并充分理解本声明的全部内容。点击“同意”即表示你已阅读、理解并接受以下全部条款;如不同意,请点击“退出”并停止使用本软件。",
"disclaimerSoftwareTitle": "软件性质",
"disclaimerSoftwareContent": "本软件是基于 Clash.Meta(mihomo)内核的开源网络代理客户端,仅提供配置管理、规则分流与流量转发等本地工具功能。",
"disclaimerNoServiceStatement": "本软件自身不提供任何代理服务器、节点、订阅、机场或网络接入服务,也不与任何此类服务的提供者存在合作、代理或担保关系。",
"disclaimerUsageTitle": "使用限制",
"disclaimerUsageContent": "本软件仅供学习交流、技术研究等非商业用途。严禁将本软件用于任何商业目的,包括但不限于付费分发、捆绑销售、作为商业服务的组成部分或以本软件名义开展经营活动;任何商业行为均与本软件及其开发者无关。\n\n严禁将本软件用于违反你所在国家或地区法律法规的活动,包括但不限于绕过依法设置的网络访问限制、传播违法信息、实施网络攻击或侵犯他人合法权益。",
"disclaimerResponsibilityTitle": "用户责任",
"disclaimerResponsibilityContent": "你应自行确认使用本软件在你所在国家或地区是合法的,并对使用本软件的全部行为及其后果独立承担法律责任。\n\n你导入的订阅、节点与配置由你自行选择和判断,其来源是否合法、内容是否安全、服务是否稳定,均由你与相关提供者自行负责。",
"disclaimerThirdPartyTitle": "第三方内容",
"disclaimerThirdPartyContent": "订阅链接、配置文件、规则集、脚本、外部资源及外部链接均由第三方提供,开发者无法也不会对其合法性、准确性、安全性和可用性进行审查或担保。\n\n因使用第三方内容导致的数据泄露、财产损失、账号封禁或其他任何损失,由你与第三方自行解决,开发者不承担任何责任。",
"disclaimerWarrantyTitle": "无担保声明",
"disclaimerWarrantyContent": "本软件按“现状”和“可用”的原则提供,不附带任何明示或默示的担保,包括但不限于适销性、特定用途适用性、不侵权以及持续可用、无错误、无安全漏洞的担保。\n\n开发者不保证本软件能满足你的需求,也不保证本软件的运行不会中断或不会出现错误。",
"disclaimerLiabilityTitle": "责任限制",
"disclaimerLiabilityContent": "在适用法律允许的最大范围内,开发者及所有贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害承担责任,包括但不限于数据丢失、设备损坏、网络异常、业务中断、利润损失或因此引起的任何法律纠纷,即使已被告知发生此类损害的可能性。",
"disclaimerPrivacyTitle": "数据收集与隐私",
"disclaimerPrivacyContent": "本软件不会收集或上传你的订阅地址、节点信息、配置内容、访问的网站、连接记录、流量内容和日志,这些数据仅保存在你的设备本地,开发者无法获取。\n\n本软件只会在你使用相应功能时访问外部网络,例如更新订阅时访问你提供的订阅地址、检查更新时访问 GitHub。\n\n本软件在所有平台上均未集成任何数据统计或崩溃上报服务。",
"disclaimerLicenseTitle": "开源协议",
"disclaimerLicenseContent": "本软件以 GPL-3.0 协议开源。你可以在遵守该协议的前提下自由使用、修改和分发本软件,但须同样以 GPL-3.0 协议开源并保留原作者的版权声明。\n\n本软件包含的第三方组件(包括 Clash.Meta 内核)分别遵循其各自的开源协议。基于本软件修改或分发的版本所产生的任何问题,与原作者无关。",
"disclaimerChangesTitle": "声明变更",
"disclaimerChangesContent": "开发者有权随版本更新修改本声明,修改后的内容随新版本发布即生效。你在更新后继续使用本软件,即视为接受修改后的声明。",
"disclaimerAcceptTitle": "接受声明",
"disclaimerAcceptContent": "一旦你安装、复制或使用本软件,即视为你已阅读并同意本声明的全部内容。如你不同意本声明的任何条款,请立即停止使用并卸载本软件。",
"disclaimerRestateTitle": "复述声明",
"disclaimerRestateTip": "为确认你已知悉,请在下方输入框中完整复述以下声明:",
"disclaimerRestateHint": "在此输入上述声明",
"disclaimerRestateMismatch": "与声明内容不一致",
"disclaimerAcknowledgeTitle": "确认声明",
"disclaimerAcknowledgeTip": "为确认你已知悉,请阅读以下声明并勾选下方选项:",
"disclaimerAcknowledgeLabel": "我已阅读并理解此声明",
"agree": "同意",
"hotkeyManagement": "快捷键管理",
"hotkeyManagementDesc": "使用键盘控制应用程序",
"pressKeyboard": "请按下按键",
"inputCorrectHotkey": "请输入正确的快捷键",
"hotkeyConflict": "快捷键冲突",
"hotkeyDesc": "全局快捷键在窗口隐藏时依然生效,点击操作即可录制组合键。",
"hotkeyNotSet": "未设置",
"pressKeyboard": "请按下组合键",
"hotkeyNeedsModifier": "至少包含 {modifiers} 中的一个",
"hotkeyConflictWith": "已被“{action}”使用,保存后将移到这里",
"hotkeyUnavailable": "未能注册,可能已被其他应用占用",
"remove": "移除",
"noHotKey": "暂无快捷键",
"noNetwork": "无网络",
"ipv6InboundDesc": "允许IPv6入站",
"ipv6InboundDesc": "开启 IPv6 后,VPN 允许 IPv6 入站。",
"exportLogs": "导出日志",
"exportSuccess": "导出成功",
"iconStyle": "图标样式",
"onlyIcon": "仅图标",
"iconStyleFilled": "色块",
"iconStylePlain": "纯图标",
"iconStyleHidden": "隐藏",
"filter": "筛选",
"hideTimeoutProxies": "隐藏超时代理",
"hideTimeoutProxiesDesc": "不显示上次延迟测试超时的代理",
"stackMode": "栈模式",
"network": "网络",
"networkDesc": "修改网络相关设置",
"bypassDomain": "排除域名",
"bypassDomainDesc": "仅在系统代理启用时生效",
"resetTip": "确定要重置吗?",
"icon": "图片",
"bypassDomainDesc": "排除域名仅在系统代理启用时生效。",
"resetTip": "确定要重置吗?",
"icon": "图标",
"noData": "暂无数据",
"fontFamily": "字体",
"installedFonts": "已安装字体",
"toggle": "切换",
"system": "系统",
"routeMode": "路由模式",
"routeModeBypassPrivate": "绕过私有路由地址",
"routeModeConfig": "使用配置",
"routeAddress": "路由地址",
"routeAddressDesc": "配置监听路由地址",
"interfaceNameMode": "出站网卡",
"interfaceNameModeClear": "清空",
"interfaceNameModeFollow": "跟随配置",
"interfaceNameModeCustom": "自定义",
"interfaceName": "网卡名称",
"interfaceNameDesc": "出站连接使用的网卡名称",
"interfaceNameDesc": "网卡名称为出站连接使用的网卡。",
"copyEnvVar": "复制环境变量",
"memoryInfo": "内存信息",
"total": "总计",
"releaseMemory": "释放内存",
"memoryCoreHeapInuse": "堆内存使用中",
"memoryCoreHeapIdle": "堆内存空闲",
"memoryCoreStack": "协程栈",
"memoryCoreRuntime": "运行时开销",
"memoryCoreNotRunning": "内核未运行",
"memoryAppResident": "常驻内存",
"memoryAppShared": "应用及共享",
"memoryEstimateDesc": "基于进程常驻内存估算,可能与系统显示的数值不同。",
"memoryEstimateSharedDesc": "内核与应用运行在同一进程,内核部分按运行时统计估算,其余计入应用及共享内存。",
"memoryReleased": "内存已释放",
"memoryReleasedSize": "已释放 {size}",
"releaseMemoryFailed": "释放内存失败",
"cancel": "取消",
"fileIsUpdate": "文件有修改,是否保存修改",
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新?",
"hasCacheChange": "是否缓存修改",
"copySuccess": "复制成功",
"copyLink": "复制链接",
@@ -305,38 +396,38 @@
"listen": "监听",
"undo": "撤销",
"redo": "重做",
"lineWrap": "自动换行",
"none": "无",
"basicConfig": "基础配置",
"basicConfigDesc": "全局修改基础配置",
"advancedConfig": "进阶配置",
"advancedConfigDesc": "提供多样化配置",
"selectedCountTitle": "已选择 {count} 项",
"advancedConfigDesc": "进阶配置包含网络、DNS、附加规则与脚本。",
"addRule": "添加规则",
"ruleName": "规则名称",
"content": "内容",
"subRule": "子规则",
"ruleTarget": "规则目标",
"matchTargetTitle": "匹配目标",
"matchTarget": "MATCH-TARGET",
"followProfile": "跟随配置",
"matchTargetDesc": "目标为 MATCH-TARGET 的规则路由到这里,默认取本配置末尾 MATCH 规则的目标。",
"selectMatchTarget": "选择 MATCH-TARGET",
"sourceIp": "源IP",
"noResolve": "不解析IP",
"saveChanges": "是否保存更改?",
"findProcessModeDesc": "开启后会有一定性能损耗",
"tabAnimationDesc": "仅在移动视图中有效",
"colorSchemes": "配色方案",
"palette": "调色板",
"tonalSpotScheme": "调性点缀",
"fidelityScheme": "高保真",
"cannotSave": "无法保存",
"navigationBarStyle": "底栏",
"floating": "悬浮",
"docked": "固定",
"slide": "滑动",
"fade": "淡入",
"pureBlack": "纯黑",
"customColor": "自定义颜色",
"colorStyle": "配色风格",
"grid": "网格",
"spectrum": "光谱",
"sliders": "滑块",
"hue": "色相",
"saturation": "饱和度",
"brightness": "亮度",
"hex": "十六进制",
"monochromeScheme": "单色",
"neutralScheme": "中性",
"vibrantScheme": "活力",
"expressiveScheme": "表现力",
"contentScheme": "内容主题",
"rainbowScheme": "彩虹",
"fruitSaladScheme": "果缤纷",
"vividStyle": "鲜明",
"tintedStyle": "氛围",
"softStyle": "柔和",
"developerMode": "开发者模式",
"developerModeEnableTip": "开发者模式已启用。",
"messageTest": "消息测试",
@@ -348,6 +439,8 @@
"launchInterruptedTip": "应用上次在启动过程中意外退出。已跳过本次自动配置,你可以手动启动重试。",
"clearData": "清除数据",
"textScale": "文本缩放",
"textScalePreview": "应用内的文字将以这个大小显示",
"followSystem": "跟随系统",
"internet": "互联网",
"systemApp": "系统应用",
"noNetworkApp": "无网络应用",
@@ -357,11 +450,23 @@
"logsTest": "日志测试",
"emptyTip": "{label}不能为空",
"urlTip": "{label}必须为URL",
"invalidLinkTip": "不是有效的订阅链接或代理分享链接",
"numberTip": "{label}必须为数字",
"numberRangeTip": "{label}必须在 {min} 到 {max} 之间",
"interval": "间隔",
"existsTip": "{label}当前已存在",
"maxLengthTip": "{label}最多{max}个字符",
"multipleValuesTip": "多个值使用逗号分隔",
"singleAdd": "单条添加",
"batchAdd": "批量添加",
"batchListInputTip": "每行一项,也可用逗号分隔",
"batchMapInputTip": "每行一条,键和值之间用空格分隔",
"singleImport": "单条导入",
"batchImport": "批量导入",
"batchUrlInputTip": "每行一个URL",
"batchLinkInputTip": "每行一个链接",
"batchPreviewTip": "将添加 {count} 项,跳过 {skipped} 项已存在",
"lineIssueTip": "第 {line} 行:{message}",
"singleValueTip": "{label}只能是一项",
"deleteTip": "确定删除当前{label}吗?",
"deleteMultipTip": "确定删除选中的{label}吗?",
"nullTip": "暂无{label}",
@@ -378,6 +483,7 @@
"import": "导入",
"importFile": "通过文件导入",
"importUrl": "通过URL导入",
"startFromScratch": "从零开始",
"autoSetSystemDns": "自动设置系统DNS",
"details": "{label}详情",
"creationTime": "创建时间",
@@ -396,36 +502,44 @@
"request": "请求",
"connected": "已连接",
"disconnected": "已断开",
"connecting": "连接中...",
"connecting": "连接中…",
"restartCoreTip": "您确定要重启核心吗?",
"forceRestartCoreTip": "您确定要强制重启核心吗?",
"dnsHijacking": "DNS劫持",
"coreStatus": "核心状态",
"dataCollectionTip": "数据收集说明",
"dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情,不包含个人敏感数据。\n您可以在设置中关闭此功能。",
"crashlytics": "崩溃分析",
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
"appendSystemDns": "追加系统DNS",
"appendSystemDnsTip": "强制为配置附加系统DNS",
"editRule": "编辑规则",
"overrideMode": "覆写模式",
"standardModeDesc": "标准模式,覆写基础配置,提供简单追加规则能力",
"scriptModeDesc": "脚本模式,使用外部扩展脚本,提供一键覆写配置的能力",
"extendMode": "扩展模式",
"standardModeDesc": "标准模式,在配置的基础上附加规则",
"scriptModeDesc": "脚本模式,使用扩展脚本改写整个配置",
"addedRules": "附加规则",
"controlGlobalAddedRules": "控制全局附加规则",
"overrideScript": "覆写脚本",
"quickActions": "快捷操作",
"rulePresetBlockQuic": "屏蔽 QUIC",
"rulePresetBlockStun": "屏蔽 STUN",
"rulePresetBlockDot": "屏蔽 DNS over TLS",
"rulePresetBlockLoopback": "防止代理回环",
"rulePresetSystemServicesDirect": "Apple 与 Microsoft 直连",
"rulePresetBittorrentDirect": "BT 下载直连",
"rulePresetLanDirect": "局域网直连",
"extendScript": "扩展脚本",
"goToConfigureScript": "前往配置脚本",
"goToConfigureAddedRules": "前往配置附加规则",
"editGlobalRules": "编辑全局规则",
"externalFetch": "外部获取",
"confirmForceCrashCore": "确定要强制崩溃核心?",
"confirmClearAllData": "确定要清除所有数据?",
"loading": "加载中...",
"loading": "加载中…",
"yearsAgo": "{count} 年前",
"monthsAgo": "{count} 个月前",
"daysAgo": "{count} 天前",
"hoursAgo": "{count} 小时前",
"minutesAgo": "{count} 分钟前",
"justNow": "刚刚",
"yearsAgoShort": "{count} 年前",
"monthsAgoShort": "{count} 个月前",
"daysAgoShort": "{count} 天前",
"hoursAgoShort": "{count} 小时前",
"minutesAgoShort": "{count} 分钟前",
"noLongerRemind": "不再提示",
"accessControlSettings": "访问控制设置",
"turnOn": "开启",
@@ -434,39 +548,45 @@
"restart": "重启",
"speedStatistics": "网速统计",
"resetPageChangesTip": "当前页面存在更改,确定重置吗?",
"overwriteTypeCustom": "自定义",
"overwriteTypeCustomDesc": "自定义模式,支持完全自定义修改代理组以及规则",
"customProfile": "自定义",
"customProfileDesc": "基于应用代理和其他配置组建策略组与规则",
"customProfileResources": "自定义配置资源",
"customProfileResourcesDesc": "自定义配置资源包含代理、规则集、筛选与图标集。",
"unknownNetworkError": "未知网络错误",
"restoreException": "恢复异常",
"networkException": "网络异常,请检查连接后重试",
"backupFromNewerVersion": "该备份来自更高版本的应用,请先更新应用再恢复",
"networkTimeoutError": "请求超时,请检查网络或代理后重试",
"networkHostLookupError": "无法解析服务器地址,请确认链接正确且 DNS 可用",
"networkConnectionError": "无法连接到服务器,请检查网络连接或代理设置",
"networkTlsError": "安全连接失败,服务器证书可能无效,或连接被拦截",
"networkCancelledError": "请求已取消",
"networkAccessDeniedError": "服务器拒绝访问(HTTP {code}),链接可能已过期或凭据有误",
"networkNotFoundError": "该地址下没有内容(HTTP {code}),请确认链接是否正确",
"networkRateLimitedError": "请求过于频繁(HTTP 429),请稍后再试",
"networkServerError": "服务器出现问题(HTTP {code}),请稍后再试",
"networkBadResponseError": "服务器拒绝了请求(HTTP {code})",
"networkRequestFailed": "网络请求失败:{detail}",
"invalidBackupFile": "无效备份文件",
"pruneCache": "修剪缓存",
"backupAndRestore": "备份与恢复",
"backupAndRestoreDesc": "通过WebDAV或者文件同步数据",
"restore": "恢复",
"restoreSuccess": "恢复成功",
"restoreFromWebDAVDesc": "通过WebDAV恢复数据",
"restoreFromFileDesc": "通过文件恢复数据",
"restoreOnlyConfig": "仅恢复配置文件",
"restoreAllData": "恢复所有数据",
"addProfile": "添加配置",
"delayTest": "延迟测试",
"proxyGroupEmpty": "策略组为空",
"proxyGroupNameEmpty": "策略组名称不能为空",
"proxyGroupNameDuplicate": "策略组名称重复",
"confirmExitWindow": "确定要退出当前窗口吗?",
"dataChangedSave": "检测到数据有更改,是否保存",
"selectProxyProviders": "选择代理集",
"proxyFilter": "节点过滤器",
"confirmExitWindow": "确定要退出当前窗口吗?",
"optional": "可选",
"maxFailedTimes": "最大失败次数",
"testInterval": "测试间隔",
"excludeProxyFilter": "排除节点过滤器",
"excludeType": "排除类型",
"excludeFilter": "排除筛选",
"expectedStatus": "预期状态",
"selectProxies": "选择代理",
"strategy": "策略",
"tolerance": "容差",
"inputProxyGroupName": "输入策略组名称",
"helperCorruptTip": "Helper 服务不可用,无法启用 TUN 模式,请重新安装 FlClash。",
"tunAuthorizationFailed": "虚拟网卡授权失败,已关闭。",
"sudoPasswordTitle": "输入 sudo 密码",
"coreBlockedByPolicyTip": "Windows 拒绝运行 FlClashCore.exe(错误 {code})。智能应用控制、AppLocker 等应用控制策略会拦截未签名程序,请在该策略中放行 FlClash 或关闭策略后重试。",
"coreBlockedBySmartAppControlTip": "Windows 智能应用控制拦截了未签名的 FlClashCore.exe。请打开 Windows 安全中心 → 应用和浏览器控制 → 智能应用控制设置,选择「关闭」后重新启动 FlClash。智能应用控制关闭后无法再开启,除非重装 Windows。",
"hideFromList": "从列表中隐藏",
@@ -479,7 +599,6 @@
"selectRuleSet": "请选择规则集",
"splitStrategy": "分流策略",
"selectSplitStrategy": "请选择分流策略",
"selectSubRule": "请选择子规则",
"noResolveHostname": "不解析主机名",
"matchSourceIp": "匹配来源IP",
"basicInfo": "基础信息",
@@ -487,21 +606,11 @@
"proxyType": "代理类型",
"basicStrategy": "基础策略",
"editProxy": "编辑代理",
"includeAllProxyProviders": "包含所有代理集",
"includeAllProxyProvidersTip": "开启后将覆盖引入的代理集",
"addProxyProviders": "添加代理集",
"includeAllProxies": "包含所有代理",
"includeAllProxiesTip": "引入不包含策略组的所有代理,可在下方额外添加策略组",
"proxiesEmpty": "代理为空",
"addProxies": "添加代理",
"addNodes": "添加节点",
"addProxyGroup": "添加策略组",
"editProxyGroup": "编辑策略组",
"confirmOverwriteTip": "确定后将会覆盖已有数据",
"configDataDetected": "检测到配置中存在数据",
"quickFill": "一键填入",
"iconUrl": "图标链接",
"iconRecords": "图标记录",
"noRecords": "暂无记录",
"custom": "自定义",
"ruleActionDomainDesc": "匹配完整域名",
"ruleActionDomainSuffixDesc": "匹配域名后缀",
@@ -510,7 +619,7 @@
"ruleActionDomainWildcardDesc": "通配符匹配,仅支持*和?通配符",
"ruleActionGeositeDesc": "匹配 Geosite 内的域名",
"ruleActionIpCidrDesc": "匹配 IP 地址范围",
"ruleActionIpCidr6Desc": "匹配 IP 地址范围, IP-CIDR6 只是一个别名",
"ruleActionIpCidr6Desc": "匹配 IP 地址范围,IP-CIDR6 只是一个别名",
"ruleActionIpSuffixDesc": "匹配 IP 后缀范围",
"ruleActionIpAsnDesc": "匹配 IP 所属 ASN",
"ruleActionGeoipDesc": "匹配 IP 所属国家代码",
@@ -533,60 +642,58 @@
"ruleActionProcessNameWildcardDesc": "使用进程名称通配符匹配,仅支持*和?通配符",
"ruleActionUidDesc": "匹配 Linux USER ID",
"ruleActionNetworkDesc": "匹配TCP或者UDP",
"ruleActionDscpDesc": "匹配DSCP标记 (仅限 tproxy udp 入站)",
"ruleActionDscpDesc": "匹配DSCP标记(仅限 tproxy udp 入站)",
"ruleActionRuleSetDesc": "引用规则集合,需配置rule-providers",
"ruleActionAndDesc": "逻辑规则 AND",
"ruleActionOrDesc": "逻辑规则 OR",
"ruleActionNotDesc": "逻辑规则 NOT",
"ruleActionSubRuleDesc": "匹配至子规则,需要注意括号的使用",
"ruleActionSubRuleDesc": "匹配至子规则,需要注意括号的使用",
"ruleActionMatchDesc": "匹配所有请求,无需条件",
"subRuleEmpty": "子规则为空",
"proxyProvidersNotEmpty": "代理集不能为空",
"contentNotEmpty": "内容不能为空",
"subRuleNotEmpty": "子规则不能为空",
"splitStrategyNotEmpty": "分流策略不能为空",
"proxyProvidersEmpty": "代理集为空",
"timeout": "超时",
"delayFailed": "失败",
"invalidSubRule": "{subRule} 是一个无效的SUB_RULE",
"invalidPolicy": "{target} 是一个无效的策略",
"invalidProxyProvider": "{providerName} 是一个无效的代理集",
"invalidRuleSet": "{ruleSet} 不是有效的规则集",
"invalidNetworkContent": "仅支持 tcp 或 udp",
"invalidRangeContent": "请输入数字或范围,如 80 或 8000-9000,多个用 / 分隔",
"invalidDscpContent": "DSCP 标记不能超过 63",
"invalidCidrContent": "请输入 CIDR 格式的 IP 段,如 192.168.0.0/16",
"invalidPortRangeContent": "请输入端口或范围,如 443 或 8000-9000",
"invalidHostContent": "请输入域名或 IP 地址",
"invalidListenContent": "请输入地址和端口,如 0.0.0.0:1053",
"invalidProxy": "{proxyName} 是一个无效的代理",
"proxyGroupDetectedAbnormal": "检测到当前策略组异常",
"proxyProviderDetectedAbnormal": "检测到选中的代理集存在异常",
"proxyDetectedAbnormal": "检测到选中的代理存在异常",
"createProfile": "创建配置",
"locationPermissionRequired": "需要位置权限",
"locationPermissionGuide": "1. 打开 系统设置 > 隐私与安全性\n2. 选择 定位服务\n3. 在右侧列表中找到并勾选 {appName}\n\n完成设置后,返回应用即可正常使用。感谢您的配合。",
"prerequisites": "前置条件",
"ignoreBatteryOptimization": "忽略电池优化",
"batteryOptimizationDesc": "为保证后台运行,请关闭本应用的电池优化。点击前往设置。",
"batteryOptimizationStatusTip": "由于系统限制,运行状态下无法正确获取电池优化状态",
"batteryOptimizationDesc": "忽略电池优化可保证应用在后台运行。",
"locationPermission": "位置权限",
"locationPermissionDesc": "根据系统要求,获取Wi-Fi名称需要您授予位置权限。Android 上请选择“始终允许”,否则应用在后台时无法获取 Wi-Fi 名称。",
"excludeSsids": "排除SSIDs",
"excludeSsidsDesc": "连接到被排除SSID的WIFI时,将会自动切换应用运行状态",
"excludeSsidsDesc": "连接到被排除 SSID 的 Wi-Fi 时,将会自动切换应用运行状态。",
"ssidsEmpty": "SSIDs为空",
"onDemand": "按需运行",
"onDemandDesc": "配置程序特定场景运行状态",
"onDemandDesc": "按需运行会在连上排除的 Wi-Fi 时暂停代理。",
"locationPermissionDeniedMessage": "位置权限已被拒绝,无法获取当前 Wi-Fi 名称。请前往系统设置手动开启位置权限。",
"addSsid": "添加SSID",
"editSsid": "编辑SSID",
"authorized": "已授权",
"tapToAuthorize": "点击授权",
"suspended": "挂起中...",
"geoOptions": "Geo 选项",
"geoAutoUpdate": "自动更新",
"geoAutoUpdateInterval": "自动更新间隔",
"geoAutoUpdateIntervalTip": "自动更新间隔必须大于0",
"suspended": "挂起中…",
"resourceUpdateInterval": "自动更新间隔",
"resourceUpdateIntervalTip": "自动更新间隔必须大于0",
"hours": "小时",
"hoursCount": "{count} 小时",
"geoResources": "Geo 资源",
"minutes": "分钟",
"geoResources": "Geo",
"geoSkipped": "{name} 已是最新版本",
"geoUpdated": "{name} 已更新",
"secondsCount": "{count} 秒",
"entriesCount": "{count} 个条目",
"proxiesCount": "{count} 个代理",
"rulesCount": "{count} 条规则",
"proxiesCount": "{count, plural, other{{label} 个代理}}",
"rulesCount": "{count, plural, other{{label} 条规则}}",
"proxyGroupsCount": "{count, plural, other{{label} 个策略组}}",
"changelogBreaking": "重大变更",
"changelogFeatures": "新功能",
"changelogFixes": "问题修复",
@@ -600,7 +707,6 @@
"pinWindow": "窗口置顶",
"unpinWindow": "取消置顶",
"exitFullScreen": "退出全屏",
"toggleLabel": "切换标签",
"torch": "手电筒",
"pickFromAlbum": "从相册选择",
"blockConnection": "阻止连接",
@@ -611,11 +717,227 @@
"previousMatch": "上一个匹配",
"nextMatch": "下一个匹配",
"clearSearch": "清除搜索",
"addWidget": "添加组件",
"noSearchResults": "没有匹配的结果",
"showPassword": "显示密码",
"hidePassword": "隐藏密码",
"authorize": "授权",
"installedAppsPermissionRequired": "需要读取应用列表权限",
"installedAppsPermissionDesc": "当前系统在授权前不会提供已安装的应用列表,授权后即可配置分应用代理。",
"installedAppsPermissionDeniedMessage": "读取应用列表权限已被拒绝,无法获取已安装的应用。请前往系统设置手动开启。"
"installedAppsPermissionDeniedMessage": "读取应用列表权限已被拒绝,无法获取已安装的应用。请前往系统设置手动开启。",
"outboundIp": "出站 IP",
"ipAddress": "IP 地址",
"ipQualityLevel": "等级",
"ipQualityGood": "优",
"ipQualityNormal": "普通",
"ipQualityRisky": "风险",
"ipType": "类型",
"ipTypeResidential": "住宅",
"ipTypeMobile": "移动网络",
"ipTypeBusiness": "商业",
"ipTypeHosting": "机房",
"ipFlags": "命中标记",
"ipFlagProxy": "代理",
"ipFlagVpn": "VPN",
"ipFlagTor": "Tor",
"ipFlagAbuser": "滥用记录",
"ipOrganization": "组织",
"ipAsn": "ASN",
"ipQualitySource": "采用来源",
"ipQualitySources": "各来源",
"ipQualityFailed": "类型查询失败",
"ipQualityRetry": "重新查询",
"ipSourceNoType": "无法判定类型",
"ipSourceRateLimited": "限流",
"ipSourceIpMismatch": "出站 IP 不一致",
"hideIp": "隐藏 IP",
"editorUnavailable": "编辑器不可用",
"replace": "替换",
"replaceAll": "全部替换",
"cameraPermissionRequired": "需要相机权限",
"cameraPermissionDesc": "请在系统设置中允许访问相机以扫描二维码,或从相册选择二维码图片。",
"cameraUnavailable": "相机不可用",
"retry": "重试",
"invalidProfileQrcode": "该二维码不包含配置文件链接",
"dnsQueries": "DNS查询",
"recordType": "记录类型",
"answers": "应答",
"responseCode": "响应码",
"error": "错误",
"initiator": "发起方",
"cache": "缓存",
"yes": "是",
"no": "否",
"nonTextProviderFile": "该外部资源不是文本文件",
"addCustomProxy": "添加代理",
"definitionNotMap": "配置必须是包含 name 和 type 的 YAML 映射",
"ruleTextInvalid": "规则必须以受支持的类型开头,例如 DOMAIN,example.com,DIRECT",
"ruleListInvalid": "规则必须是 YAML 列表,每项一条规则,例如 - DOMAIN,example.com,DIRECT",
"shareLinksInvalid": "无法从链接中读取代理",
"shareLinkUnreadable": "无法从 {link} 中读取代理",
"proxiesProfileLabel": "{name} 等 {count} 个代理",
"singleShareLinkOnly": "这里只能填写一条链接,多条请在列表的快速编辑中填写",
"username": "用户名",
"emptyFallback": "空组兜底",
"defaultSelected": "默认选中",
"hashByInUser": "按入站用户哈希",
"appProxiesEmpty": "暂无本地代理",
"nodes": "节点",
"localProxies": "本地代理",
"customIssueEmptyName": "名称为空",
"customIssueReservedName": "{name} 是内置策略名,不能在此使用",
"customIssueDuplicateName": "名称 {name} 已被其他代理或策略组使用",
"customIssueCoreRejected": "内核无法解析该代理:{message}",
"customIssueMissingProxies": "以下代理或策略不存在:{names}",
"customIssueMissingProviders": "以下代理集不存在:{names}",
"customIssueNoProxySource": "未选择任何代理或代理集,内核会拒绝该策略组",
"customIssueGroupLoop": "策略组之间存在循环引用:{path}",
"customIssueInvalidEmptyFallback": "空组兜底只能是代理,{name} 不存在或是策略组",
"customIssueInvalidFilter": "{name}不是有效的正则:{message}",
"customIssueMissingDialer": "拨号代理 {name} 不存在",
"customIssueDialerLoop": "{proxy} 经 {target} 拨号,而 {target} 又会回到 {proxy},形成环路",
"dialerProxyLoopStopped": "经 {name} 拨号会绕回使用它的代理,连接已中止",
"customIssuesSummary": "{count} 项存在问题,应用配置可能失败",
"providerInUse": "{label} 仍被 {profiles} 使用,删除后这些配置会异常。确定删除吗?",
"proxyGroupInUse": "{label} 仍被当前配置的规则、策略组、拨号代理、DNS 或 NTP 引用,删除后这些引用会失效。确定删除吗?",
"proxiesRemovedTip": "以下代理将被删除:{removed}。改了名字的行按新代理处理,自定义配置中对被删除代理的引用会标为缺失。确定应用吗?",
"proxiesReplacedTip": "以下代理将被删除:{removed}。以下代理将新增:{added}。改了名字的行按新代理处理,自定义配置中对被删除代理的引用会标为缺失。确定应用吗?",
"fontSize": "大小",
"large": "大",
"extraLarge": "超大",
"switchProfile": "切换配置",
"lastUpdated": "上次更新",
"filters": "筛选",
"label": "标签",
"regex": "正则",
"matchCase": "区分大小写",
"iconSets": "图标集",
"invalidIconSet": "不是有效的图标集",
"recommendedIcons": "推荐",
"localImage": "本地图片",
"iconSetsEmptyTip": "暂无图标集,可在「自定义配置资源 → 图标集」中添加",
"outbound": "出站",
"strictRoute": "严格路由",
"congestionController": "TCP 拥塞控制",
"congestionControllerDesc": "TCP 拥塞控制仅在 mips 栈生效。",
"routeExcludeAddress": "排除路由地址",
"keepAliveIdle": "TCP 保持活动空闲时间",
"disableKeepAlive": "禁用 TCP 保持活动",
"routingMark": "路由标记",
"strictRouteDesc": "严格路由防止 DNS 等流量绕过虚拟网卡,开启后其他设备将无法访问本机。",
"initFailed": "启动失败",
"initFailedTip": "FlClash 启动时出错,无法继续运行。可以复制错误详情用于反馈;如果问题反复出现,清除数据后通常可以重新启动。",
"errorDetails": "错误详情",
"stackTrace": "堆栈信息",
"clearDataAndExitTip": "将删除所有配置、设置和本地数据,随后关闭应用。重新打开即可从头开始。",
"clearDataFailed": "清除数据失败:{detail}",
"bypassPrivateRoute": "绕过私有路由地址",
"bypassPrivateRouteDesc": "开启绕过私有路由地址后,局域网、回环和组播地址不经过虚拟网卡。",
"disableIcmpForwarding": "禁用ICMP转发",
"disableIcmpForwardingDesc": "禁用 ICMP 转发后由虚拟网卡直接应答 ping,ping 将不再显示真实延迟。",
"excludeInterface": "排除网卡",
"excludeInterfaceDesc": "从这些网卡(如 docker0)进入的流量不经过虚拟网卡。",
"includeInterface": "包含网卡",
"includeInterfaceDesc": "只有从包含网卡进入的流量经过虚拟网卡,设置后排除网卡不再生效。",
"includeUid": "包含用户 ID",
"includeUidDesc": "只有这些用户 ID 的进程产生的流量经过虚拟网卡。",
"excludeUid": "排除用户 ID",
"excludeUidDesc": "这些用户 ID 的进程产生的流量不经过虚拟网卡。",
"excludeSrcPort": "排除源端口",
"excludeSrcPortDesc": "源端口为这些端口的流量不经过虚拟网卡。",
"excludeDstPort": "排除目标端口",
"excludeDstPortDesc": "发往这些目标端口的流量不经过虚拟网卡。",
"udpTimeout": "UDP 超时",
"udpTimeoutDesc": "UDP 超时以秒为单位,仅在 gvisor、system 和 mixed 栈生效。",
"dnsHijackDesc": "未添加时劫持所有发往 53 端口的 DNS 请求(any:53)。",
"tunOverrideDesc": "添加的项会覆写所有配置的虚拟网卡设置,未添加的项沿用配置自身的设置。自定义配置自己添加的项优先。",
"profileTunSettingsDesc": "仅作用于当前配置,并优先于网络设置中的虚拟网卡覆写项。",
"settingsCount": "{count} 项设置",
"sectionsInEffect": "{count} 项生效",
"itemsCount": "{count} 项",
"additionalPrefix": "名称前缀",
"additionalSuffix": "名称后缀",
"skipCertVerify": "跳过证书验证",
"ipVersion": "IP 版本",
"addFilters": "添加筛选",
"failedItem": "{label}:{message}",
"failedCount": "{count} 项失败",
"importConfigInvalid": "此文件不是可导入的配置",
"importConfigReplaceTip": "导入会替换此配置现有的策略组、规则和设置",
"networkName": "网络名称",
"networkSecret": "网络密码",
"peers": "初始节点",
"virtualIpv4": "虚拟 IPv4",
"hostname": "主机名",
"exitNodes": "出口节点",
"cipher": "加密方式",
"protocol": "协议",
"protocolParam": "协议参数",
"obfs": "混淆",
"obfsParam": "混淆参数",
"obfsPassword": "混淆密码",
"obfsMode": "混淆模式",
"obfsHost": "混淆主机",
"plugin": "插件",
"pluginMode": "插件模式",
"pluginHost": "插件主机",
"privateKey": "私钥",
"publicKey": "公钥",
"privateKeyPassphrase": "私钥密码",
"preSharedKey": "预共享密钥",
"secretKey": "密钥",
"transport": "传输协议",
"uploadBandwidth": "上行带宽",
"downloadBandwidth": "下行带宽",
"protocolVersion": "协议版本",
"portHopping": "端口跳跃",
"hopInterval": "跳跃间隔",
"congestionControl": "拥塞控制",
"udpRelayMode": "UDP 中继模式",
"allowedIps": "允许的 IP",
"remoteDnsResolve": "远程 DNS 解析",
"clientFingerprint": "客户端指纹",
"required": "必填",
"ageSecretKey": "age 私钥",
"ageEncryptedProfile": "配置已加密",
"ageSecretKeyRequiredTip": "该配置已用 age 加密,需要 age 私钥才能解密",
"currentOutboundDesc": "含启动键,不可移除",
"running": "运行中",
"notRunning": "未运行",
"trafficPeak": "峰值 {value}",
"trafficTotal": "累计 {value}",
"ipQualityShort": "质量",
"ipTypeUnknown": "类型未知",
"tapIconToRetry": "点击图标重试",
"networkTakeover": "网络接管",
"portValue": "端口 {port}",
"serviceAbnormalCount": "{count} 项异常",
"serviceAllAvailable": "全部可用",
"serviceShownOnDashboard": "显示在仪表盘",
"groupTimeoutCount": "{count} 超时",
"groupCountUnit": "个",
"groupCount": "{count} 个",
"proxyGroupsAfterStart": "启动后在这里显示策略组",
"usedShort": "已用",
"profileUpdatedAt": "更新于 {time}",
"profileExpiresIn": "{date} 到期,剩余 {days} 天",
"profileDaysLeft": "剩余 {days} 天",
"target": "目标",
"outboundExit": "出口",
"duration": "时长",
"traffic": "流量",
"totalDownload": "下载总量",
"totalUpload": "上传总量",
"initiatorShort": "发起",
"result": "结果",
"elapsed": "耗时",
"connectionStats": "连接统计",
"connectionStatsDesc": "实时连接、最近请求、DNS查询",
"memoryInfoDesc": "应用与内核占用的内存",
"editDashboard": "编辑仪表盘",
"done": "完成",
"dashboardAddableSection": "可添加",
"customImportDroppedSubRules": "{count, plural, other{自定义配置不支持子规则,已丢弃 {label} 条 SUB-RULE 规则}}",
"autoInclude": "自动包含",
"allReferencedProxies": "全部被引用的代理",
"allReferencedProxyProviders": "全部被引用的代理集"
}
Binary file not shown.
-9
View File
@@ -1,9 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="69" y="69" width="70" height="70" rx="22" fill="#FDF7FF" stroke="#C4C7C5" stroke-width="3"/>
<rect x="161" y="69" width="70" height="70" rx="35" fill="#6750A4"/>
<rect x="69" y="161" width="70" height="70" rx="35" fill="#6750A4" opacity="0.58"/>
<path d="M174 161H218C225.18 161 231 166.82 231 174V218C231 225.18 225.18 231 218 231H174C166.82 231 161 225.18 161 218V174C161 166.82 166.82 161 174 161Z" fill="#FDF7FF" stroke="#C4C7C5" stroke-width="3"/>
<path d="M90 104H118" stroke="#6750A4" stroke-width="8" stroke-linecap="round"/>
<path d="M104 90V118" stroke="#6750A4" stroke-width="8" stroke-linecap="round"/>
<path d="M181 196H211" stroke="#6750A4" stroke-width="8" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 827 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M122 150H178" stroke="#6750A4" stroke-width="14" stroke-linecap="round"/>
<circle cx="92" cy="150" r="34" fill="#FDF7FF" stroke="#6750A4" stroke-width="9"/>
<circle cx="208" cy="150" r="34" fill="#6750A4"/>
</svg>

Before

Width:  |  Height:  |  Size: 329 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M82 108C82 90.33 112.45 76 150 76C187.55 76 218 90.33 218 108V197C218 214.67 187.55 229 150 229C112.45 229 82 214.67 82 197V108Z" fill="#6750A4"/>
<ellipse cx="150" cy="108" rx="52" ry="17" fill="#FDF7FF"/>
<path d="M105 161C128 170 172 170 195 161" stroke="#FDF7FF" stroke-width="9" stroke-linecap="round" opacity="0.72"/>
</svg>

Before

Width:  |  Height:  |  Size: 446 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="150" cy="150" r="78" fill="#FDF7FF" stroke="#6750A4" stroke-width="12"/>
<path d="M150 105V154L183 177" stroke="#6750A4" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 311 B

-10
View File
@@ -1,10 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="57" y="72" width="186" height="156" rx="28" fill="#FDF7FF" stroke="#C4C7C5" stroke-width="3"/>
<path d="M57 100C57 84.54 69.54 72 85 72H215C230.46 72 243 84.54 243 100V112H57V100Z" fill="#E8DEF8"/>
<circle cx="84" cy="92" r="6" fill="#6750A4"/>
<circle cx="104" cy="92" r="6" fill="#6750A4" opacity="0.55"/>
<path d="M88 150L108 166L88 182" stroke="#6750A4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M126 183H169" stroke="#6750A4" stroke-width="10" stroke-linecap="round"/>
<circle cx="215" cy="204" r="34" fill="#6750A4" stroke="#FDF7FF" stroke-width="6"/>
<path d="M215 186V204L226 214" stroke="#FDF7FF" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 838 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="91" y="126" width="118" height="92" rx="28" fill="#6750A4"/>
<path d="M116 126V111C116 92 131 77 150 77C169 77 184 92 184 111V126" stroke="#6750A4" stroke-width="15" stroke-linecap="round"/>
<circle cx="150" cy="169" r="10" fill="#FDF7FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 363 B

-14
View File
@@ -1,14 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M82 58H182L218 94V234C218 244 210 252 200 252H82C72 252 64 244 64 234V76C64 66 72 58 82 58Z" fill="#FDF7FF" stroke="#C4C7C5" stroke-width="3" stroke-linejoin="round"/>
<path d="M182 58V94H218" fill="#E8DEF8"/>
<path d="M182 58V94H218" stroke="#C4C7C5" stroke-width="3" stroke-linejoin="round"/>
<path d="M94 124H188" stroke="#6750A4" stroke-width="8" stroke-linecap="round" opacity="0.55"/>
<circle cx="126" cy="124" r="12" fill="#6750A4"/>
<path d="M94 164H188" stroke="#6750A4" stroke-width="8" stroke-linecap="round" opacity="0.55"/>
<circle cx="166" cy="164" r="12" fill="#6750A4"/>
<path d="M94 204H188" stroke="#6750A4" stroke-width="8" stroke-linecap="round" opacity="0.55"/>
<circle cx="142" cy="204" r="12" fill="#6750A4"/>
<circle cx="213" cy="218" r="33" fill="#6750A4" stroke="#FDF7FF" stroke-width="6"/>
<path d="M193 220C204 207 222 207 233 220" stroke="#FDF7FF" stroke-width="7" stroke-linecap="round"/>
<circle cx="213" cy="204" r="8" fill="#FDF7FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-6
View File
@@ -1,6 +0,0 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M150 205V153M150 153L96 99M150 153L204 99" stroke="#6750A4" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="96" cy="99" r="27" fill="#FDF7FF" stroke="#C4C7C5" stroke-width="3"/>
<circle cx="204" cy="99" r="27" fill="#FDF7FF" stroke="#C4C7C5" stroke-width="3"/>
<circle cx="150" cy="205" r="30" fill="#6750A4"/>
</svg>

Before

Width:  |  Height:  |  Size: 466 B

Some files were not shown because too many files have changed in this diff Show More