Skip to content

Routing: built-in rules, App-based routing section, region chip - #2365

Merged
hiddify-com merged 147 commits into
hiddify:mainfrom
veto9292:feat/preset-rules
Oct 6, 2026
Merged

hiddify-com merged 147 commits into
hiddify:mainfrom
veto9292:feat/preset-rules

Conversation

@veto9292

@veto9292 veto9292 commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Reworks Settings → Routing: built-in rules, an App-based routing section (formerly Per-app proxy, Android only), and the general options moved out of the bottom panel.

Depends on #2333. This branch is built on it. Its own commits start after 8aaeb368; the diff shrinks to them once #2333 is merged. main is merged in, so it has no conflicts.

Rules

  • The predefined rules sheet is replaced by three built-in rules that are always in the list: Bypass local network, Block ads and harmful sites, and Bypass sites in the region. They can only be switched on or off and moved. The region rule follows the region, and is left out while the region is Other.
  • A new rule goes to the top and gets a numbered name.
  • Package names: an app picker on Android, which marks the apps App-based routing keeps outside the VPN.
  • IP CIDR fields accept IPv6.

App-based routing (Android)

  • A section above the rules: a switch, the two modes (Bypass, Proxy only), a row of app logos that opens the list, and an auto selection row with its status.
  • The switch is saved apart from the mode (per_app_proxy_enabled). Preferences migration v2 moves the old off value. Android reads the new key, and falls back to the old rule when the Quick Settings tile or a reboot starts the VPN before the migration has run.
  • Auto selection runs when App-based routing is turned on or its mode changes, until the user turns it off by hand. A region change turns it off.
  • App list page: titled by the mode; chips for Hide system apps and Restore auto picks (with Undo); an Auto tag; search by package name; a bar that offers to share the list's changes, with a preview sheet before opening a prefilled GitHub issue.
  • Fixes: a list that loads after a region or mode change is dropped; a failed list after a mode change leaves auto selection off; importing replaces the lists, as its dialog says.

General options

  • The bottom panel and its toggle are gone. Region is a chip in the app bar. Balancer strategy, Resolve destination and IPv6 route head the rules list.

Toasts

  • Flat style; 4 s, or 8 s with an action button such as Undo.

Notes

  • The region rule's rule-set URLs contain {region}, which the core still has to replace.
  • Share opens an issue on hiddify/Android-GFW-Apps, where issues are currently turned off.

Testing

Android emulator, API 36, upgraded over a saved exclude mode:

  • the migration;
  • the switch, both modes, and auto selection with Iran's Bypass and Proxy only lists;
  • the region chip with China, Other and Iran;
  • the option menus and the rules menu;
  • the app list: restore with Undo, and the share sheet (not submitted);
  • a new rule and the package names picker.

After merging main: dart analyze is clean, and flutter test passes (150 tests).

🤖 Generated with Claude Code

veto9292 and others added 30 commits July 6, 2026 12:30
Update the hiddify-core submodule to db74dfc (main HEAD), which moves
hiddify-sing-box onto the testing-merge-base line (AWG/amnezia option
API, public RedirectStderr, removed broken client submodules).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Return a CupertinoPage from customTransition() on iOS so pushed pages get
the native interactive edge-swipe (pop) gesture, which CustomTransitionPage
does not support. Other platforms keep the existing slide/fade transitions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the success heavy-impact from the Connected() transition to the
moment the tunnel has a valid url-test ping, so it fires when the
connection actually works. Add a medium then heavy buzz when a live
connection times out, and a heavy buzz when it recovers.

