github hiddify/hiddify-app draft

pre-release3 hours ago

Release Downloads

Download based on your OS:

بر اساس سیستم عامل خود دانلود کنید:

OS Download Downloads VirusTotal
Android downloads VirusTotal
downloads VirusTotal
downloads VirusTotal
downloads VirusTotal
Windows downloads VirusTotal
downloads VirusTotal
downloads VirusTotal
macOS (v10.15+) downloads VirusTotal
downloads VirusTotal
Linux downloads VirusTotal
downloads VirusTotal
downloads VirusTotal

List of all changes: ChangeLog

All changes from to the latest commit:

(unreleased)

New

  • Vaydns with scanner and multi resolver.

Fix

  • Reopen the proxies stream instead of showing an error.
    _The proxies page turned any failure of its gRPC stream into an error and
    showed "Unexpected error" until the page was rebuilt, though the tunnel
    kept working. The stream fails whenever its connection drops, e.g. when
    iOS suspends the app, and nothing opened it again.

Like the active proxy, the page now keeps its last list, reopens the
stream when it ends (with the same backoff) and when the core is set up
again on resume. Reopening no longer drops the pinned proxy._

  • Back off when reopening the active proxy stream keeps failing.
    When the core stays unreachable while the tunnel is reported as running,
    the stream was reopened every 2 seconds, and each failure is logged as an
    error with a stack trace, which floods the in-memory log. The wait now
    doubles up to 30 seconds and goes back to 2 seconds once the stream
    delivers again or the connection state changes.

  • Close the old gRPC channels when the core is set up again on resume.
    HiddifyCoreService.init() runs on every resume, and each run opened new
    channels to the core without closing the ones they replaced, plus a hello
    channel that was never closed. Each stayed connected until its idle
    timeout, so switching back to the app often left extra connections open,
    on iOS to the VPN extension, which has a tight memory cap. The replaced
    channels and the hello channel now shut down gracefully.

  • Reopen the active proxy stream after the app returns from the background.
    _After the phone was locked for a few minutes, or after a while on macOS,
    the connection button stayed yellow on "connecting" while the tunnel kept
    working. The gRPC stream that carries the active outbound's delay drops
    when the app is suspended; its error was turned into an empty outbound
    with no delay, and the stream was never opened again because
    serviceRunning stays true.

The notifier now drops the failed event, keeps the last outbound, opens
the stream again when it ends, and also rebuilds when the core is set up
again on resume. Stats get the same rebuild._

  • Update ConfigOptions to use unblocker mode and license keys; add tests for CoreStatus behavior.

  • Enable Android APK and Linux/Windows builds in CI workflow.

  • Update code signing process and add local signing materials.

  • Issues.

  • Update Android SDK setup to install platform-tools.

  • Leak issue.

  • Check for existence before dropping profile_override column.

  • Update Turkish i18n strings for LAN sharing.

  • Update non-English translations for LAN sharing settings.

  • Correct arrow icon direction in chain quick settings.

  • Resolve ChainTimelineHeader background visibility in light theme.

  • Correct Turkish translation for WARP license required.

  • Add missing required text to fa WARP missingLicense translation.

  • Add confirmation for deep link profile addition to prevent SSRF.

  • Temporarily patch Riverpod lazy build-phase collision on connection status changes.
    _Temporary hotfix to resolve fatal 'setState() called during build' errors caused by lazy
    evaluation of dirty provider dependency chains during high-frequency status transitions.

  • Establishes eager evaluation by listening to activeProxyNotifierProvider at the root
    container level during bootstrap to resolve dirty state flushes in microtasks.
  • Refactors serviceRunningProvider and all downstream dependent stream/future
    notifiers to execute synchronously to prevent build-phase timing hops.
  • Defers secondary state updates using Future.microtask().

Note: This is a temporary architectural patch to bypass Riverpod's lazy widget evaluation
mechanics under high-frequency stream updates. A full core communication redesign is
recommended for future maintenance to address this natively._

  • Liquid Glass icon-launcher setup for iOS and macOS.

  • Get error when the string len of title is less than 3.

  • Set getApplicationDocumentsDirectory as a fallback for getExternalStorageDirectory in Android.