Extract the delay-validity threshold into ConnectionConst.maxDelay /
isValidDelay as the single source of truth, and route the connection
button, system tray, proxy tile, and delay indicator through it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the leading action icon with a live status: a spinning indicator while updating, then a transient success/failure icon (cross-faded). The result is held for 2s inside UpdateProfileNotifier so it survives list rebuilds/reordering. Also tidy ProfileTile (extract body/title/tap helpers, drop commented-out code and the unused color param) and remove the never-used ProfileTileMain widget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route bulk updates through UpdateProfileNotifier.updateProfile so each tile shows its status, and report one aggregate toast (N success, M failed with names) instead of one per profile. Single user-initiated updates no longer toast on success. Toast titles now wrap to multiple lines and error toasts stay longer to fit the failed names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Key profile tiles by id so their update indicator and animation aren't lost when the list reorders (it sorts by last-update, so an updated profile jumps position).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix swapped sort labels, replace the ambiguous up/down arrow with explicit direction labels (A -> Z / Z -> A, Newest/Oldest first) shown as a subtitle, and default to the natural direction per sort type. Persist the sort choice via a new general preference so it survives app restarts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add translations for bulkSuccess, bulkPartial and the sort direction labels (newestFirst/oldestFirst) across all locales, matching the keys added to en.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove pages.profiles.msg.update.successNamed (single updates no longer toast on success) and components.subscriptionInfo.{remainingTime, remainingDurationNew, remainingUsage, profileSite, profileSupport} (only used by the deleted New*SubscriptionInfo widgets) from en and all locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
English base-locale strings for the profile pin/unpin action. Kept as its own commit (source locale); other languages follow separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a pinned column (DB schema v7 + migration; drift steps/schema/test helpers regenerated) plus a README documenting the migration workflow. The profiles list now orders active -> pinned -> sorted; Pin/Unpin is the first profile-menu item and pinned list tiles show a pin badge. Pin state persists and a subscription refresh no longer clobbers it (toUpdateEntry omits pinned).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translate common.pin / common.unpin into all non-English locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the aider-based translation helper and migrate its workflow into
Claude Code project instructions.

- delete .aider.conf.yml and SLANG.md
- remove aider history/cache entries from .gitignore
- add CLAUDE.md documenting the slang translation workflow
- git-ignore slang analyze reports (_missing/_unused_translations*.json)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `?url=` deep-link form documented as
`hiddify://install-config?url=<uri>#<name>` never picked up the name:
the parser only read a `name=` query parameter and ignored the fragment.
Prefer an explicit `name=` and fall back to the fragment.

Adds tests covering both deep-link forms and the other panel schemes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a `showLeading` flag to LanSharingPreferenceWidget and turns it off
in the quick settings modal, where the leading icon is redundant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switches the profiles modal from a Column to a Stack so the buttons float
above the list instead of taking their own row. Bottom list padding is
increased to keep the last profile reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Strings for the profile web page / support chips and for the warning shown
when a subscription-provided link is not in the trusted list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`profile-web-page-url` and `support-url` were stored on SubscriptionInfo,
so they were only kept when the `subscription-userinfo` header was also
present. They are independent headers, so they move onto RemoteProfileEntity
and are now parsed unconditionally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scammers publish look-alike channels and sites (e.g. t.me/Hiddiify vs the
real t.me/hiddify) inside fake subscriptions to collect donations meant for
real providers. This adds the list used to tell a verified provider link from
an unverified one.

Matching is exact on the whole url, after normalising the scheme, a leading
`www.`, host case, a trailing slash and any fragment — so a look-alike handle
never matches. The list is managed by the project manager and holds Hiddify
only for now; there is no public request flow to be added to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reworks the unverified-link dialog so it informs rather than alarms: an
unverified link is not necessarily malicious, so it drops the icon and the
error styling and states plainly that the link cannot be verified.

The Open button stays disabled for 10 seconds with a visible countdown, the
same friction used for sensitive permissions, so the note is actually read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renders the provider's web page and support links as a single chip below the
main profile card. With both present the chip cycles every 10s: it slides up
behind the card, then the next one slides down into place. The card's bottom
corners square off so the chip reads as attached to it.

A tapped link is checked against the trusted list first: a trusted link opens
after a plain confirmation, anything else goes through the countdown warning.

Hidden behind ProfileTileConst.showProviderLinks while the design is decided.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header was split on `;` and every segment was assumed to be a
`key=value` pair, reading index 1 unconditionally. A trailing `;`, a stray
`;;` or any malformed token therefore threw — and because parse() is wrapped
in a single try/catch, the whole profile failed to load: no name, no traffic,
nothing. A trailing semicolon is common in the wild.

Segments without a `key=value` pair are now skipped instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interval was read with int.parse, so a decimal or any non-numeric value
threw and — like the subscription-userinfo case — failed the entire profile
parse rather than just the auto-update setting.

The convention is a positive integer number of hours, so anything else
(garbage, a decimal, zero or negative) is now ignored and the profile keeps
its default update behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps in the name hierarchy:

content-disposition was matched with `filename="..."` only, so the RFC 5987
extended form `filename*=UTF-8''<percent-encoded>` — how servers send a
non-ASCII name — was skipped and the provider's real name was lost. The
extended form is now preferred and decoded, falling back to the quoted form.

The steps also mixed `isEmpty` with `isBlank`: a whitespace-only result was
treated as a name by the later steps but as blank by the final fallback, so a
real url fragment or filename could be dropped in favour of "Remote Profile".
Every step now treats whitespace as blank.

Also documents that the url-based steps are remote-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eader

Replaces the `enable-fragment` boolean with `fragment`, which carries the same
value as a proxy link's own `fragment` parameter, so a provider can set the
size and sleep instead of only switching fragmentation on.

The value is read exactly like a proxy link: a full `size,sleep,method`
triplet, or `tlshello,size,sleep`. Anything shorter, or a size or sleep that
isn't a valid range, is incomplete and leaves fragmentation off rather than
enabling it with half the values.

The user's own setting still wins, and enables fragmentation with the size and
sleep from their settings — it carries no values of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warp and psiphon are mutually exclusive — the core runs one chain mode at a
time — so the two boolean headers become a single `extra-security` header
holding the mode. UserOverride follows, replacing `enableWarp`/`enablePsiphon`
with `extraSecurity`, bumped to v2 with a read-time migration; psiphon wins
when both were set, matching v1 where it overrode warp.

The value is matched strictly against ChainMode: it must equal a documented
mode key exactly or it is ignored, so no case folding and no lists. `profile`
is never accepted from a subscription, since chaining to another profile is the
user's choice. ChainMode and ChainStatus also replace the hardcoded strings.

The raw header is consumed rather than passed through — `extra-security` is on
the output allow-list, so an unrecognised value would otherwise reach the built
config as a string where the core expects an object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tile no longer shows or edits a password, so the title itself has to
tell the user the shared link is protected.

Drop the two keys the old password UI needed:
- lanSharingPassword (input dialog title)
- lanSharingPasswordNotSet (subtitle placeholder)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirror the English change in the other ten locales: lanSharingPassword
and lanSharingPasswordNotSet no longer exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The user had to invent and maintain the LAN sharing password, and an
empty one silently shared the VPN with no authentication at all.

Generate a 16-character password with Random.secure the first time
sharing is turned on, store it, and reuse it from then on. Sharing that
was already enabled without a password gets one as soon as the tile is
shown. Visually ambiguous characters are left out of the alphabet so the
password survives being typed by hand from the QR code.

With the password out of the user's hands, the edit dialog, the subtitle
and the no-password link branch are gone, and the tile is a plain
SwitchListTile again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The protobuf/gRPC output under lib/hiddifycore/generated/ is produced by
protoc and json_editor.dart is a vendored copy of json_editor_flutter.
Neither is ever hand-edited, yet together they accounted for 148 of the
243 analyzer diagnostics, burying the findings in our own code.