Other

  • Chore: enhance macOS App Store build workflow.
    _- Added TARGET_NAME_macos_appstore_pkg to build configuration for macOS App Store package.
  • Improved artifact handling by copying the macOS App Store package to the output directory.
  • Updated comments for clarity regarding artifact uploads and error handling in the CI workflow._
  • Chore: update build scripts and configurations for iOS and macOS.
    _- Added PrivacyInfo.xcprivacy to the Resources build phase in the Xcode project to ensure proper inclusion in the app bundle.
  • Enhanced validation scripts to check for the presence of PrivacyInfo.xcprivacy in both the main app and the packet tunnel extension.
  • Updated Makefile comments for clarity regarding CI build processes for the Mac App Store._
  • Chore: update iOS build configurations for manual signing and improve macOS App Store workflow.
    _- Changed code signing style to Manual and updated provisioning profile specifier in Xcode project files.
  • Enhanced macOS build workflow to ensure proper artifact uploads and error handling for Apple Distribution identity.
  • Updated exportOptions.plist to correct provisioning profile references._
  • Refactor: streamline macOS build workflow and improve provisioning profile handling.

  • Feat: add macOS App Store installation dependencies to Makefile.

  • Feat: update macOS build process and enhance core integration.

  • Feat: update .gitignore and enhance desktop mTLS smoke test.

  • Feat: improve animation rendering.

  • Feat: update connection notifier to reflect disconnecting state.
    _- Modified the _disconnect method in ConnectionNotifier to set the state to Disconnecting when a disconnect action is initiated.

  • Added comments to clarify the behavior of the connection state during the disconnect process._
  • Feat: enhance IP information handling with custom ipinfo-url support.
    _- Updated ProfileParser to include ipinfo-url in allowed override configurations and populated headers.
  • Modified IpInfoNotifier to utilize a custom IP information URL from headers when available.
  • Enhanced ProxyRepository to accept a custom ipinfo-url for fetching IP information.
  • Added flexible parsing for various IP information JSON structures to accommodate custom endpoints.
  • Introduced tests for new IP information parsing logic and validation of ipinfo-url handling._
  • Feat: implement mTLS support for gRPC connections.
    _- Updated hiddify-core submodule to the latest commit for mTLS functionality.
  • Added new test cases for mTLS handshake in both Android and iOS environments.
  • Enhanced MethodHandler to register client certificates for secure connections.
  • Integrated mTLS setup in CoreInterface for mobile and desktop platforms.
  • Introduced new assets (client certificate and key) for testing mTLS functionality.
  • Updated build.gradle and other configurations to support new dependencies for testing._
  • Feat: enhance release notes with VirusTotal integration and SHA-256 digest automation.
    _- Added VirusTotal status badges and download counts to the release message template.
  • Introduced a new script to replace placeholders with actual SHA-256 digests of release assets.
  • Updated GitHub Actions workflows to automate the generation of release notes with VirusTotal links and asset information._
  • Feat: integrate VirusTotal scanning into release workflow.
    _- Added a new workflow for VirusTotal scanning of release assets.
  • Implemented two jobs: virustotal-draft for draft releases and virustotal-release for non-draft releases.
  • Enhanced build.yml to include VirusTotal scanning steps and permissions.
  • Created virustotal_scan.yml to handle the scanning process and update release notes with scan results._
  • Feat: add CoreInterfaceDesktopTun for VPN management on Windows and Linux.
    _- Introduced CoreInterfaceDesktopTun to manage VPN connections using gRPC.
  • Implemented snapshotVpnConfiguration for handling local file references in VPN profiles.
  • Updated CoreInterfaceMacOS to utilize snapshotVpnConfiguration for configuration management.
  • Enhanced TunnelServiceController to ensure service readiness and manage build IDs.
  • Updated core interface wrapper to support the new desktop tunnel interface._
  • Chore: hiddify-tunnel-service in macos.
    _- Updated hiddify-core submodule to the latest commit.
  • Changed cache dependency path in GitHub Actions build workflow to point to hiddify-core/go.sum.
  • Enhanced TunnelStartRequest in tunnel.proto to include additional fields for configuration.
  • Removed unused NetworkExtensionCore files and adjusted macOS build scripts accordingly.
  • Updated TunnelServiceController to manage VPN service state and configuration more effectively._
  • Feat: Implement VPN service management for Mac App Store and Developer ID builds.
    _- Added support for running the VPN through a bundled hiddify-core library with administrator rights.
  • Introduced AppStore.entitlements for sandboxing and network extension capabilities.
  • Created AppStore.xcconfig for Mac App Store build configurations.
  • Updated NetworkExtension configurations to differentiate between app extension and system extension.
  • Added Unsigned.xcconfig for builds without Developer ID, utilizing hiddify-core's tunnel service.
  • Enhanced Info.plist to include new keys for tunnel kind and application category.
  • Developed TunnelServiceController to manage VPN service installation and state.
  • Implemented install and uninstall scripts for the VPN service.
  • Updated build scripts to handle different distribution modes (unsigned, developer-id, app-store).
  • Removed obsolete prepare-tunnel-source.sh script and adjusted source handling in build scripts._
  • Feat: update GitHub Actions permissions for release workflow.

  • Feat(macos): adopt latest changes for macos no root vpn.

  • Merge pull request #2354: run the macOS VPN in a packet tunnel system extension.
    Conflicts in startup_connection.dart and hiddify_core_service.dart: both sides
    kept. When the extension manages the core, its own start, stop and status path
    runs first; otherwise the start/stop cancellation and status listener numbering
    from main apply.

  • Chore: merge upstream main into macOS network extension branch.
    Preserve the macOS managed VPN lifecycle while adopting upstream logging and dependency changes. Avoid restarting a tunnel already restored by macOS on app startup.

  • Fix(macos): Change user-visible bundle display name to more user friendly.

  • Fix(macos): support universal Flutter builds with Apple lipo.

  • Fix(macos): make signing errors self-contained.

  • Refactor(macos): adopt shared extension provider.

  • Fix(macos): resolve VPN resources from the core working directory.

  • Fix(macos): restore process lookup in VPN mode.

  • Chore: remove unnecessary MD file.

  • Chore: clean lint and whitespace in touched code.

  • Fix(macos): prepare signed releases for notarization.

  • Fix(macos): disable saved VPN before changing engines.

  • Chore(macos): isolate local VPN testing and configure release signing.

  • Fix(macos): present VPN extension approval as a permission prompt.

  • Feat(macos): run VPN in a packet tunnel system extension.

  • Feat(connection): update EclipseLook states to include stopping and adjust animation logic.

  • Fix(dialogs): streamline focus node initialization and improve markdown link handling.

  • Feat(home): show the traffic as an eclipse round the connect button.
    The button is a white disc that curves at its edge; while connected, download
    glows sun yellow and upload plasma blue behind it, reaching further as more
    flows. A failed connection shows a badge that opens the error. The home page
    background lightens a little round the button.

  • Feat(connection): improve connection handling and add cancellation logic.

  • Merge branch 'main' of https://github.com/hiddify/hiddify-app.

  • Merge pull request #2382 from saeed-jalilian/fix/proxies-list-recovers.
    Fix "Unexpected error" on the Proxies page after the app returns from the background

  • Merge pull request #2386 from veto9292/fix/pre-release.
    feat: QR code from image, http2 cancel fix, bump hiddify-core

  • Chore(l10n): carry the QR code texts into the ten locales.

  • Feat(profile): read a QR code from an image in the scanner dialog.

  • Feat(i18n): rename the scan button to QR code and add the no-QR-in-image text.

  • Fix(deps): use http2 3.1.0 so a cancelled call no longer drops the core connection.
    _With http2 2.3.1, when the app cancels a gRPC call just as the core answers
    it, the late answer makes the client close the whole connection
    (dart-lang/http#1799). Every call on it fails, including the status
    listener the connect button relies on. The app's own listener re-opening
    already triggers it: the logs show 90 such drops on Windows and 2 on
    Android.

http2 3.1.0 fixes this, but grpc still allows only 2.x, so the version is
overridden until grpc allows 3.x._

  • Chore(dev): allow flutter_driver in profile builds.
    The iPhone build running on a Mac only starts as a profile build, so the
    driver was never available there. It stays behind the driver flag, which
    still costs the real keyboard, and stays out of release builds.

  • Merge remote-tracking branch 'upstream/main' into fix/pre-release.

  • Feat(vpn): add always-on VPN functionality and UI toggle.

  • Merge branch 'main' of https://github.com/hiddify/hiddify-app.

  • Merge pull request #2367 from veto9292/fix/pre-release.
    Pre-release fixes

  • Fix(home): stop the connect spinner from throwing duplicate keys.
    The spinner sat in an AnimatedSwitcher, which keeps an outgoing child on
    screen for its 300 ms fade. When the state flipped twice within that time,
    as a hot restart while connected did, two unkeyed empty boxes faded out
    together with the same key and Flutter threw "Duplicate keys found". The
    spinner is now one child that fades with AnimatedOpacity, and its
    animation stops while hidden.

  • Chore(l10n): carry the balancer strategy texts into the ten locales.

  • Feat(routing): describe the balancer strategy for a few seconds after a pick.

  • Feat(i18n): add the text describing each balancer strategy.

  • Test(proxy): cover the pinned proxy row.

  • Fix(proxy): keep the picked proxy in the row the user tapped.
    A new ping re-sorted the proxies page right after a tap, so the proxy the
    user had just picked could jump, often to the end of the list. It now
    stays in the row it was tapped in while the rest sorts around it, as long
    as it is the main group's choice. Picking a group such as lowest pins
    that row, not the proxy the group picks. A new connection or sort clears
    the pin.

  • Test(proxy): cover a failed proxy switch.

  • Chore(l10n): carry the failed proxy switch text into the ten locales.

  • Fix(proxy): tell the user when switching to a proxy fails.
    A failed switch threw from the tap handler, where nothing caught it: the
    app logged an uncaught error and the tap looked ignored. It now shows an
    error SnackBar, "Couldn't switch to this proxy. Try again."

  • Feat(i18n): add the text for a failed proxy switch.

  • Test(rules): cover the built-in rules.
    Test file only, no app code changes, so it needs no close review.
    It checks the built-in rules' order, switches, region changes, reset
    and import, and how the routing page shows them.

  • Fix(profile): keep the config editor's type dropdown from crashing on unknown values.
    Add awg to the endpoint types, and show any value the list doesn't know as
    an extra item instead of tripping DropdownButton's assertion.

  • Feat(logs): keep the category filter across runs.
    Same as the level filter: the category filter is saved as the
    log-filter-category preference, the page opens on it, and picking a
    category saves it. "All" is saved as an empty string and stays the
    default.

  • Feat(logs): keep the level filter across runs.
    The logs page's level filter went back to warn on every run, and on a
    phone also every time the page was closed, since the page's state is
    dropped with it. It is now saved as the log-filter-level preference:
    the page opens on the saved level, and picking a level saves it. warn
    stays the default.

  • Fix(android): keep the VPN service's platform interface when the front core restarts.
    _The core keeps one platform interface per process, and the front
    core's setup passed null. After the front core was closed on pause and
    set up again, the running VPN service lost its link to Android's
    VpnService, and the next in-place restart (profile update or switch)
    failed with "open /dev/tun: permission denied", leaving a dead tunnel
    that Android still showed as connected.

BoxService now records its platform interface while it runs, and the
front core's setup passes that one instead of null._

  • Fix(app): stop the front core on pause only, not on inactive.
    _The lifecycle state came from useAppLifecycleState, which reports a
    change only when the widget rebuilds, and no frame is drawn once the
    app is in the background. On Android "paused" often never arrived, so
    the front core was closed on "inactive" instead, which also fires for
    a dialog or the notification shade.

The state now comes from AppLifecycleListener, which the engine calls
directly: "paused" closes the front core, "resumed" sets it up again,
and "inactive" does nothing. The state at startup is still handled
once, as before._

  • Fix(profile): stop the add when an inner link download is canceled.
    Cancelling used to write "null" lines and carry on. It now returns
    ProfileFailure.cancelByUser and leaves the file unwritten.

  • Fix(profile): keep HTTP proxy lines and log failed inner links.
    A line counts as a subscription link only when its URL has a path.
    HTTP proxy links have none, so they stay as they are instead of being
    downloaded and lost. A subscription link that fails to download is
    dropped and logged with its host only, since the URL carries a token.

  • Fix(profile): drop the Windows line ending from inner links.
    The trailing \r went into the request URL, so the download failed.

  • Fix(profile): find inner links in base64 subscriptions.
    Decode the file before looking for links. A base64 file without links
    is still left as it is.

  • Fix(profile): keep lines that are not links untouched.
    The expander trimmed every line, which removed YAML indentation.

  • Refactor(profile): rebuild the inner link expander as text in, text out.
    _Replace expandRemoteLinesInParallel with expandLinks and
    expandLinksInFile. They now run after the headers are read, in
    addLocal, addRemote and updateRemote. A file with no links is no longer
    rewritten, so indented Clash YAML stays intact, and links are fetched
    into memory instead of side files that were never deleted.

  • Feat(http): let DioHttpClient.get return the body as text.
    Add an optional responseType to get(). Callers that leave it out keep
    Dio's default JSON decoding.

  • Test(profile): cover the temp file cleanup.
    Check that adding, a failed add, updating and an offline update each
    leave no .tmp.json behind, and that the file still exists while
    the config is validated.

  • Fix(profile): delete the temp file after adding or updating a profile.
    upsertRemote and offlineUpdate returned a lazy TaskEither from inside
    try/finally, so finally ran before the task wrote .tmp.json, and
    the file was never deleted. Run the task inside the try instead, so
    the file is deleted after the task, whether it succeeds or fails.

  • Test(profile): cover the shared subscription link.
    Check that a non-default port, userinfo, path and query survive, that
    the old fragment is kept when no name is given, and that the name is
    percent-encoded into the fragment.

  • Fix(profile): keep the port in the shared subscription link.
    _The link was rebuilt from scheme, host, path, query and fragment only,
    so an explicit port (https://panel.example.com:2053/sub) fell back to
    the default one and the shared URL or QR pointed at the wrong server.
    Userinfo was dropped the same way.

Replace only the fragment on the parsed Uri instead. This also stops
adding an empty "#" when the link has no fragment and no name is given._

  • Feat(notification): pause the SnackBar timer on hover.
    The pointer on a SnackBar stops its close timer, as on the toast;
    leaving starts it again with the full time. SnackBar's own timer can't
    stop, so it is turned off and a timer in the content closes it. That
    timer starts when the SnackBar is first drawn, so one raised in the
    background still shows, then closes.

  • Chore(l10n): carry the applying-changes text into the ten locales.
    Persian is re-translated from the new English; the other nine get the key for
    the first time. Each follows its locale's wording of "Reconnecting to apply
    changes…".

  • Fix(i18n): call the hot reload state "Applying changes…"
    "Hot reloading" is developer jargon on a user-facing label. "Applying
    changes…" says what happens and matches "Reconnecting to apply changes…".

  • Fix(proxy): stop the active proxy type from overflowing its card.

  • Build(deps): remove toastification.
    Its two helper packages, equatable and pausable_timer, leave the lock
    file with it.

  • Chore(notification): delete the toast hover workaround and its test.
    ToastHoverPause only stood in for toastification's pauseOnHover, which
    broke the mouse tracker when a toast closed under the pointer. Nothing
    uses it now.

  • Refactor(app): remove ToastificationWrapper.
    Nothing shows a toast anymore.

  • Refactor: replace CustomToast with InAppNotificationController.
    CustomToast showed the same message as the controller, only from a
    widget's context and without an action. Its two uses, the About page's
    update check and choosing a profile, now call the controller. Its unused
    toast widget goes with it.

  • Feat(notification): show a close button on hover in SnackBars.
    A SnackBar without an action shows a close button while the pointer is
    over it, as the toast did. Hidden, the button keeps its room so the text
    doesn't move, but it can't be pressed or reached by a screen reader.

  • Feat(notification): round SnackBars and space them like the toast.
    Corners of 12 instead of 4, and the toast's spacing: 16 before the icon,
    10 between the icon and the text, and a minimum height of 40. Text of
    several lines keeps room above and below it.

  • Feat(notification): give SnackBars a type icon and medium text.
    Each message shows the icon and color of its type, as the toast did: a
    green check for success, a red cross for an error, a blue info icon. The
    text is medium weight, like the toast's title, instead of regular.

  • Feat(notification): show in-app messages as SnackBars.
    _toastification starts a toast's close timer before the toast is drawn.
    A toast raised while the app was in the background drew no frames, so
    its timer ran out first and the toast stayed on screen for good. A
    SnackBar starts its timer only once it is fully on screen.

InAppNotificationController now shows a floating SnackBar through the
app's ScaffoldMessenger. A new one replaces the one on screen. It closes
after 4 s, or 8 s with an action, is at most 480 wide, and sits 92 above
the bottom on a phone, clear of the bottom bar, and 16 elsewhere._

  • Feat(notification): add a SnackBar layer above every screen.
    _A Scaffold around the Navigator, so a SnackBar can show above every
    route: a dialog or a bottom sheet can't cover it. A ScaffoldMessenger
    shows a SnackBar only on the outermost of its Scaffolds, and the pages'
    Scaffolds sit inside this one.

The pages get the app's MediaQuery unchanged. The layer's own MediaQuery
has no top padding, since on iOS a Scaffold puts a status bar tap target
there, over the pages' own. It counts the keyboard as bottom padding, so
a floating SnackBar stays above it. Nothing shows a SnackBar here yet._

  • Feat(profile): import a zip as one profile named after it.
    _A zip used to become one profile per file. The files are now merged
    into one text, with a line of #=== between them, and added as a single
    profile named after the zip file. The core reads the #=== line as a
    comment, so it ends one config and starts the next, and it skips a
    file it cannot read.

readZipText replaces readZipTexts: it returns the merged text, or an
empty string when the bytes are not a zip or no file could be read.
addArchive makes one addLocal call and takes the profile name._

  • Refactor(utils): remove the unused BottomSheetPage.
    Left over from when bottom sheets were go_router pages. The sheets open
    through BottomSheetsNotifier now.

  • Fix(router): label the dim layer behind bottom sheets for screen readers.
    BottomSheetsNotifier built its ModalBottomSheetRoute by hand. That skips the
    barrier label and tap hint showModalBottomSheet adds, so screen readers found
    the dim layer behind every sheet without a name. Open the sheets with
    showModalBottomSheet on the root navigator instead; they look and close the same.

  • Fix(settings): apply a setting changed back after a hot reload.
    The listener read the running options snapshot once, in build(). A restart
    rebuilt the notifier through the status change; a hot reload keeps the status
    connected, so the snapshot went stale and changing a setting back was skipped.
    It is now read on every change.

  • Feat(home): show the hot reloading look on the connect button.
    _A new _Look, hotReloading: "Hot reloading…" label, the yellow of noPing, the
    connected image, and the tap off while the core applies the config. It comes
    before the ping checks. Same behaviour as upstream's version, rebuilt on Look.

  • Feat(profile): accept zip files in the file import.
    The picker now lists .zip as well. A picked zip goes to addArchive, and
    every other file takes the same path as before.

  • Feat(profile): add one profile per file of a zip.
    addArchive reads a zip with readZipTexts and adds each file through
    addLocal, named after the file, so the core decides which files are
    valid configs. Files the core rejects are skipped. The add fails, with
    the first failure, only when no file could be added.

  • Feat(utils): read the files of a zip as text.
    _readZipTexts turns the bytes of a zip into a list of (name, text), one
    per file, and never looks at what a file contains. It skips folders,
    the MACOSX folder and . files that macOS adds when it zips a folder,
    and files over 256 KB. The name is the file name without its
    extension. Bytes that are not a zip give an empty list.

  • Build(deps): declare archive as a direct dependency.
    The package was already in the lockfile, pulled in by another package.
    The profile import is about to read zip files with it, so the app now
    declares it. The resolved version stays 4.0.4.

  • Merge upstream/main into fix/pre-release.
    _Upstream's hot reload (1839ecf) and the v5.0.0 beta bump (98387fe).

connection_button.dart keeps this branch's version. Upstream edited the old
per-parameter switches that the _Look refactor removed, so the hot reloading
look is added to Look in the next commit.

  • Fix(profile): show the bulk update toast only for manual runs.
    The periodic background update no longer pops a toast every time it
    refreshes profiles; only the user-triggered update reports its result.

  • Refactor(connection): tidy the checks on a failed core start.
    The message is read once before the first check, an empty message
    gets its own check instead of a ternary at the end, each check has
    one comment, and the commented-out leftovers are gone. The failure
    returned for every input is unchanged.

  • Chore(l10n): carry the VPN dialog texts into the ten locales.

  • Feat(connection): explain a failed start when another VPN holds the TUN.
    The core gives its TUN adapter the fixed address 172.19.0.1. When
    another adapter already has it, Windows answers "The object already
    exists" and the dialog showed that raw text. The failure now maps to
    its own dialog, "Another VPN is active".

  • Fix(i18n): shorten the administrator warning for VPN mode.
    The title already names the cause, so the message now gives the
    effect and the fix in one line, like the other-VPN dialog.

  • Feat(i18n): texts for the dialog when another VPN holds the TUN.

  • Fix(connection): show the core's text when a start fails.
    Until February 2026 the dialog for a failed core start showed the
    core's message. A type change replaced it with a fixed line, so the
    reason stayed in the log. The core's message is back in the dialog,
    and the fixed line stays for a core that sends none.

  • Fix(connection): warn when VPN mode needs administrator rights.
    _The warning, its text and the MissingPrivilege failure exist, but
    nothing has built the failure since the app-side check went away in

  1. The core's own start error now decides: when it says Windows
    denied access to the TUN adapter, the start fails with
    MissingPrivilege._
  • Fix(connection): open the failed-connect dialog in debug builds.
    A failed start sends the core restart signal, which marks the notifier
    out of date until the next frame. The failure handler then asked
    Riverpod for the dialog, and debug builds refuse that, so no dialog
    opened. The dialog, the translations and startedByUser are now read
    before the connect starts.

  • Feat(home): ring, ripple and error text for the connect button.
    The colors stay. The colored glow gives way to the standard level 3
    elevation, and the shrink and blur while busy give way to a ring that
    spins around the button while it connects or disconnects. One ripple
    marks a connect the user waited for, loading dims the logo, and a
    failure reads "Couldn't connect" with "Tap to try again" below.

  • Fix(home): show a failed start as a failure on the connect button.
    The core reports a failed start as disconnected with the failure
    attached, and the button showed it as a plain "Tap to connect". It now
    shows the error look until the next tap or status.

  • Fix(home): keep the connect button's look while the status reloads.
    After every core start the connection status rebuilds, and Riverpod
    marks it as reloading: AsyncLoading, still carrying the last value. The
    button matched settled data only, so it flashed its loading look in the
    middle of a connect. It now reads the last value; an error is checked
    first because it carries an old value too.

  • Fix(connection): set a failed connect's error before showing its dialog.
    The error was set only after the dialog closed. A status the core sent
    while it was open, such as a late Started, was then overwritten, and
    the button showed an error over a working tunnel. Set first, a later
    status wins, and nothing after it can leave the connect hanging.

  • Fix(connection): report Connecting from the first step of a connect.
    The core reports Starting only once the config is built, about a
    second after the tap, so the app still showed Disconnected in between.
    The notifier now marks itself connecting as soon as a connect starts.

  • Chore(l10n): carry the failed-connect texts into the ten locales.

  • Feat(i18n): texts for a failed connect on the connect button.

  • Fix(home): stop at Add profile when there is no profile to connect.
    With no active profile, a tap showed the dialog, opened Add profile and
    then went on to connect: it could show the experimental notice over the
    sheet, vibrated, and saved startedByUser as true before the connect
    step gave up. On desktop that flag made the next launch connect by
    itself once a profile was added.

  • Refactor(home): derive the connect button's look in one place.
    _One function now turns the core's status and the active server's ping
    into the button's look; label, color and image all read from it. A
    tunnel with no ping yet still looks like connecting, so one wait has
    one look, and a ping timeout says Connected. Loading no longer looks
    like an error.

The ping is read without Riverpod's previous value. After a
disconnect that value is the last session's ping, which made the next
connection look up before its own ping answered.

The reconnect branch, the secure label and the unused values are gone:
the reconnect flag only ever held false, and the provider behind it
stays alive through ConnectionWrapper._

  • Fix(qr): center the password eye and even out tile padding.
    The eye now sits at the tile's end, centered top to bottom, instead of
    in the value's row. Tiles get the same 12 dp on both sides; the eye's
    own padding lines it up with that.

  • Feat(qr): confirm a copy in place instead of with a toast.
    Tapping a tile, or the copy-full-link button, swaps its label for a
    green "Copied" for a moment. The hidden text keeps its size, so
    nothing around it shifts.

  • Chore(l10n): carry the QR dialog "Copied" text into the ten locales.

  • Feat(i18n): a "Copied" text for the QR dialog.

  • Feat(qr): Material 3 QR dialog with copyable LAN proxy details.
    _The QR dialog is now a standard Material 3 dialog: the code on a white
    card with the app logo, a title and subtitle, and a copy-full-link
    button. For LAN sharing it also lists IP, port, username and password
    as tiles that copy themselves on tap; revealing the password gives
    each credential its own full-width row.

The sharing row in settings and quick settings keeps its switch and
gets a QR button beside it, replacing the two buttons that sat under
the row._

  • Chore(l10n): carry the QR dialog texts into the ten locales.

  • Feat(i18n): English texts for the new QR dialog.
    Adds the dialog's copy-full-link button, its IP, port, username and
    password labels, the show/hide password tooltips, and the LAN sharing
    subtitle. Drops the old "Copy link" key, which the sharing row no
    longer uses.

  • Chore(l10n): carry the shorter built-in rule titles into the ten locales.

  • Feat(i18n): shorter titles for two built-in rules.
    "Bypass LAN" and "Block ads and malware" fit beside the Built-in tag.

  • Feat(rules): a lock marks built-in rules after their name.
    The Built-in word moves from the outbound line to the end of the
    name, after a lock, both in the accent color, and always shows whole.

  • Feat(rules): keep a rule's name on one line.
    A long name ends in an ellipsis instead of making its tile taller.

  • Feat(routing): a mode change turns auto selection off.
    As with a region change, the old mode's auto picks go and the user's
    own picks stay; the new mode's list isn't applied by itself. Auto
    selection still comes on when App-based routing is turned on, until
    the user turns it off by hand.

  • Style(connection): end two files with a newline.
    flutter analyze reported eol_at_end_of_file for both.

  • Merge pull request #2369 from xNOTHlNGx/fix/fix-profile-name-after-edit.
    fix(profile): preserve profile names after config edits

  • Test(profile): verify renamed profiles persist across edits.

  • Test(profile): cover name preservation across protocols.

  • Fix(profile): preserve imported name after config edits.

  • Merge pull request #2377 from saeed-jalilian/fix/stuck-connecting-after-resume.
    Fix status stuck on "connecting" after the app returns from the background

  • Feat: add Profile.xcconfig and update project settings for new configuration.

  • V5.0.0 beta.

  • Feat(hot-reload): implement hot reload functionality for core configuration.

  • Merge branch 'main' of https://github.com/hiddify/hiddify-app.

  • Merge pull request #2365 from veto9292/feat/preset-rules.

  • Merge upstream/main into feat/preset-rules.
    _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
    #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._
  • Fix(routing): the region chip leaves the direct DNS server alone.
    The old region picker never reset it: its handler never ran. So the
    chip doesn't either.

  • Fix(rules): rule chips keep their tint with dynamic colors.
    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.

  • Fix(toast): alert toasts start on the reading side.
    They were pinned to the bottom left, which in Persian and Arabic is
    the end of the line.

  • Refactor(routing): build the rules menu when it opens.
    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.

  • Fix(routing): sharp app logos at any screen density.
    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.

  • Fix(routing): count the days since an update across clock changes.
    The days between two midnights were counted in whole days, so a
    23-hour day at a clock change turned yesterday into today.

  • Fix(routing): the region chip resets DNS even if the page closes.
    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.

  • Fix(rules): a new install gets the default rule order.
    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.

  • Refactor(rules): one set tracks the built-in rules still missing.
    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.

  • Refactor(rules): save the rules without sorting them again.
    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.

  • Fix(rules): Proxy only with no apps keeps every app in the VPN.
    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.

  • Fix(routing): importing app lists replaces them.
    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.

  • Fix(routing): auto selection's loading bar waits for every run.
    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.

  • Refactor(routing): fewer hops in App-based routing.
    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.

  • Refactor(routing): auto selection fetches only what it's asked for.
    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.

  • Fix(routing): the app list survives being rebuilt.
    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.

  • Refactor(routing): drop the last-update clear that never ran.
    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.

  • Fix(routing): a mode whose list fails leaves auto selection off.
    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.

  • Fix(routing): skip an auto selection list that loaded too late.
    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.

  • Feat(routing): auto selection row under App-based routing.
    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.

  • Feat(routing): show App-based routing's apps as logos.
    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.

  • Feat(routing): App-based routing section above the rules.
    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.

  • Feat(routing): the general options head the rules list.
    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.

  • Feat(routing): the rules list fills the page.
    The built-in rules keep the list from being empty, so its empty text
    goes and the menu always offers every item.

  • Feat(routing): remove the general options panel under the rules.
    The panel, its Routing tab and the setting that kept it open are gone.
    Its options move to the top of the rules list.

  • Feat(routing): the region as a chip in the app bar.
    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.

  • Feat(routing): a plain button adds a rule.
    The expandable button offered a new rule and the predefined rules,
    which the built-in rules have replaced.

  • Feat(routing): share the list's changes from a bar, after a preview.
    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.

  • Feat(routing): chips to hide system apps and restore auto picks.
    The chip row under the app bar also shows while searching. Restoring
    the removed auto picks shows a toast that can undo it.

  • Refactor(routing): auto selection leaves the app list page.
    Its button and the spinner that took the menu's place while it loaded
    are gone. Auto selection lives on the routing page now.

  • Feat(routing): the app list follows the mode chosen on the routing page.
    The page reads the mode in use and takes its title from it. Its own
    mode menu, which still offered "off", is gone.

  • Feat(routing): tag auto picks in the app list.
    An Auto tag after the name of each auto pick replaces the red dot that
    marked the removed ones.

  • Feat(routing): search the app list by package name too.

  • Chore(routing): name the exported file after App-based routing.

  • Feat(routing): share the lists the share sheet shows.
    Sharing no longer loads the region's list again or asks again: it
    opens the issue with the added and removed apps the share sheet
    already showed, and the link now encodes its title and body.

  • Perf(routing): fetch each app logo once per run.
    The plugin lists every installed app for each logo it looks up, so ten
    logos took seconds. Each logo now stays until the app closes, which
    suits the few logos on the routing page, not whole app lists.

  • Feat(routing): auto selection comes on until turned off by hand.
    Turning App-based routing on, or changing its mode, runs auto
    selection unless the user turned it off with its switch. A region
    change or Clear all doesn't count as turning it off by hand.

  • Feat(routing): a region change turns auto selection off.
    The new region's list isn't applied by itself. As with the auto
    selection switch, the auto picks go and the user's own picks stay.

  • Feat(routing): turn App-based routing on and off, and change its mode.
    Turning it on again brings back its mode and list. Each mode keeps its
    own list, and when the mode changes, auto selection moves to it with
    that mode's region list.

  • Refactor(routing): move auto selection into AppBasedRouting.
    Applying, clearing and restoring auto selection move from the app list
    into AppBasedRouting, next to the new handlers for its switch and its
    Update now button. Results show on a status line instead of toasts,
    and a region with no list turns auto selection off. The daily refresh
    runs every day instead of after a set interval, and its toast is gone.

  • Feat(routing): save App-based routing's switch apart from its mode.
    The switch gets its own key, and the mode keeps just Proxy only and
    Bypass, with Bypass by default. Turning it off no longer forgets the
    mode. A saved "off" from older versions counts as never turned on.
    Android reads the new switch, and falls back to the old "off" until
    the app has moved it.

  • Feat(routing): keep auto selection's loading and result in state.
    The loading flag now always clears, even when the download fails, and
    stays alive while no page watches it. A new state keeps why the last
    auto selection gave no list, for a status line under its switch
    instead of a passing toast.

  • Feat(region): give the region's name without its code.
    presentName turns "Iran (ir)" into "Iran", for texts that put the name
    in a sentence, such as "Auto selection based on Iran".

  • Fix(routing): an empty auto selection list means no list.
    Some regions have an empty list file, such as direct_cn and proxy_ru.
    It was taken as a list with no apps, so auto selection showed as on
    while doing nothing. It now counts as no list for the region.

  • Fix(routing): tapping an auto-picked app goes auto, removed, yours.
    A tap on an auto pick now removes it first, as the user most likely
    means, then makes it theirs, then gives it back to auto selection. A
    removed app that auto selection no longer picks becomes the user's own
    instead of vanishing.

  • Feat(rules): pick a rule's package names from the installed apps.
    The Package names field of a route rule is back. On Android it opens a
    list of installed apps with checkboxes, search and a filter for system
    apps; elsewhere it opens the plain text list. Apps the rule already has
    come first, and the order stays put while ticking. An app that
    App-based routing keeps out of the VPN carries a mark, and once ticked
    it says the rule won't apply to it. Apps the rule names but that aren't
    installed show as such. The old picker and its notifier, both
    commented out, are replaced.

  • Feat(rules): built-in rules instead of predefined ones.
    _Bypass local network, Block ads and harmful sites, and the region rule
    are now always in the rule list, tagged Built-in. They can be switched
    and moved but not edited or deleted. Their content comes from the app,
    so an update can improve it; the saved list keeps only their key,
    switch and place. The region rule takes the region's name; it leaves
    the list when the region is set to Other and comes back at the top,
    turned on, with any other region. A list that lacks a built-in rule,
    such as a new or imported one, gets it at the top in its default state.
    A new rule now goes first, ahead of every other rule.

The predefined rules sheet goes, and so does the auto selection sheet
that the app list page no longer opens._

  • Chore(l10n): carry app-based routing and built-in rule texts into the ten locales.
    The same text changes as the English commit. Arabic and Russian carry
    more plural forms. The region rule reads "region $region" in Russian,
    French and Portuguese, so the country name keeps its plain form.

  • Feat(i18n): English texts for app-based routing and built-in rules.
    New texts for the App-based routing section and its app list, auto
    selection's status, the share sheet, the built-in rules and Undo.
    Per-app proxy is now called App-based routing, and Proxy is now Proxy
    only. The texts of the old per-app page, the predefined rules sheet and
    the General routing options heading are removed.

  • Refactor(routing): rename the per-app proxy code after the UI.
    Names only, no logic: the per_app_proxy folder becomes app_based_routing;
    PerAppProxyService, PerAppProxy, PerAppProxyPage and AppProxyLoading
    become AppBasedRouting, AppList, AppListPage and AutoSelectionLoading,
    with their files; the perAppProxy route becomes appList; and the code
    reads the moved text keys. Imports are re-sorted where a path changed
    their order.

  • Chore(l10n): move the routing text keys in the ten locales.
    The same key moves as the English commit; no text changes.

  • Refactor(i18n): move the routing text keys to match the UI.
    Only key paths change, no text: generalOptions.region, regions,
    balancerStrategy, resolveDestination, ipv6Route and ipv6Modes move up to
    routing.*, generalOptions.perAppProxy becomes routing.appBasedRouting,
    and dialogs.confirmation.perAppProxy becomes dialogs.confirmation.appList.

  • Docs(claude): note hot reload and build_runner watch.

  • Feat(toast): flat toasts with an optional action button.
    _Update toastification to 3.2. Toasts use the flat style instead of the
    filled one, and a toast can carry one action button, such as Undo, at
    its end; pressing it closes the toast.

Every toast now stays a fixed time: 4 s, a Material 3 snackbar's time,
or 8 s when it has a button, to read it and reach the button. The
duration arguments are gone, so errors, the clipboard size warning and
the bulk profile update no longer set their own times. A toast with a
button hides the close button, so the button reaches the toast's end._

  • Feat(rules): give each new rule a numbered name.
    A new rule was always called "Rule", so several new rules looked alike in
    the list. Add a random four-digit number, such as "Rule 4821".

  • Fix(rules): accept IPv6 addresses in IP CIDR fields.
    The IP CIDR check took only IPv4, and an unescaped dot in its pattern let
    any character stand between the last two numbers, so 1.2.3x4 passed.
    Parse the address with Dart's own IPv4 and IPv6 parsers instead, and
    allow a prefix of up to 32 or 128 bits.

  • Fix(makefile): ship the Windows portable package as a real zip.
    _The portable package is repacked with tar -a, which picks the archive
    format from the file extension. The recipe runs in Git's sh, where tar
    is GNU tar. GNU tar does not know .zip, so it quietly wrote an
    uncompressed tar under a .zip name; v4.0.4 and v4.1.1 both shipped that
    way. Windows' own zip support shows the file as empty and Expand-Archive
    rejects it, so only tools that read the content, such as 7-Zip, could
    open it.

Call Windows' own tar.exe, which is bsdtar and writes a real, compressed
zip for -a. Checked by running the recipe with a stand-in fastforge: the
result opens in Explorer and in Expand-Archive, with the Hiddify folder
intact.

Reported in #2348._

  • Fix(profile): drop .mobileconfig from the accepted import formats.
    A .mobileconfig holds an Apple IKEv2 profile. Hiddify is a sing-box
    client and cannot read one, so offering it in the picker only ever led
    to a failed import, with a message that reads like a broken file rather
    than an unsupported format.

  • Fix(router): keep the back button working after a settings sub-page.
    _The PopScope that sent back to home lived inside the settings branch. Branches
    stay mounted after the user leaves them, so it kept answering presses meant for
    other branches and the app could no longer be closed with back at all. Reported
    upstream as flutter/flutter#193098.

Move the handling to the shell, where exactly one instance exists and it always
belongs to the branch on screen. The NotificationListener beside it keeps the
platform told that Flutter handles back; without it Android stops believing that
once a branch pops a page, and closes the app without calling Flutter.

Verified on an Android 16 emulator with both the back button and the edge swipe._

  • Feat(profile): accept more config formats when importing from a file.
    _The picker allowed .txt and .json only, so a Clash .yaml, a WireGuard
    .conf or an .ovpn had to be opened and pasted by hand. It now takes
    .yaml, .yml, .conf, .ovpn and .mobileconfig as well.

Widening it exposed two ways the read could fail. utf8.decode is strict,
so one byte that is not valid UTF-8 threw and took the whole import with
it; it now runs with allowMalformed, and a UTF-8 byte order mark is
skipped rather than left in front of the first character, where it
breaks JSON parsing. The path was also dereferenced with a bang, which
the web build never has._

  • Fix(profile): name the real host in the deep-link import warning.
    The confirmation parsed the deep link itself, whose host is always the
    literal "import", so every link claimed to come from the same place. It
    now unwraps the link to the subscription URL first and reports that host.

  • Fix(connection): log a failed connection at error, not info.
    Every status went through one info call, success and failure alike. A
    failure carries the core's own panic text, which is the last thing that
    should need hunting for among the ordinary comings and goings — and the
    app log file now keeps errors only, so at info it was not kept at all.

  • Feat(logs): rebuild the logs page.
    _The page used to show only what the engine said, from a buffer that
    started empty every time it opened. It now reads the shared ring, so it
    holds every category and already has history the moment it opens.

The list reads like a terminal: oldest at the top, newest at the bottom,
and the view follows the newest line until you scroll away from it. A
long press or a right click copies one line.

Three chips and a search field sit under the list, next to the newest
line rather than the oldest. Two of them hide rows; the third is the
core's log level, which changes what gets recorded, so a toast says it
lands on the next connection. Below 700px the search takes its own row.

Sharing became a choice rather than two buttons: the visible history,
which can be filtered again before it leaves, or one of the files, each
listed only when it exists and has something in it. A history too long
for the clipboard falls back to a file.

Both sheets go through BottomSheetsNotifier, so they get the app's own
width, corners and route rather than a bare showModalBottomSheet._

  • Refactor(logs): read the shared ring instead of the engine's stream.
    _Four pieces did one job. The engine kept its own buffer of the last 300
    messages and re-emitted the whole list on every line, a parser turned
    those messages into an entity, a repository wrapped the parser, and the
    page watched the repository.

The engine already writes each message straight into the shared ring:

coreLogFor(event.type).log(getLogLevel(event.level), event.message)

so the buffer, the parser, the entity and its failure type are gone. The
repository's last job — making sure the file exists — moved into the
sink that opens it, which leaves nothing for the repository to hold.

The path resolver now points at the engine's real crash file rather than
the goroutine dump it writes at every start. That dump is not a crash
report: it exists on a healthy machine and says nothing about a failure._

  • Feat(logs): translate the renamed app log entry.

  • Feat(logs): name the app log file for what it now holds.
    The file used to hold everything the app recorded. It now holds errors
    from the app and the core alike, so "App log file" named the wrong two
    things: the wrong source, and the wrong amount.

  • Feat(logging): keep only errors in the app log file.
    _The file exists so a crash leaves something behind. Everything quieter
    than an error is already somewhere better: the page reads the in-memory
    ring, and the core keeps its own complete log in data/box.log.

Narrowing it to errors removed three mechanisms that only existed to
make a busy file affordable — the write batching, its timer, and the
size cap with the trim that enforced it. Errors are rare, so each one is
written straight through and is on disk before the crash that follows
it. The file grows by the handful, so it no longer needs bounding.

Four tests cover what the sink now promises._

  • Feat(logging): three log categories instead of five.
    _The page filtered by five prefixes: core, app, ui, infra, boot. Two of
    them told the reader nothing — a logger named HomePage already says it
    is a widget — so ui is gone and PresLogger writes app like AppLogger.

The remaining three become a type rather than a prefix string, and app
is defined as whatever boot and core do not claim, so loggers from
packages that never heard of our naming still land somewhere.

The ring also returns oldest first now, which is how a log reads on the
page, in the clipboard and in a file, and drops the per-category counts
nothing was reading._

  • Feat(logging): drop debug mode, keep only the log level.
    _Two controls produced one outcome. The app had a debug mode switch it
    passed to the core at startup, and the core turns the same flag on by
    itself whenever the level is trace or debug:

    static.debug = static.debug || static.logLevel <= LogLevel_DEBUG

So the two could disagree — switch off, level debug, core in debug, app
believing otherwise. The switch is gone and the app reads the level with
the same rule the core uses, which is what isVerbose says.

The buffer size preference lands here too. It answers the same question
as the level does: how much gets recorded._

  • Feat(logging): give every level one name.
    _package:logging spells its levels FINEST, FINE, INFO, WARNING, SEVERE.
    The engine says trace, debug, info, warn, error. Both names appeared on
    the same page, next to each other, for the same idea.

The engine's win, since those are the ones the user picks from, and the
log file now writes them too. uiLevels is the subset a filter can offer:
FINER and CONFIG are never produced, and SHOUT never needs its own entry
because the filter is a floor and error already covers it._

  • Feat(logs): translate the new logs page.
    _The same 17 strings in the other ten locales, written from the English
    rather than from each other, and anchored on the words each language
    already uses for core, app and log so the page reads like the rest of
    the app.

Drops the two old share buttons and the Settings strings for debug mode
and log level, which no longer exist in the base locale._

  • Feat(logs): word the new logs page.
    _Adds the strings the redesigned page needs: the three filter chips, the
    share sheet's sources, the export action and its match count, and the
    buffer chip in the app bar.

Drops shareCoreLogs and shareAppLogs, since sharing is no longer two
buttons, and the Settings strings for debug mode and log level, since
neither setting lives there any more._

  • Fix(driver): stop the driver extension from blocking the keyboard.
    _enableFlutterDriverExtension registers a TestTextInput, which takes over
    the text input channel. Every debug build called it, so no text field in
    the app could accept a real keyboard — the search box on the logs page
    would focus and show a caret, and then swallow every keystroke.

It now runs only when asked for, so a normal debug build types again and
the driver is still one flag away._

  • Feat(logging): keep the log file across runs, and point at the engine's real one.
    _Three changes to what lands on disk.

The file is no longer emptied at launch. A crash leaves its evidence in the
previous run's lines, and wiping on startup threw exactly that away. Growth is
bounded instead: past 5 MB the older half is dropped and the newer half kept,
checked every 500 lines rather than on every write. Each run writes a marker
line, since one file now spans several.

Engine lines are filtered to warnings and above. The engine keeps its own
complete log, so repeating its routine chatter here only flooded the file and
ate the cap — while an engine failure still sits next to the app line that led
to it.

coreFile() pointed at /box.log, which nothing writes. The engine
logs into /data/box.log — so "share core logs" has been handing
over an empty file. Adds coreHangFile() for the goroutine dump the engine
writes beside it, which is what a hang needs._

  • Fix(logging): point the origin at the caller, and say where logs appear.
    _Two defects the first real run exposed, neither visible to the analyzer.

The origin pointed at the logger itself. error() captures StackTrace.current
inside the extension, so frame zero was log_level_compat.dart rather than
whoever called it. Frames from core/logger are skipped when picking the origin
now, and the '.' suffix is dropped — it said nothing the
file and line did not already say, and splitting it on whitespace produced
'closure>'.

before status <- LogLevelCompat.error (log_level_compat.dart:29)
after status <- HiddifyCoreService.startListeningStatus (hiddify_core_service.dart:442)

A terminal showed no logs at all. dev.log writes to the VM service, never to
stdout — verified with a probe: print and debugPrint appear there, dev.log does
not. Printing every record as well would fix the terminal but show everything
twice in an IDE console, so stdout gets one notice at startup naming the three
places records do appear.

CI is unaffected: build failures come from the compiler and the flutter tool,
never through the app's logger._

  • Fix(logging): let a phone share its app logs.
    _A phone in release keeps no app.log — writing one continuously costs storage
    nobody reads — so the share menu was hidden there entirely and the user had no
    way to send app logs at all.

The history is in memory anyway. Sharing now writes the ring out to
app-logs.txt at the moment the user asks, so the file exists exactly when it is
needed and never otherwise. Continuous cost stays zero, and the menu is useful
on every platform except web.

The export uses its own file name rather than app.log, which the file sink is
still writing to on desktop.

Engine logs were never affected: box.log is written by the engine itself on
every platform._

  • Refactor(logging): remove loggy.
    _The last step of the switch. Sentry becomes a listener like every other sink,
    the file sink is attached, and loggy's plumbing is deleted.

    sinks/sentry_sink.dart replaces analytics_logger.dart. Same rule as before —
    info and up becomes a breadcrumb, severe and up
    becomes a reported event — but as a subscription the
    analytics toggle attaches and cancels, rather than an
    entry in a printer map. Levels map onto Sentry's five
    properly now: SHOUT is fatal instead of collapsing
    into error.

    bootstrap.dart the three LoggerController calls become logStart,
    logAddFile and logFinish. This reattaches app.log,
    which had no writer since the mixins switched.

Deleted: logger_controller.dart, custom_logger.dart, analytics_logger.dart, and
the dependency itself. Nothing imports loggy any more.

Whether to keep the log file is unchanged in effect but clearer to read: worth
its cost on desktop or while debugging, pure waste on a phone in release._

  • Feat(logging): split engine logs by their type.
    _Every LogMessage from the engine already carries a type — CORE, SERVICE or
    CONFIG — and hiddify dropped it, so a failed profile import and a thousand
    connection lines landed in the same pile.

    core.core the proxy engine: connections, routing, dns
    core.service the background tunnel: start, stop, permissions
    core.config reading and validating a profile

Nothing new is transmitted; the field was already on the wire. When a profile
fails to import you can now ask for core.config alone.

Also renames the boot logger to boot.bootstrap, after the file it reports on._

  • Refactor(logging): make the category impossible to get wrong.
    _Nothing outside custom_loggers.dart builds a Logger any more. A name without a
    category would quietly break the logs page filter, so the only way to get one
    is through a mixin or a factory, and both stamp the prefix.

Classes use a mixin. Static and top level code cannot — a mixin needs an
instance — so it uses the matching factory. UriUtils, the FFI interface and the
preferences provider were all constructing loggers by hand; they now go through
infraLogger() and appLogger().

Adds CoreLogger, so the engine bridge is a category like the rest rather than a
bare name, and boot., which separates startup from the app layer. Startup logs
are what you read first when the app fails to start, and they were mixed in
with everything else.

Drops LoggerMixin. It declared "this class has a loggy" for ExceptionHandler,
which never used it._

  • Refactor(logging): switch the three mixins to package:logging.
    _The step that moves every app log at once. PresLogger, AppLogger and
    InfraLogger now hand out a package:logging Logger.

The getter keeps the name loggy, and log_level_compat is re-exported from
this file, so the ~200 classes that write logs are untouched. Only five call
sites needed editing — the ones that passed loggy's own LogLevel as an
argument, which is a type the extension cannot alias away.

Logger names gain a layer prefix: ui., app. and infra. That is what will let
the logs page filter a whole layer, and taking the rest from $runtimeType means
a renamed class cannot leave a stale name behind.

Note app.log receives nothing until the next step. loggy's file printer still
owns the file but no longer has any writers, and the replacement file sink is
not attached yet — two IOSinks on one path would truncate each other._

  • Feat(logging): say where an error came from.
    _Every log line that carries a stack now ends with the method, file and line it
    came from, taken from the first frame belonging to this package:

    could not save the profile <- ProfileRepository.save (profile_repository.dart:88)

It is appended to the message rather than kept in a separate field, because the
message is the one thing DevTools, the logs page and app.log all display.

error() and fatal() capture StackTrace.current when the caller does not pass
one, so a bare loggy.error('...') still knows its origin. Errors are rare
enough to afford the stack walk.

The stack handed to dev.log is trimmed to this package's frames as well. A raw
trace is about twenty frames, nearly all of them inside Flutter, and the
console printed every one — three lines instead of twenty-two, and the ones
that survive are still clickable in the terminal._

  • Fix(logging): stop swallowing the Flutter error report.
    _FlutterError.onError defaults to presentError, which prints the full formatted
    report. Assigning a handler replaced it, so an error that used to arrive as a
    complete report with the widget context became a single terse line — which is
    why crashes have been hard to read.

    • presentError is called again in debug builds
    • details.library and details.context are now logged; exceptionAsString()
      leaves both out, and they are the parts that say which subsystem failed and
      what it was doing

Also completes log_level_compat with trace() and fatal(), so all six levels the
engine can produce have a short name, not just the four loggy happened to have._

  • Refactor(logging): move the crash hooks off loggy.
    _Both uncaught-error handlers now write through package:logging. The class keeps
    the name Logger so the existing Logger.bootstrap.info(...) call sites are
    untouched; package:logging's own Logger is aliased to avoid the clash.

Adds log_level_compat.dart, which aliases loggy's debug() and error() onto
fine() and severe(). This is the single reason the migration does not have to
edit the ~200 classes that write logs.

A Dart extension is only visible where it is imported, so it is re-exported
from logger.dart rather than added to every call site._

  • Refactor(logging): move engine logs onto the new pipeline.
    _First real traffic through package:logging. App logs still go through loggy.

The level mapping was rewritten and fixes two defects:

TRACE had no case at all and fell into the default, so engine trace lines
arrived as INFO — they looked as important as normal traffic and could not be
filtered out. FATAL collapsed into error because loggy had nothing above it.
Six engine levels now map onto six distinct levels, FINEST through SHOUT.

The per-line split is gone. It existed defensively, but the engine already
sends one line per message, so it only allocated a list and multiplied the
per-record work during a connect burst.

Engine records now carry the name core, so the logs page can filter the whole
engine in or out with a single prefix check.

Until the file sink is attached, engine lines are absent from app.log. Nothing
is lost: the logs page still reads the gRPC stream directly, and box.log is
written by the engine itself._

  • Refactor(logging): run the new pipeline alongside loggy.
    _Adds the replacement for LoggerController and starts it in bootstrap. Still no
    behaviour change: every class logs through loggy, so Logger.root has no writers
    and the console and ring sinks receive nothing.

Where loggy took a single printer and needed a class to fan out to the rest,
the root stream accepts any number of listeners, so each destination is just a
subscription that can be attached or cancelled.

Two sinks are deliberately absent until loggy is gone:

  • the file sink, because loggy's FileLogPrinter still owns app.log and two
    IOSinks on one file would corrupt it
  • Sentry, which needs the real SDK integration rather than the bench's
    counting stand-in_
  • Refactor(logging): add the ring buffer and file sink, wired to nothing.
    _First step of moving off loggy. These files are inert: nothing imports them
    yet, so behaviour is unchanged.

    ring/log_ring.dart bounded history the logs page will read. Stores
    LogRecord objects, not formatted strings, and bumps a
    revision counter instead of publishing a stream, so one
    log line does not become one stream event.
    sinks/file_sink* three files behind one conditional export. The compiler
    picks the dart:io version natively and a no-op stub on
    web, which replaces the kIsWeb checks.

package:logging is added next to loggy rather than replacing it, so both
systems can coexist until the last step._

  • Fix(debug): import foundation.dart for kDebugMode.
    material.dart re-exports foundation.dart with a show list that does not
    include kDebugMode, so both entrypoints failed to compile. app.dart
    already imports foundation.dart explicitly for the same reason.

  • Feat(debug): enable flutter_driver in debug builds.
    _Both entrypoints register the flutter_driver extension behind kDebugMode,
    so a running debug app can be driven from outside — taps, text input,
    scrolling, reading widgets, screenshots. kDebugMode is a compile-time
    const, so release builds drop the branch entirely.

flutter_driver is declared as a normal dependency, matching how
accessibility_tools is already handled._

  • Refactor(i18n): fix the notAvailabeInThisPlatform key spelling.
    _The key was missing an "l" in "Available". Renamed with dart run slang edit move, which rewrites the key across all eleven locale files in one pass, per
    the tooling note in CLAUDE.md.

Key name only — no translated string changes, so this needs no separate en
and locale commits._

  • Chore(l10n): carry the routing empty state into the ten locales.
    _Follow-up to the two previous commits. Every locale had translated the "+
    button" and the tap gesture literally, so all ten needed the same rewrite,
    translated from the new English text rather than from another locale.

Also adds routeRule.add, the FAB's accessibility label, per locale.

Terminology follows each locale's own routing.title and
predefinedRules.title, so "routing rules" and "predefined rules" read the same
here as on the rest of the screen._

  • Fix(a11y): name the routing rules FAB and its menu for screen readers.
    _The FAB carried only an add icon, so a screen reader announced it as an
    unnamed button. It now takes a tooltip, which serves as both the hover label
    and the accessibility name: "Add rule" when closed, "Close" when open.

The two menu buttons had their names in a sibling Text widget, so each button
was also announced bare. MergeSemantics joins each pair into one node.

Both menu buttons were built even while the menu was closed and hidden with
Opacity(0), which hides pixels but keeps the widget hit-testable and in the
semantics tree — they were clickable and reachable while invisible. Naming
them would have made that worse, so they are now wrapped in IgnorePointer and
ExcludeSemantics while closed._

  • Fix(i18n): drop the tap gesture from the empty routing rules state.
    _"Tap the + button to create a new rule or choose from predefined rules" was
    wrong on the desktop builds, where the button is clicked with a mouse, and it
    named the control by its icon shape, which a screen reader user never sees.
    It also promised one action while the button opens a menu of two.

Naming the two choices instead removes the gesture entirely, so one string is
correct on every platform, and the wording now matches the two menu labels —
the consistent-verbs rule in .claude/writing/m1-writing.md. Per
.claude/writing/m1-empty-states.md the tagline names the topic and no longer
reads like a button.

Adds routeRule.add for the button's accessibility label, used in the next
commit._

  • Chore(l10n): carry the English wording fixes into the ten locales.
    _Follow-up to the previous commit. Each string was translated from the new
    English text, not from another locale, and kept in the v2ray/VPN context.

18 strings per locale, 19 for fa. Four of the English fixes were rules about
English only and changed nothing here: the "don't" to "do not" contraction,
"which" to "that", and the two "config" spellings, which every locale except
fa had already spelled out. fa gets the extra edit for the same reason —
"کانفیگ" to "پیکربندی".

Typography kept per language: fr keeps U+202F inside its guillemets, and the
two Chinese locales keep their fullwidth punctuation._

  • Fix(i18n): apply the writing rules to the English strings.
    _First pass over en.i18n.json against the rules captured in .claude/writing/ —
    the three Material Design 3 files, plus the older Material pages for the
    subjects M3 does not cover. 22 strings changed:
  • "Remember my choice" mixed first person into UI text (m3-word-choice)
  • the two version labels dropped their trailing colon (m1-writing)
  • seven confirmations lost the polite run-up: "Are you sure you want to
    permanently delete this profile?" is now "Delete this profile? You can't
    undo this." (m1-writing, and m3-ux-writing-best-practices on consequences)
  • an em dash and a hyphen-as-dash became periods (m3-grammar-and-punctuation)
  • the unverified-link warning takes "do not" over "don't" for weight (ditto)
  • "Warning" became "Check this link first", so the title names its topic
    (m3-ux-writing-best-practices)
  • "config" spelled out, and "enable" became "turn on" (m3 abbreviations,
    m1 jargon)
  • the chain filtering text lost its side-note parentheses and split into two
    sentences (m3-grammar-and-punctuation)
  • two strings moved into second person, two ungrammatical ones fixed_
  • Fix(ui): move the version label separator out of the translation.
    _The M1 writing rules in .claude/writing/m1-writing.md say a label takes no
    colon. The two version labels in the update dialog carried a trailing ": ",
    and the dialog glued them straight onto the version number, so dropping the
    colon would also drop the space and render "Current version1.2.3".

Put the space in the widget instead. The labels lose the colon in the next
commit, and a translator can no longer break the spacing in ten locales by
trimming a trailing space._

  • Fix(readme): render the Google Play badge in local previews.
    _The badge pointed at a github.com /blob/ URL. GitHub redirects those to the
    raw bytes for images, so it rendered there, but every other renderer gets an
    HTML page back and draws a broken image.

A relative path works in both places, and drops the stale hiddify-next repo
name at the same time._

  • Docs(i18n): move the writing rules out of docs/ into .claude/
    docs/ is not a documentation tree here - it holds one image, the Play Store
    badge, which six README files link to through a GitHub URL. The nine writing
    files are read by Claude, not published, so they belong beside the other
    Claude project files.

  • Docs(i18n): replace copied writing rules with captured sources and links.
    _CLAUDE.md carried a 48-line summary of the Material writing rules with no
    clear provenance, plus slang command documentation copied from pub.dev.
    Both are now sourced.

The nine writing docs under docs/writing/ each carry their source URL and
capture date, so they can be refreshed later:

  • three from the M3 style guide (all it has)
  • six from the older Material site, for subjects M3 does not cover

slang is not captured at all. Its README is one page, so CLAUDE.md keeps a
link map into it instead, one line each on which section answers what.

Dropped along the way: the standard button labels, which the confirmation
page turns out never to define; the per-language exception list; and the
rule about preserving placeholders and keys.

The remaining prose is only project decisions - context, English first,
long strings wait for approval, en commits alone._

  • Chore(l10n): apply the Material Design 3 rules to the ten locales.
    _Carries 3662764 into every locale, following the rules now recorded in
    CLAUDE.md rather than copying English shape.

Colons dropped from all 19 proxyInfo labels in each locale, 190 in
total, including the full-width colons the Chinese files used.

Quotation marks follow each language instead of the ASCII quotes most
files carried: guillemets for Arabic, Persian and Russian; French adds
its narrow no-break spaces; curly quotes for Spanish, Indonesian,
Portuguese, Turkish and Simplified Chinese; corner brackets for
Traditional Chinese. Simplified Chinese also loses the spaces it had
around them, which do not belong with full-width marks.

The ellipsis character replaces three dots in the five in-progress
strings per locale.

Turn on / turn off replaces enable / disable where the language draws
that distinction: Spanish moves from habilitar to activar, Turkish to
aç / kapat, both Chinese to 开启 / 关闭. Arabic, Persian, French,
Indonesian, Portuguese and Russian already used their turn-on verb.
The IPv6 dropdown becomes a state rather than an action in every
locale: Off / On, Desactivado / Activado, Kapalı / Açık, 关闭 / 开启.

The link warning loses its "we" in all ten, matching the English fix.

please / por favor / veuillez / harap / пожалуйста / lütfen / 请 removed
from the Psiphon licence text.

Final periods added where a string has two or more sentences:
contentTooLarge, unblocker.msg, and useXrayCoreWhenPossibleMsg, which
was missing one in six locales — using 。 for the two Chinese files.

Verified: zero violations across all eleven locales for colons after
labels, three-dot ellipsis, straight quotes, missing final periods and
exclamation points. dart run slang analyze --full stays clean._

  • Chore(i18n): drop "we" from the unverified-link warning.
    Material says to avoid "we" unless a real person is acting for the
    user. "We can't verify this link" made the app speak as a company in
    the one dialog where the user needs a fact, not a voice.

  • Chore(i18n): bring English fully in line with Material Design 3.
    _Applies the rule set just written into CLAUDE.md, including the two
    rules the old summary had wrong.

Colons dropped from all 19 proxyInfo labels. Material says to skip
colons after labels, and the layout makes it worse than style: the row
is spaceBetween, so the label sits far left and the value far right,
leaving the colon stranded in empty space. This reverses the isGroup
change from e6192f5, which added a colon to match its siblings when
all 19 were wrong. newVersion.currentVersion keeps its colon because
that TextSpan is followed immediately by the version number.

Periods added to the two multi-sentence strings that lacked them,
contentTooLarge and chain.levels.unblocker.msg. Both already used a
period between their sentences, so they contradicted themselves.

Curly quotes and apostrophes replace the straight ones, 16 quote pairs
and 9 apostrophes. As a side effect the escaped " sequences are gone
from the JSON.

The ellipsis character replaces three dots in the five in-progress
strings. The truncated-path example keeps literal dots.

Enable/Disable become Turn on/Turn off across labels and prose. The
IPv6 route dropdown reads Off/On instead, since those are option values
rather than actions.

"Please read them carefully before proceeding" loses the "please" and
the stiffer verb. The WARP consent switches to active voice.

The auto-selection dialog dropped the passive "was disabled due to the
region change" for "was turned off because the region changed"._

  • Docs(i18n): write the full Material Design 3 rules into CLAUDE.md.
    _The principles list compressed MD3 to "sentence case, concise, no
    trailing period on short UI strings, action-oriented". That was enough
    to miss real rules, and I did miss two of them: colons are meant to be
    skipped after labels, and periods ARE required once a string holds more
    than one sentence. The old wording implied the opposite of both.

Replaces it with the actual rule set from Material's writing style
guide, the M3 grammar and punctuation page, and Material's error
patterns page, grouped by capitalization, punctuation, voice and tone,
word choice, errors, and buttons.

Also records how far the rules travel into the other ten locales, since
several are English-only or script-specific: letter case does not apply
to Arabic, Persian or CJK; contractions and singular "they" are English
only; each language keeps its own quotation marks and spacing; and
plurals follow that language's real CLDR forms._

  • Chore(l10n): sync the ten locales with the reworked English.
    _Follows f73458d and 5f2b8e6, which changed 40 English strings. Around
    half of those changed meaning rather than wording, so the existing
    translations were wrong, not merely stale:
  • the clipboard entries said "add" in every locale, but the action is a
    copy: نسخ, Copiar, کپی, Salin, Копировать, kopyala, 复制, 複製
  • isGroup was a sentence ("Is group") and is now a label, so its ten
    translations gain the colon their siblings already had
  • both license messages became two short sentences naming the fix
  • badResponse names the server, invalidConfig went singular
  • experimentalNotice points at Settings by the name each UI uses
  • eight toasts dropped "successfully" in all ten languages

Fills every gap the analyze report has been carrying:

  • remainingDuration and bulkSuccess return as real plurals, in each
    language's own CLDR forms. Arabic gets six (zero/one/two/few/many/
    other), Russian four with the noun case changing per form, Indonesian
    Turkish and both Chinese a single "other" because those languages do
    not inflect a noun after a number. This is the bug that started the
    thread: the old hardcoded string could not have been right anywhere.
  • remainingUnlimited, openWebPage, support, openLinkTitle,
    openLinkAction and the five unknownDomainsWarning strings
  • French was missing addProfileByDeepLinkWarning and
    importRouteRuleByDeepLinkWarning outright, so French users saw
    English on both, one of them a security prompt

Removes youAreAboutToVisit and thisWebsiteIsNotInOurTrustedList, the
two keys that no longer exist in English. They were the only entries in
the unused report, which is now empty for all ten locales.

Sentence case in the routing hints reaches id, pt-BR and tr, the three
locales that had copied English's old Title Case; es, fr and ru never
had it.

avoidIfUnsure keeps the wording that was already there, per review.

dart run slang analyze --full now reports nothing missing and nothing
unused across all eleven locales._

  • Chore(i18n): keep "connection profile" in the empty-profile message.
    _The previous shortening dropped "connection" from "connection profile".
    That is the term the rest of the UI uses, and someone seeing this
    message has no profiles yet, so "profile" alone gives them nothing to
    attach the idea to.

Restores the full term, brings back a friendly opening, and links the
two paragraphs: the first now asks for VPN server details, which is
what the second offers to help create._

  • Chore(i18n): tighten English against UI-writing and warning guidance.
    _Second pass over the base locale, after reading Material Design 3, the
    Microsoft UI text style guide, and Mozilla's research on security
    warnings. 39 strings.

Warnings now name the concrete risk instead of the abstract one, which
is the change Mozilla measured a 22-50% drop in warning-dismissal from:

  • importRouteRuleByDeepLinkWarning said importing from untrusted
    sources "can be a security risk"; it now says route rules decide
    where your traffic goes. Its title also loses a stray hyphen.
  • ownResponsibility read as a liability disclaimer that blamed the
    user ("Any loss from misuse is your own responsibility") and is now
    "You continue at your own risk".
  • avoidIfUnsure is deliberately left alone.

Errors state the fix rather than the mechanism. The WARP and Psiphon
license messages were one passive sentence naming the feature twice;
both are now "This profile uses X. Accept the X license to continue."
"Bad response" and "Bad certificate" say which thing is invalid.

Filler removed: eight toasts dropped "successfully", which adds no
information and inflates every translation of them.

Verbs match the action: clipboard entries said "Add"/"URL to
clipboard" where the action is a copy, and the QR entries omitted
"code". Note this makes the existing ten translations of these keys
wrong rather than merely stale.

Sentence case finishes the job started in dfea6ac: the routing hints
still capitalised "Process Name", "Process Path", "Port Range" and
"Domain Suffix", and several locales had copied that.

proxyInfo labels read like field dumps ("Is selected:", "Is group");
they are now plain labels, and isGroup gains the colon its siblings
all had.

One screen, one name: "config options page" now points at Settings,
and "Quick setting options" and "Route general options" are named the
way the UI names them.

noActiveProfile drops from 44 words to 25, loses the marketing aside
and the curly apostrophe, and stops using "connection" three times in
one sentence._

  • Fix(l10n): apply the translation principles to all ten locales.
    _Reviewed every non-English locale line by line against the project
    translation principles. Also drops remainingDuration and bulkSuccess,
    which became slang plural maps in the base locale, so the flat strings
    the ten locales still carried no longer matched that structure; both
    fall back to English until the next translation pass.

Wrong syntax in a user-facing hint (ar, pt-BR, ru, zh-CN, zh-TW):
validPortRangeEx taught "1-65000", but the app parses a colon. Five
locales share the defect, so it came from an older English string
rather than five independent mistakes.

Text that was never translated (tr, zh-TW):

  • tr memoryLimit was the English "Memory limit", sitting directly above
    its own Turkish description
  • tr tap_license read "Licence"
  • zh-TW enableAnalyticsMsg had an English "and" mid-sentence

Wrong script in the Traditional Chinese file:

  • simplified 护/过/规则 replaced with 護/過/規則
  • addProfileByDeepLinkWarning had been copied from zh-CN wholesale:
    simplified vocabulary (添加/配置 instead of 新增/設定檔) and ASCII
    quotes instead of 「」
  • directPort said 本地 (zh-CN) where Taiwan uses 本機, as the next line
    already did

Terms the files spelled two different ways:

  • id: proxy/proksi and Fragment/fragmen/Fragmentasi, now one form each
  • fa: Psiphon, Fragment, TUN and IP each had a transliterated variant
  • ar: WARP was spelled وارب, fragment as التجزئة, Telegram as التيليجرام
  • ru: one stray "Warp" among six correct "WARP"
  • padding is kept in English in the six Latin-script locales, matching
    how they already treat Fragment and Balancer; the CJK locales keep
    填充 because they localize systematically

Sentence case per Material Design 3, respecting each language:

  • Title Case removed from id, pt-BR, tr, es and the license dialogs
  • es and pt-BR continue lowercase after a colon; tr keeps its capital,
    because TDK capitalizes a full sentence after one
  • fr regions.cz is now "République tchèque"; pt-BR keeps "República
    Tcheca", which is the correct official form there

Mistranslations:

  • id mixedSniCase used "kasus" (a legal case) for letter case
  • fa delay.timeout said the test failed rather than timed out

Trailing periods dropped from activeProfileName in the seven locales
that had them, clearListMsg switched from past to future tense in all
ten, and lanSharing regained the "secure" that English gained in
39539c6. es fixes two strings that slipped into formal usted where the
file otherwise uses tú, one of them a security warning._

  • Refactor(profile): use the plural remaining-days string.
    remainingDuration is now a slang plural taking a num, so the ">365 days"
    branch can no longer pass the infinity glyph through it. Read the
    dedicated remainingUnlimited string instead.

  • Chore(i18n): align English strings with Material Design 3.
    _Apply the project translation principles to the base locale:

  • sentence case: "QR code", "Fragmentation packets", "Psiphon license
    agreement", "Extra security & unblocker", and the Psiphon license
    links, which now match the WARP dialog wording
  • drop trailing periods from the two short strings that had them
  • restore standard casing for technical terms: WARP, TUN
  • reword six strings to be concise and action-oriented, e.g.
    "Reconnecting to apply changes..." and "Cannot be empty"
  • turn the two hardcoded plurals into slang plurals so a count of 1 no
    longer renders "1 days"; the infinite case moves to the new
    components.subscriptionInfo.remainingUnlimited key_
  • Chore(analysis): clear the last analyzer infos.
    _Redundant async removed from three functions that never await, and
    EdgeInsets.all(0) replaced with EdgeInsets.zero.

The two empty catches in closeFront now log at debug level. Only one of
the two close modes matches how the channel was opened, so the other is
expected to throw; swallowing it silently also hid any real failure.

Three cases are silenced rather than changed. jsonObject stays dynamic in
profile details because that parsing belongs to another package and is due
for removal. The bare bgClient statement in isInitialized is a deliberate
read of a late field, now written as a discarded local. And throttle only
reads the timing of its window stream, never the value, so the bool inside
it is meant to be dropped.

Analyzer is clean: 243 diagnostics at the start of this branch, 0 now.
All 119 tests pass._

  • Build: declare flutter_test as a dev dependency.
    It was commented out, so all six test files imported a package the project
    did not declare and the analyzer flagged each one. flutter_test was already
    resolving transitively: enabling it changes exactly one word in the
    lockfile, transitive to direct dev, with no version moving. All 119 tests
    pass and the release build is unaffected, since dev dependencies are never
    compiled into the app and flutter_test ships with the SDK.

  • Chore(analysis): drop the last two deprecated Riverpod APIs.
    _ProviderScope(parent:) nested a second container under the one bootstrap
    builds, so where a provider ended up depended on whether bootstrap or the
    UI touched it first, and a parent-side provider could never reach an
    instance created in the child. UncontrolledProviderScope reuses the single
    container, so the observers move onto ProviderContainer itself.

showIp existed only to give IPText a bool and buzz on reveal, and
Ref.listenSelf is going away in 3.0. The state is local to one widget, so
it is now a useState hook and the haptic fires from the tap handler. Two
consequences: each IPText keeps its own state (only one is ever on screen
today), and the 20 second disposeDelay grace period is gone.

  • Refactor(dialog): move radio selection onto RadioGroup.
    Flutter 3.32 deprecated groupValue and onChanged on Radio and its list
    tiles; the group state now belongs to a RadioGroup ancestor. The three
    selection dialogs each wrap their Column in a typed RadioGroup and the
    tiles keep only their value.

  • Chore(analysis): use plain Ref instead of the deprecated per-provider refs.
    _riverpod_generator still emits AppUpdateRepositoryRef, EnvironmentRef,
    LogRepositoryRef, LogPathResolverRef and StatsRepositoryRef, but all five
    are deprecated and go away in Riverpod 3.0. The provider functions now take
    Ref directly, and RefLifeCycle extends Ref instead of AutoDisposeRef.

Verified against riverpod 2.6.1: keepAlive(), onCancel, onDispose and
onResume all live on Ref, so disposeDelay keeps working unchanged.
AutoDisposeRef only added the deprecated maintainState, which is unused.

Ref itself is not exported by riverpod_annotation, so the four provider
files now import hooks_riverpod. Generated code was rebuilt to match._

  • Chore(analysis): replace deprecated withOpacity with withValues.
    _Color.withOpacity loses precision and is deprecated in favour of
    withValues(alpha:). Seven call sites converted across the skeleton
    placeholder, both work-in-progress connection buttons, and the active
    proxy card.

The active proxy card also dropped a withOpacity(1) outright: alpha 1
is what the surface colour already was, so the call did nothing._

  • Chore(analysis): clear the remaining analyzer warnings.
    _Removed what was provably dead: three duplicate arms in the connection
    button image switch (unreachable since the switch was written in 1bdae7a),
    the orphaned _debugUpgrader constant, an unused _logger and its import, the
    _keyMap left over from an unfinished gRPC factory, and an unread local in
    HiddifyCoreService.stop().

Two intentional cases were kept and silenced instead: the && false guard
in LoggerController (added upstream in b5f6a06 for iOS stability) and the
defensive default arm in getCoreLogLevel.

_sanitize now writes to a local instead of its own parameter.

Behavior is unchanged throughout; the analyzer reports zero warnings._

  • Chore: apply dart fix across lib and test.
    _Automated cleanup only: unused imports dropped, redundant break removed
    from Dart 3 switch cases, Key? key folded into super.key, final
    promoted to const, braces added to single-line flow control, imports
    sorted, and the deprecated ColorScheme.background swapped for .surface
    (same value, new name).

One fix was deliberately reverted: dart fix removed
withTrustedRoots: false from the mTLS SecurityContext because false is
already the default. The core connection must trust only its pinned
certificate, so that intent stays spelled out with a local ignore._

  • Chore(analysis): exclude generated and vendored code from the analyzer.
    _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._

  • Feat(settings): give VPN sharing a permanent generated password.
    _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._

  • Chore(l10n): drop the removed VPN sharing password keys.
    Mirror the English change in the other ten locales: lanSharingPassword
    and lanSharingPasswordNotSet no longer exist.

  • Feat(settings): rename VPN sharing to Secure VPN sharing.
    _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)_
  • Feat(profile): merge enable-warp and enable-psiphon into extra-security.
    _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._

  • Feat(profile): let a subscription tune fragmentation via a fragment header.
    _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._

  • Fix(profile): resolve profile names from every documented source.
    _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._

  • Fix(profile): ignore an invalid profile-update-interval.
    _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._

  • Fix(profile): tolerate a malformed subscription-userinfo header.
    _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._

  • Feat(profile): show provider links as a cycling chip.
    _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._

  • Feat(dialog): hold back the open button on unverified links.
    _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._

  • Feat(core): add a curated trusted-link list.
    _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._

  • Refactor(profile): move provider urls out of SubscriptionInfo.
    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.

  • Feat(i18n): add provider link and unverified-link warning strings.
    Strings for the profile web page / support chips and for the warning shown
    when a subscription-provided link is not in the trusted list.

  • Refactor(profile): overlay the sort and update buttons on the list.
    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.

  • Feat(settings): allow hiding the LAN sharing leading icon.
    Adds a showLeading flag to LanSharingPreferenceWidget and turns it off
    in the quick settings modal, where the leading icon is redundant.

  • Fix(deep-link): read profile name from the url fragment.
    _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._

  • Chore: replace aider translation setup with Claude instructions.
    _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)
  • Chore(i18n): localize pin/unpin.
    Translate common.pin / common.unpin into all non-English locales.

  • Feat(profile): pin profiles above the sorted list.
    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).

  • Feat(profile): add pin/unpin labels (en)
    English base-locale strings for the profile pin/unpin action. Kept as its own commit (source locale); other languages follow separately.

  • Chore(i18n): drop translation keys orphaned by profile changes.
    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.

  • Chore(i18n): localize new profile update and sort strings.
    Add translations for bulkSuccess, bulkPartial and the sort direction labels (newestFirst/oldestFirst) across all locales, matching the keys added to en.

  • Feat(profile): clearer sort dialog and persisted sort choice.
    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.

  • Fix(profile): keep tile state stable when the list re-sorts.
    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).

  • Feat(profile): summarize bulk subscription updates in one toast.
    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.

  • Feat(profile): show subscription update status on profile tiles.
    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.

  • Feat: fire connection haptic on verified ping, not on Connected state.
    _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._

  • Feat: use CupertinoPage on iOS for native swipe-back gesture.
    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.

  • Chore: bump hiddify-core to db74dfc.
    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).

  • Feat: update ChainMode to include selectable modes and add fromStored method; implement tests for new functionality.

  • Merge branch 'main' of https://github.com/hiddify/hiddify-app.

  • Add new configuration profiles for region 'ir'
    Added new configuration profiles with consent details and security information.

  • Compress libs.

  • Feat: enhance ProfileParser to handle additional link schemes and improve test coverage.

  • Restore build matrix for Android, Windows, Linux, and macOS platforms.

  • Refactor: remove unused WifiManager and update readWIFIState implementation.

  • Fix.

  • Test.

  • Test.

  • Chore: update GitHub Actions workflows and submodule reference.

  • Merge pull request #2357 from kokhlo/fix/region-picker-locale-coverage.
    test: cover region picker labels across locales

  • Test: cover region picker labels across locales.

  • Merge pull request #2361 from kokhlo/fix/restore-connection-on-startup.
    fix(desktop): restore the tunnel on startup when the user left it running

  • Fix(desktop): restore the tunnel on startup when the user left it running.

  • Merge pull request #2359 from kokhlo/fix/portable-zip-format.
    fix(windows): package the portable build as a real zip instead of a tar

  • Fix(windows): package portable build as a real zip archive.

  • Feat: add cancelNotification method and update notification handling.

  • Limit memory usage of IDE.

  • Merge pull request #2235 from veto9292/merge-routing-with-route-rule.
    feat: post-routing hardening — deep-link safety, Psiphon licenses, profile import, and Linux fixes

  • Feat: add importRouteRuleByDeepLinkWarning translations.

  • Feat: add importRouteRuleByDeepLinkWarning translations for other languages.

  • Feat(routing): enhance deep link handling and add routeRule parameter to RoutingOptionsPage.

  • Feat(routing): enhance clipboard import/export with deep link support.

  • Feat(routing): add routeRule parameter to RoutingOptionsPage and import rules from deep link.

  • Translations: add dialogs.confirmation.importRouteRuleByDeepLinkWarning.

  • Feat(settings): move VPN sharing password to config options and exclude from export.

  • Feat: update Spanish pages.settings.inbound translations to match en.

  • Feat(settings): implement LAN sharing functionality with password support and UI integration (adding to quick settings section)

  • Feat(settings): add new port configuration options and integrate with inbound options page.

  • Feat(settings): add trailing widget support to ValuePreferenceWidget and implement SwitchPreferenceWidget.

  • Pubspec.yaml: remove unused packages.

  • Feat(linux): enhance desktop entry with localization and AppImage update keys.
    _- Add Keywords and Comment fields with Persian (fa) translations.

  • Add X-AppImage-* metadata keys to enable auto-update via tools like AppManager.
  • Extract AppImage version dynamically from version.json._
  • Fix AppImage launch crashes with launcher %u args and enable deep-linking.

  • Aider: Translation helper.

  • Build: update macOS plugin registrant after dependency removal.

  • Chore(makefile): optimize Linux docker builds and fix core download URL.
    _- Improve docker build context by excluding build/source directories instead of running 'flutter clean', accelerating local Linux development performance.

  • Update release URL for hiddify-core._
  • Fix(router): pass triggeredByDeepLink flag when auto-importing profiles.

  • Refactor: resolve LAN IP via core RPC in settings.

  • Build: update dependencies and remove unused packages.
    _- Remove 'network_info_plus' since LAN IP is now fetched natively via core gRPC RPC.

  • Remove 'bottom_navy_bar' as it is no longer used in the project.
  • Upgrade 'protobuf' dependency to ^5.0.0._
  • Chore: sync proto files from hiddify-core.

  • Chore: regenerate protobuf bindings.

  • Feat: add common.file translation to multiple languages.

  • Feat: add file import option for profiles and adjust modal grid layout.

  • Feat: add Psiphon license translations and update WARP messages.

  • Feat: add Psiphon translations and fix warp message trailing periods.

  • Feat: add Psiphon override support and unified chain license flow.

  • Feat: add addProfileByDeepLinkWarning translation for ar, tr, zh-TW.

  • Refactor: rename addProfileFromLink key and add translations.

  • Looking at the diff and the error message, I need to create a proper SEARCH/REPLACE block that exactly matches the existing content. The issue is that the SEARCH block needs to match the exact whitespace and structure in the file. Based on the error output showing the actual file content, here's the corrected version:
    _assets/translations/en.i18n.json

<<<<<<< SEARCH
      "routeRule": {
        "delete": {
          "title": "Delete rule",
          "msg": "Are you sure you want to delete the \"$rulename\" rule?"
        }
      }
    },
=======
      "routeRule": {
        "delete": {
          "title": "Delete rule",
          "msg": "Are you sure you want to delete the \"$rulename\" rule?"
        }
      },
      "addProfileFromLink": {
        "title": "Add Profile?",
        "message": "An external link from \"${host}\" is requesting to add a new profile. Do you want to import it?"
      }
    },
>>>>>>> REPLACE
```_

* Refactor: use slang translations for add profile dialog. 

* Merge pull request #2240 from jabagawee/fix/core-init-toast-crash. 
  _Prevent blank-screen launch when core init fails during bootstrap_

* Make hiddify-core init non-fatal during bootstrap. 
  _hiddify-core initialized through _init, which rethrows on failure.
When core setup fails (for example when its port is already in
use), the failure handler reports the error and the app is meant to
keep running, but a rethrow here aborts lazyBootstrap before
runApp() and leaves a blank screen.

This commit switches it to _safeInit so a setup failure is logged
and the app still launches, matching how the active profile and
system tray are already initialized. This is defense in depth
alongside the notification overlay guard._

* Guard notifications against a missing toast overlay. 
  _Errors surfaced during bootstrap call showErrorToast before
runApp(), when no Toastification overlay exists yet.
findToastificationOverlayState then null-checks a null value and
throws, and that exception propagates out of bootstrap before
runApp() is reached, leaving the app on a blank screen.

This commit wraps the show call in a try/catch that logs and
returns null when the overlay is not available, so a notification
can never abort startup or crash the app. _show now returns a
nullable item, matching the already-nullable public show methods._

* Merge pull request #2241 from fcdm25/fix-1974-unlimited-traffic-sentinel. 
  _fix(profile): show subscription-userinfo total=0 as unlimited (#1974)_

* Fix(profile): treat subscription-userinfo total=0 as unlimited (#1974) 
  _The parser maps total=0 to an ~857 GiB sentinel, but the UI only treats >10TB as
unlimited (isInfinitSize / profile_tile.dart), so it rendered as a finite cap.
Raise the sentinel above the 10 TB gate._

* Update. 

* Merge pull request #2189 from veto9292/merge-routing-with-route-rule. 
  _Merge routing with route rule_

* Replace IconButton with ExpandButton for Quick Settings and improve quickSettings translations. 

* Update sing box repo and model for adding route rule and removing blockAds and bypassLan. 

* Move chain section and rename routeOptions to routingOptions. 

* Merge route_options_page and rules_page into routing_options_page, add expandable route general options, FAB menu, and empty state for rules list and more... 

* Using go router instead of Navigator. 

* Remove package section from rule (replaced by per-app proxy), remove PopScope logic (moved to onExit in routing config notifier), and update navigation and route logic. 

* Get validator from RuleEnum instead of constructor parameter to simplify and bypass go_router limitations. 

* Replace hardcoded strings with translations. 

* Fix stale isRuleEdited state when checking for auto-save in RulePage onExit. 

* Move RulePage items val

Don't miss a new hiddify-app release

NewReleases is sending notifications on new releases.