Compilation still covers both; only the analyzer stops reporting them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
veto9292 and others added 29 commits October 6, 2026 23:06
While auto selection is on and the list differs from the region's
list, a bar counts the changes. Share opens a sheet, shown like the
app's other sheets, with exactly what gets sent and where before it
opens GitHub. The share item leaves the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expandable button offered a new rule and the predefined rules,
which the built-in rules have replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The region leaves the bottom panel for a chip beside the menu, with
its flag and the word Region. Changing it still resets the direct DNS
server, whose default follows the region.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel, its Routing tab and the setting that kept it open are gone.
Its options move to the top of the rules list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The built-in rules keep the list from being empty, so its empty text
goes and the menu always offers every item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Balancer, Resolve destination and IPv6 sit above the rules and can't
be moved. Each value sits at the end of its row and opens its menu
there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Android, App-based routing heads the rules list, since it applies
before every rule. Its switch turns it on and off, and its two modes
sit under its name, the current one in a tinted pill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While it's on, a row shows up to ten app logos, the user's own picks
first, and opens the app list. It keeps its height while loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outside region Other, a row turns auto selection on and off, updates
it now, and shows when it was updated or why its list is missing.
While it loads, a bar takes the switch's place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The region or the mode can change while a list loads, and that change
already settled auto selection. A list that arrives after it is
dropped instead of bringing back the old region's or mode's picks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mode change cleared only the old mode's auto picks, so when the new
mode's list failed to load, auto selection still showed on with no
picks, and Share counted the user's own apps as added. It now turns
auto selection off first, and a list that loads turns it back on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writing null to that preference fails and only logs a warning, so the
date was never cleared. Nothing reads it while auto selection is off,
and turning it on writes a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AppList copied its family argument into a late final field in build,
so a second build threw, and its exports watched the translations,
which rebuilds it on a language change. It now uses the generated
mode, and the exports read the translations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its one caller always passes the mode and the region, so they are
required, and the unused include/exclude getters, result helpers and
preference fallbacks are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The region listener needs no checks: it only fires on a change. The
list page restores the removed auto picks through the data source it
already uses for Undo, so AppBasedRouting no longer passes it along.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mode change can start a run while another still loads. The first
one to end hid the bar and brought back the switch while the other
was still loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flags were cleared through a cascade, which kept the column as it
was, so an import merged into the current lists although its dialog
says it replaces them. Auto picks stay, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an empty Proxy only list, Android lets every app into the VPN,
but the package picker marked every app as outside it, and every
picked one as a rule that won't apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every change renumbers the list or keeps each rule in its place, so
the list is always in order. The sort also changed the state in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The allowed built-ins, the ones found and the checks between them
become one ordered set: a built-in seen for the first time is crossed
off, and what's left goes first. Same behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The intro sets the region after the rules load with region Other, so
the region rule was added on top, above Block ads, while Reset puts it
last. A list that was never saved is the default list, so it is now
rebuilt in the default order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chip read the DNS setting after waiting for the region to save,
when the page may already be gone. It now takes it before the wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The days between two midnights were counted in whole days, so a
23-hour day at a clock change turned yesterday into today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The logos on the routing page and in the share sheet were decoded for
a 2x screen, so most phones scaled them up and blurred them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu no longer has a short form for an empty list, so its items
are built in its itemBuilder instead of on every rules change, and the
two export items use plain tear-offs like the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They were pinned to the bottom left, which in Persian and Arabic is
the end of the line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dynamic_color 1.7 leaves surfaceContainerHighest unset, so it falls
back to the page color and the chips disappear. Until the Flutter
upgrade, they use surfaceVariant, the same tone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The old region picker never reset it: its handler never ran. So the
chip doesn't either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- .gitignore: this branch drops the aider entries; upstream adds the
  local signing material. Both kept.
- analysis_options.yaml: both sides' analyzer excludes kept.
- Makefile: upstream's portable zip (7z, or Compress-Archive) from
  hiddify#2359, which fixes the same thing as this branch's tar.exe line.
- hiddify-core: upstream's e5b8ef2, which already contains db74dfc.
- profile_parser_test.dart: both new test groups kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hiddify-com
hiddify-com merged commit 095ba14 into hiddify:main Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants