diff --git a/.github/scripts/release-gate.sh b/.github/scripts/release-gate.sh new file mode 100644 index 0000000..118565d --- /dev/null +++ b/.github/scripts/release-gate.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Release-notes gate. Verifies that every human-authored release text exists and +# is well-formed for the version currently in pubspec.yaml. Run BOTH on the cut +# PR (so it goes red before merge) and as the first job of the tag workflow (a +# cheap backstop before build/sign). +# +# rc.2 and rc.3 shipped with only a pubspec bump and no notes. This makes that +# state impossible: no notes, no release. +# +# Exit 0 = all present and valid. Exit 1 = a hard failure (prints every problem +# before exiting, so one run surfaces all of them, not one at a time). +# +# #342, epic #312. + +set -uo pipefail + +PLAY_MAX=500 + +fail=0 +note() { printf ' %s\n' "$1"; } +bad() { printf '::error::%s\n' "$1"; fail=1; } + +# --- version from pubspec (strip the +build suffix) ------------------------- +VERSION=$(grep -E '^version:' pubspec.yaml | head -1 | sed 's/version:[[:space:]]*//' | sed 's/+.*//' | tr -d '[:space:]') +if [ -z "${VERSION}" ]; then + bad "Could not read a version from pubspec.yaml." + exit 1 +fi +echo "Release gate for version: ${VERSION}" +echo + +# --- 1. CHANGELOG has a section for this version ---------------------------- +# Matches a markdown heading containing the version, e.g. "## [1.1.2-rc.4]". +if grep -qE "^#+.*\[?${VERSION//./\\.}\]?" CHANGELOG.md; then + note "CHANGELOG.md: section for ${VERSION} present" +else + bad "CHANGELOG.md has no section for ${VERSION}. Add one before releasing." +fi + +# --- 2. GitHub release notes exist and are non-empty ------------------------ +RN="release-notes/${VERSION}.md" +if [ -s "${RN}" ]; then + note "release-notes: ${RN} present" +else + bad "${RN} is missing or empty. This becomes the GitHub release body." +fi + +# --- 3. Play 'What's New' exists and is within the length limit ------------- +PLAY="play/${VERSION}.txt" +if [ ! -s "${PLAY}" ]; then + bad "${PLAY} is missing or empty. Required for the Play Console 'What's new'." +else + # Unicode-aware count (wc -m), matching Google's per-character limit. + LEN=$(wc -m < "${PLAY}" | tr -d '[:space:]') + if [ "${LEN}" -gt "${PLAY_MAX}" ]; then + bad "${PLAY} is ${LEN} chars; Play limit is ${PLAY_MAX}. Trim it." + else + note "play: ${PLAY} present (${LEN}/${PLAY_MAX} chars)" + fi +fi + +# --- 4. Discord announcement exists and is non-empty ------------------------ +DISCORD="discord/${VERSION}.md" +if [ -s "${DISCORD}" ]; then + note "discord: ${DISCORD} present" +else + bad "${DISCORD} is missing or empty. Paste-ready community announcement." +fi + +echo +if [ "${fail}" -ne 0 ]; then + echo "Release gate FAILED for ${VERSION}. Fix the above before tagging." + exit 1 +fi +echo "Release gate passed for ${VERSION}." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58abccd..1c1ba57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: java-version: "17" - uses: subosito/flutter-action@v2 with: - channel: "stable" + flutter-version: "3.44.1" cache: true - name: Cache Gradle uses: actions/cache@v4 @@ -30,7 +30,12 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - run: flutter pub get - - run: flutter build apk --release --no-pub + - run: dart run build_runner build --delete-conflicting-outputs + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json + - run: flutter build apk --release --no-pub --dart-define-from-file=dart_defines.json - name: Upload APK uses: actions/upload-artifact@v4 with: @@ -45,9 +50,10 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: - channel: "stable" + flutter-version: "3.44.1" cache: true - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs - run: flutter build ios --release --no-codesign --no-pub linux: @@ -56,12 +62,17 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: - channel: "stable" + flutter-version: "3.44.1" cache: true - name: Install Linux build deps run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev - run: flutter pub get - - run: flutter build linux --release --no-pub + - run: dart run build_runner build --delete-conflicting-outputs + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json + - run: flutter build linux --release --no-pub --dart-define-from-file=dart_defines.json - name: Upload Linux bundle uses: actions/upload-artifact@v4 with: @@ -76,9 +87,10 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: - channel: "stable" + flutter-version: "3.44.1" cache: true - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs - run: flutter build macos --release --no-pub web: @@ -87,10 +99,15 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: - channel: "stable" + flutter-version: "3.44.1" cache: true - run: flutter pub get - - run: flutter build web --release --no-pub + - run: dart run build_runner build --delete-conflicting-outputs + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json + - run: flutter build web --release --no-pub --dart-define-from-file=dart_defines.json - name: Upload web build uses: actions/upload-artifact@v4 with: @@ -105,10 +122,19 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: - channel: "stable" + flutter-version: "3.44.1" cache: true - run: flutter pub get - - run: flutter build windows --release --no-pub + - run: dart run build_runner build --delete-conflicting-outputs + # Windows runner defaults to PowerShell; jq is not guaranteed there, + # so build the JSON with ConvertTo-Json, which escapes correctly. + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: | + @{ GIPHY_API_KEY = $env:GIPHY_API_KEY } | ConvertTo-Json -Compress | + Set-Content -Path dart_defines.json -Encoding utf8 -NoNewline + - run: flutter build windows --release --no-pub --dart-define-from-file=dart_defines.json - name: Upload Windows build uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/configure-web-domains.yml b/.github/workflows/configure-web-domains.yml new file mode 100644 index 0000000..f0cb672 --- /dev/null +++ b/.github/workflows/configure-web-domains.yml @@ -0,0 +1,94 @@ +name: Configure offband.app domains + +# One-time (idempotent) setup: attaches custom domains to the offband-app +# Pages project and creates the proxied CNAME records. +# +# offband.app -> offband-app.pages.dev (production branch) +# dev.offband.app -> dev.offband-app.pages.dev (dev branch alias) +# +# The dev record deliberately targets the BRANCH ALIAS rather than the +# project root. Cloudflare's documented behaviour: a custom domain CNAME'd +# to .pages.dev always serves production, so pointing dev at +# dev..pages.dev is what makes it serve the dev branch. This only +# works with Cloudflare-managed DNS and a PROXIED record; an unproxied or +# external record silently falls through to production. +# +# Manual trigger only. Domains do not need reconfiguring on every deploy, +# and this touches DNS. +# +# Mirrors OffbandMesh/offband-site/.github/workflows/configure-domains.yml, +# which is the working reference for offband.org. +# +# NOTE: offband.org belongs to a DIFFERENT Pages project (offband-site) in +# the same Cloudflare account. This workflow only ever touches the +# offband.app zone. See #339. + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + domains: + runs-on: ubuntu-latest + env: + API: https://api.cloudflare.com/client/v4 + ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PAGES_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + DNS_TOKEN: ${{ secrets.CLOUDFLARE_DNS_ZONE_TOKEN }} + PROJECT: offband-app + ZONE: offband.app + steps: + - name: Attach custom domains and create DNS records + run: | + set -euo pipefail + + ZID=$(curl -s -H "Authorization: Bearer $DNS_TOKEN" \ + "$API/zones?name=$ZONE" | jq -r '.result[0].id // empty') + if [ -z "$ZID" ]; then + echo "::error::Could not resolve zone id for $ZONE. Check CLOUDFLARE_DNS_ZONE_TOKEN has Zone:Read." + exit 1 + fi + echo "zone $ZONE -> ${ZID:0:6}…" + + # name -> CNAME target. Apex serves production; dev serves the + # dev branch alias. Cloudflare flattens the apex CNAME. + attach() { + NAME="$1"; TARGET="$2" + echo "── $NAME -> $TARGET ──" + + REC=$(curl -s -H "Authorization: Bearer $DNS_TOKEN" \ + "$API/zones/$ZID/dns_records?type=CNAME&name=$NAME" \ + | jq -r '.result[0].id // empty') + BODY=$(jq -n --arg n "$NAME" --arg c "$TARGET" \ + '{type:"CNAME",name:$n,content:$c,proxied:true}') + + if [ -n "$REC" ]; then + curl -s -X PUT -H "Authorization: Bearer $DNS_TOKEN" \ + -H "Content-Type: application/json" \ + "$API/zones/$ZID/dns_records/$REC" --data "$BODY" \ + | jq -r 'if .success then " DNS: updated" else " DNS ERR: " + (.errors|tostring) end' + else + curl -s -X POST -H "Authorization: Bearer $DNS_TOKEN" \ + -H "Content-Type: application/json" \ + "$API/zones/$ZID/dns_records" --data "$BODY" \ + | jq -r 'if .success then " DNS: created" else " DNS ERR: " + (.errors|tostring) end' + fi + + curl -s -X POST -H "Authorization: Bearer $PAGES_TOKEN" \ + -H "Content-Type: application/json" \ + "$API/accounts/$ACCOUNT_ID/pages/projects/$PROJECT/domains" \ + --data "$(jq -n --arg n "$NAME" '{name:$n}')" \ + | jq -r 'if .success then " Pages: attached (" + (.result.status // "pending") + ")" else " Pages: " + (.errors|tostring) end' + } + + attach "$ZONE" "${PROJECT}.pages.dev" + attach "dev.$ZONE" "dev.${PROJECT}.pages.dev" + + curl -s -X PATCH -H "Authorization: Bearer $DNS_TOKEN" \ + -H "Content-Type: application/json" \ + "$API/zones/$ZID/settings/always_use_https" --data '{"value":"on"}' \ + | jq -r 'if .success then "AlwaysHTTPS: on" else "AlwaysHTTPS (skip): " + (.errors|tostring) end' + + echo "configure-web-domains complete" diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..3fb45c9 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,112 @@ +name: Deploy web (Cloudflare Pages) + +# Publishes the Flutter web client to Cloudflare Pages. +# +# push to main -> --branch=main -> PRODUCTION -> offband.app +# push to dev -> --branch=dev -> dev tier -> dev.offband.app +# +# WHY THE TRIGGER LIST IS THE SAFETY MECHANISM: +# Direct-upload Pages projects cannot configure production branch controls +# in the Cloudflare dashboard, so the "never auto-deploy arbitrary branches +# to prod" requirement (#122) is enforced HERE, by the trigger scope. Adding +# a branch to `on.push.branches` is what grants it deploy rights. Do not add +# one casually, and never add `pull_request`. +# +# DO NOT enable Cloudflare's dashboard "Connect to Git" for this project. It +# creates a second, git-integrated project that races with this workflow. +# Deploys are Actions direct-upload only, matching offband-site. +# +# The marketing site (offband.org) is a SEPARATE Pages project in the same +# account, deployed from OffbandMesh/offband-site. Nothing here touches it. +# +# See #339, epic #312. + +on: + push: + branches: + - main + - dev + workflow_dispatch: + +concurrency: + # Serialise per target so two pushes cannot race the same environment, + # but let dev and main deploy independently. + group: deploy-web-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Pinned to the bench toolchain, same as every other workflow (#331). + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.44.1" + cache: true + + - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs + + # Same mechanism as build.yml (#331). build_pipe's build_command in + # pubspec.yaml passes --dart-define-from-file, so this file must exist + # before the build runs or the GIF picker ships dead. + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json + + # build_pipe, NOT a plain `flutter build web`. It appends ?v= to the bootstrap/JS files, which is what stops browsers + # serving stale code after a deploy. A plain flutter build has no cache + # busting and would leave users on old bundles. + - name: Build web (versioned, cache-busted) + run: dart run build_pipe:build + + - name: Verify build output + run: | + set -eu + test -f build/web/index.html || { echo "::error::build/web/index.html missing"; exit 1; } + COUNT=$(find build/web -type f | wc -l) + BIGGEST=$(find build/web -type f -printf '%s\n' | sort -rn | head -1) + echo "files: ${COUNT}, largest: $((BIGGEST / 1048576)) MB" + # Cloudflare Pages hard limits: 25 MiB per asset, 20000 files. + # Fail here with a clear message rather than mid-upload. + if [ "${BIGGEST}" -gt 26214400 ]; then + echo "::error::An asset exceeds the Cloudflare Pages 25 MiB limit." + find build/web -type f -size +25M -printf ' %s %p\n' + exit 1 + fi + if [ "${COUNT}" -gt 20000 ]; then + echo "::error::More than 20000 files; exceeds the Cloudflare Pages limit." + exit 1 + fi + grep -q 'v=' build/web/index.html \ + && echo "cache-busting query params present" \ + || echo "::warning::no ?v= found in index.html; check build_pipe config" + + - name: Deploy to Cloudflare Pages + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + set -eu + npx --yes wrangler@3 pages project create offband-app \ + --production-branch=main \ + || echo "project already exists, continuing" + npx --yes wrangler@3 pages deploy build/web \ + --project-name=offband-app \ + --branch="${GITHUB_REF_NAME}" \ + --commit-dirty=true + + - name: Report target + run: | + if [ "${GITHUB_REF_NAME}" = "main" ]; then + echo "Deployed PRODUCTION -> https://offband.app" + else + echo "Deployed dev tier -> https://dev.offband.app" + fi diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index d53a271..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Deploy to Cloudflare Workers - -# Manual-only. This workflow was inherited from upstream zjs81 and deploys the web -# build to *its* Cloudflare account; this fork has no Cloudflare secrets, so the old -# `push: tags: ['*']` trigger failed on every tag (missing CLOUDFLARE_API_TOKEN). -# Disabled the auto-trigger to stop the per-tag failures (chore #120). The job is -# preserved for the deliberate Offband web-app launch (Feature #121 / Epic #122), -# which will define the proper trigger scope + secrets. -on: - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - # Match local development version which provides Dart 3.11.0 - flutter-version: '3.41.2' - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Get dependencies - run: flutter pub get - - - name: Build Web - run: bun run build - - - name: Deploy to Cloudflare - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_dart.yml index e267557..4076ea5 100644 --- a/.github/workflows/flutter_dart.yml +++ b/.github/workflows/flutter_dart.yml @@ -17,11 +17,14 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - channel: stable + flutter-version: "3.44.1" - name: Install dependencies run: flutter pub get + - name: Generate code (drift / build_runner) + run: dart run build_runner build --delete-conflicting-outputs + - name: Analyze code run: flutter analyze --fatal-infos --fatal-warnings diff --git a/.github/workflows/release-gate.yml b/.github/workflows/release-gate.yml new file mode 100644 index 0000000..d6ef81f --- /dev/null +++ b/.github/workflows/release-gate.yml @@ -0,0 +1,27 @@ +name: Release gate + +# Fails a PR whose pubspec version lacks any of the four human-authored release +# texts (CHANGELOG section, release-notes, play blurb <=500 chars, discord). +# +# This runs on EVERY PR so a release-cut PR goes red before merge if a note is +# missing. It is cheap and platform-agnostic. The tag workflow runs the same +# script as its first job (a backstop before build/sign). +# +# rc.2 and rc.3 shipped with only a pubspec bump. This makes that unmergeable. +# +# #342, epic #312. + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check release texts for the pubspec version + run: bash .github/scripts/release-gate.sh diff --git a/.github/workflows/release-signed.yml b/.github/workflows/release-signed.yml new file mode 100644 index 0000000..fd8e32a --- /dev/null +++ b/.github/workflows/release-signed.yml @@ -0,0 +1,304 @@ +name: Release (signed, all platforms) + +# Produces a COMPLETE, published release from a `v*` tag: signed Android APK + +# AAB, Windows, and Linux, attached to a GitHub release whose body is the +# human-authored release notes. This is what makes a partial release (the +# rc.2/rc.3 failure) impossible. +# +# SECURITY: this repo is PUBLIC and the `android` job has the signing keystore, +# the single most irreplaceable credential in the project. If it leaks the app +# can never be updated again on any channel. +# +# - NEVER add a `pull_request` trigger. A PR (incl. from a fork) must never +# run a job that can read the keystore. +# - Only the `android` job references the `release-signing` Environment, so +# ONLY it receives the keystore secrets, and only after the required +# reviewer approves. The windows/linux/release jobs never see them. +# - Keystore written to disk only for the build, deleted with `if: always()`. +# - Signing values passed via env:, never on a command line, never echoed. +# +# Debug-signed artifacts for day-to-day PR testing still come from build.yml +# and are unaffected. See #330 / #342, epic #312. + +on: + push: + tags: + - "v*" + workflow_dispatch: + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + # Cheap backstop: the same gate that runs on the cut PR, re-run here so a tag + # can never publish without the four release texts. Fails in seconds, before + # any expensive build. + gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Release-notes gate + run: bash .github/scripts/release-gate.sh + + android: + needs: gate + runs-on: ubuntu-latest + environment: release-signing # required-reviewer gate; do not remove + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.44.1" + cache: true + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/settings.gradle', 'android/app/build.gradle', 'pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # PKCS12 keystore (despite the .jks extension) has one password for both + # store and key. See #330. + - name: Decode signing keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + run: | + set -eu + if [ -z "${KEYSTORE_BASE64}" ] || [ -z "${KEYSTORE_PASSWORD}" ]; then + echo "::error::Signing secrets are not available to this run." + exit 1 + fi + printf '%s' "${KEYSTORE_BASE64}" | base64 -d > android/app/release.jks + SIZE=$(stat -c%s android/app/release.jks) + if [ "${SIZE}" -lt 1000 ]; then + echo "::error::Decoded keystore is ${SIZE} bytes; expected ~2.8 KB. Secret is malformed." + exit 1 + fi + umask 077 + cat > android/key.properties <<'PROPS' + storeFile=release.jks + keyAlias=meshcore + PROPS + { + printf 'storePassword=%s\n' "${KEYSTORE_PASSWORD}" + printf 'keyPassword=%s\n' "${KEYSTORE_PASSWORD}" + } >> android/key.properties + echo "Keystore decoded (${SIZE} bytes), key.properties written." + + - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json + - name: Build signed APK + run: flutter build apk --release --no-pub --dart-define-from-file=dart_defines.json + - name: Build signed AAB + run: flutter build appbundle --release --no-pub --dart-define-from-file=dart_defines.json + + # A green build does NOT prove release signing: build.gradle.kts silently + # falls back to the debug config when key.properties is absent (#111). Read + # the cert off the APK and fail on the debug key or any mismatch. + - name: Verify APK signing certificate + env: + EXPECTED_SHA256: e7da8cd5bf22ac3eda7cb954b8b120a18b6c4382d1b6e6fdd204ddedddaf5488 + run: | + set -eu + APK=build/app/outputs/flutter-apk/app-release.apk + BUILD_TOOLS=$(ls -d "${ANDROID_HOME}"/build-tools/* | sort -V | tail -1) + CERTS=$("${BUILD_TOOLS}/apksigner" verify --print-certs "${APK}") + echo "--- apksigner --print-certs output ---" + echo "${CERTS}" + echo "--------------------------------------" + if echo "${CERTS}" | grep -qi "CN=Android Debug"; then + echo "::error::APK is DEBUG-SIGNED. key.properties not picked up; see #111." + exit 1 + fi + # Extract the fingerprint by shape, not by label: only the cert's + # SHA-256 digest is 64 hex chars (SHA-1 is 40, MD5 is 32), so this is + # unambiguous and independent of how a given apksigner version phrases + # the line. The label-based awk parse broke on the runner's apksigner + # (#345 first live run: label differed, ACTUAL came back empty). + ACTUAL=$(echo "${CERTS}" | grep -ioE '[0-9a-f]{64}' | head -1 | tr 'A-F' 'a-f') + if [ -z "${ACTUAL}" ]; then + echo "::error::Could not read a SHA-256 fingerprint from the APK (see output above)." + exit 1 + fi + if [ "${ACTUAL}" != "${EXPECTED_SHA256}" ]; then + echo "::error::Signing cert MISMATCH. expected ${EXPECTED_SHA256} actual ${ACTUAL}" + exit 1 + fi + echo "APK signing certificate verified: ${ACTUAL}" + + # The AAB is the artifact that goes to Play, so verify IT directly rather + # than trusting the APK as a proxy. apksigner cannot read an AAB; keytool + # can, and reports the same SHA-256 fingerprint. Same pin as the APK. + # (Gemini review, #342.) + - name: Verify AAB signing certificate + env: + EXPECTED_SHA256: e7da8cd5bf22ac3eda7cb954b8b120a18b6c4382d1b6e6fdd204ddedddaf5488 + run: | + set -eu + AAB=build/app/outputs/bundle/release/app-release.aab + # keytool comes from the JDK that setup-java put on PATH in this job. + echo "--- keytool -printcert -jarfile output ---" + keytool -printcert -jarfile "${AAB}" | grep -iE 'Owner|SHA256' || true + echo "------------------------------------------" + # Strip the label, keep only hex (robust to spacing/case), lowercase. + AAB_SHA=$(keytool -printcert -jarfile "${AAB}" \ + | grep -i 'SHA256:' | head -1 \ + | sed 's/.*SHA256://' | tr -cd '0-9a-fA-F' | tr 'A-F' 'a-f') + if [ -z "${AAB_SHA}" ]; then + echo "::error::Could not read a SHA-256 from the AAB signing cert (see output above)." + exit 1 + fi + if [ "${AAB_SHA}" != "${EXPECTED_SHA256}" ]; then + echo "::error::AAB signing cert MISMATCH. expected ${EXPECTED_SHA256} actual ${AAB_SHA}" + echo "::error::This AAB would be rejected by Play App Signing or break cross-channel updates." + exit 1 + fi + echo "AAB signing certificate verified: ${AAB_SHA}" + + - name: Stage signed artifacts + run: | + mkdir -p dist + cp build/app/outputs/flutter-apk/app-release.apk dist/ + cp build/app/outputs/bundle/release/app-release.aab dist/ + - uses: actions/upload-artifact@v4 + with: + name: release-android + path: dist/* + if-no-files-found: error + retention-days: 7 + + - name: Remove keystore material + if: always() + run: | + rm -f android/app/release.jks android/key.properties dart_defines.json + echo "Keystore material removed." + + windows: + needs: gate + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.44.1" + cache: true + - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: | + @{ GIPHY_API_KEY = $env:GIPHY_API_KEY } | ConvertTo-Json -Compress | + Set-Content -Path dart_defines.json -Encoding utf8 -NoNewline + - run: flutter build windows --release --no-pub --dart-define-from-file=dart_defines.json + - name: Zip Windows build + run: Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath offband-windows-x64.zip + - uses: actions/upload-artifact@v4 + with: + name: release-windows + path: offband-windows-x64.zip + if-no-files-found: error + retention-days: 7 + + linux: + needs: gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.44.1" + cache: true + - name: Install Linux build deps + run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev + - run: flutter pub get + - run: dart run build_runner build --delete-conflicting-outputs + - name: Write dart_defines.json + env: + GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }} + run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json + - run: flutter build linux --release --no-pub --dart-define-from-file=dart_defines.json + - name: Tar Linux bundle + run: tar -czf offband-linux-x64.tar.gz -C build/linux/x64/release/bundle . + - uses: actions/upload-artifact@v4 + with: + name: release-linux + path: offband-linux-x64.tar.gz + if-no-files-found: error + retention-days: 7 + + # Creates the release ONLY after all platform builds (and the signing cert + # check) have passed, so a signing failure yields no release rather than an + # empty one. Published directly: the notes were reviewed in the cut PR. + release: + needs: [android, windows, linux] + runs-on: ubuntu-latest + permissions: + contents: write # create the release + upload assets + steps: + - uses: actions/checkout@v4 + - name: Resolve version + notes + id: v + run: | + set -eu + VERSION=$(grep -E '^version:' pubspec.yaml | head -1 | sed 's/version:[[:space:]]*//' | sed 's/+.*//' | tr -d '[:space:]') + NOTES="release-notes/${VERSION}.md" + test -s "${NOTES}" || { echo "::error::${NOTES} missing at release time"; exit 1; } + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "notes=${NOTES}" >> "$GITHUB_OUTPUT" + - uses: actions/download-artifact@v4 + with: + path: incoming + - name: Collect assets + run: | + mkdir -p out + cp incoming/release-android/* out/ + cp incoming/release-windows/* out/ + cp incoming/release-linux/* out/ + echo "Assets for the release:" + ls -la out/ + # Draft-first so publication is ATOMIC: the release is not visible to + # anyone until every asset is uploaded. Creating-then-uploading would + # leave a public release with missing assets if an upload stalls or the + # runner dies mid-way — the exact partial-release failure this pipeline + # exists to prevent. `gh release create --help` documents this pattern. + # The final `--draft=false` is applied in BOTH branches so a run retried + # after a mid-way death still ends published, never stuck as a draft. + - name: Create GitHub release (draft → upload → publish) + env: + GH_TOKEN: ${{ github.token }} + NOTES: ${{ steps.v.outputs.notes }} + VERSION: ${{ steps.v.outputs.version }} + run: | + set -eu + TAG="${GITHUB_REF_NAME}" + if gh release view "${TAG}" >/dev/null 2>&1; then + # Retry / re-run: refresh assets and notes, then ensure published. + gh release upload "${TAG}" out/* --clobber + gh release edit "${TAG}" --notes-file "${NOTES}" --prerelease --draft=false + else + gh release create "${TAG}" \ + --draft \ + --title "Offband Meshcore ${VERSION}" \ + --notes-file "${NOTES}" \ + --prerelease + gh release upload "${TAG}" out/* + gh release edit "${TAG}" --draft=false + fi + echo "Release ${TAG} published with $(ls out | wc -l) assets." diff --git a/.gitignore b/.gitignore index 2c71b17..31e840c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ migrate_working_dir/ .flutter-plugins-dependencies .pub-cache/ .pub/ -pubspec.lock /build/ /coverage/ # fvm project files diff --git a/CHANGELOG.md b/CHANGELOG.md index 76aaf62..ff39c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,108 @@ All notable changes to Offband Meshcore. Pre-releases are tagged `-beta.N` / `-rc.N`. +## [1.2.0] - 2026-07-21 + +A major uplift: the storage engine moved to a real database, the app got a new +navigation shell, and GIFs are now shareable across MeshCore clients. First +release cut end-to-end through CI (signed, all platforms). + +### Storage + +- **Bulk data (messages, contacts, channels, and more) now lives in a drift / + SQLite database** instead of individual SharedPreferences entries. Existing + data is **migrated automatically on first launch** and preserved, not + discarded (#335, #355, #348). +- The database is stored in the app-support directory, so it is not caught up in + OneDrive/Documents sync on desktop (#335). +- Message history is kept in full; only the in-memory window is bounded, so + reopening a conversation no longer risks losing older messages (#343). +- Storage failures that used to fail silently now surface (#306, #333). + +### Navigation + +- **New app shell with a hamburger nav drawer that can be pinned open** (#292). +- The channel list lives in the drawer and is switchable from inside a channel; + channels swap in place so a pinned panel stays put (#289). +- Map layer toggles moved into the nav panel; Disconnect and Settings moved into + the panel footer (#291, #290). +- Contacts gained a prebuilt filter rail, including a Sensors type (#307, #308). +- Back-button behavior fixed: back now backgrounds the app instead of killing + it, and no longer drops a user off a connected radio (#291). + +### GIFs + +- GIFs are now shared as a **Giphy URL** instead of the Offband-only `g:`, + so other MeshCore clients can open them (#282). +- Pasted Giphy / Tenor URLs render inline, behind a trusted-host allowlist + (#283, #284). + +### Other features + +- Desktop window position and size are remembered across launches (#349). +- Reach a contact's settings from its long-press menu (#351). +- FEM LNA toggle in Radio Settings and Offband capabilities shown in Device Info, + gated on firmware support (#304). +- Message arrival time (rxTime) is logged and persisted, distinct from the + claimed send time (#285). + +### Fixes + +- You can no longer accidentally block your own node (#250). +- Path length decodes per the firmware contract (width-aware), and path + diagnostics report units truthfully (#309, #298). +- Connecting no longer rewrites the entire preferences file per channel (#306). + +### Under the hood + +- Offband is now available as a web app at offband.app, and every release is + built, signed, and published across Android, Windows, Linux, and web through CI. + +## [1.1.2-rc.3] - 2026-07-17 + +Combined entry for rc.2 (versionCode 59) and rc.3 (versionCode 60). Both shipped +to the Play closed test with only a version bump; this backfills the changelog +they should have carried. + +### Added +- Block / mute system: block a contact or a channel sender from contacts, DMs, + discovery, or a channel message; blocked DMs and adverts are suppressed with a + visible marker, and a name-only block promotes to the sender's public key on + the next advert or DM and ages out after 30 days. Managed from Settings → + Blocked. Synced to firmware on connect where the radio supports it (#168–#180, + #172–#174, #169, #170). +- Per-channel notification modes: a 3-way selector (all / mentions / mute) on the + Channels screen, applied at the notification gate and stored keyed to the + channel PSK (#260, #261, #262). +- Path trace and topology: width-matched path tracing, a topology service and + readout, RX ingestion, and a route-builder UX that groups hops by hash width + rather than raw bytes (#150, #186, #156, #155, #224). +- Channel QR: share a channel as a scannable QR and scan one to add it, unified + with the community scanner and using the reference-app format (#163). +- Own device public key is now full, selectable, and one-tap copyable (#234). +- Reply-to chip shown above a reply-gif, with inline `@[Name]` mention chips + (#235). +- GPS refresh falls back to stock-firmware self-telemetry when the fork-specific + query is unavailable, chosen by firmware capability (#123). +- Queue-sync diagnostics: a file log of queue-drain activity plus a manual + force-drain action (#51). + +### Fixed +- USB open no longer wedges ESP32-C6 boards: the DTR open pulse is gated by USB + vendor ID, so it fires only for nRF52 reconnect and is skipped for chips it + would reset into ROM download mode (#248). +- Outgoing DMs to a blocked contact are dropped at the send path (#252). +- Contact path-length byte decodes at the device's hash width instead of being + read as a raw byte count (#224). +- Stale channel slot history is cleared on channel reassignment, and the message + store's device-key scope is aligned (#193, #194). +- Enter selects the highlighted emoji or mention in autocomplete (#238). +- Manual path entry honors the configured path-hash width (#155). + +### Changed +- The fork-only GPS query is gated on firmware capability, so stock and + non-Offband radios degrade gracefully instead of erroring (#123, #144). + ## [1.1.2-rc.1] - 2026-06-28 ### Added diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 9022c8b..27136ea 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -2,13 +2,32 @@ package com.meshcore.meshcore_open import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { private val usbFunctions by lazy { MeshcoreUsbFunctions(this) } + private val appLifecycleChannelName = "meshcore_open/app_lifecycle" + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) usbFunctions.configureFlutterEngine(flutterEngine) + + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + appLifecycleChannelName + ).setMethodCallHandler { call, result -> + when (call.method) { + // Send the task to the background WITHOUT finishing the + // activity. SystemNavigator.pop() calls finish(), which tears + // down the Flutter engine and drops the radio connection, so + // reopening the app lands on a disconnected radio. + "moveTaskToBack" -> { + result.success(moveTaskToBack(true)) + } + else -> result.notImplemented() + } + } } override fun onDestroy() { diff --git a/discord/1.1.2-rc.3.md b/discord/1.1.2-rc.3.md new file mode 100644 index 0000000..8df3296 --- /dev/null +++ b/discord/1.1.2-rc.3.md @@ -0,0 +1,13 @@ +**Offband Meshcore 1.1.2-rc.3 is out** 📡 + +Big one this round: + +🚫 **Block & mute** — block contacts or channel senders from just about anywhere (contacts, DMs, discovery, a channel message). Blocked stuff is hidden with a marker, name-only blocks upgrade to a real key when the sender shows up again, and there's a Blocked screen in Settings to manage it all. Syncs to the radio where firmware supports it. + +🔔 **Per-channel notifications** — set each channel to all / mentions only / muted. + +🗺️ **Path trace + topology** — width-matched traces, a topology readout, and a route builder that groups hops properly. Plus share/add channels by QR. + +🔧 Fixes: USB no longer wedges some ESP32-C6 boards, DMs to a blocked contact actually stop sending, and more radios report GPS without flooding the link. + +Download for your platform below. Full changelog in the repo. diff --git a/discord/1.2.0.md b/discord/1.2.0.md new file mode 100644 index 0000000..e8157c6 --- /dev/null +++ b/discord/1.2.0.md @@ -0,0 +1,20 @@ +🚀 **Offband Meshcore 1.2.0 is here, and it's our biggest release yet.** + +A ground-up interface redesign, a real database under the hood, GIFs that work across every MeshCore client, and hands-on control of your radio's RF front end. This is also the first Offband release built, signed, and shipped to every platform automatically. + +🎨 **A brand-new interface.** The headline feature. There's a new **left rail** you open from the hamburger button and can **pin open**, so it lives right alongside your chat instead of covering it. Your channels live in the rail and switch **in place**, so you never lose your spot. Map toggles, Disconnect, and Settings all found sensible new homes, and the back button finally behaves. + +🗄️ **Your data on solid ground.** Messages, contacts, and channels now live in a real **SQLite database** instead of scattered preference files. Your existing data migrates automatically on first launch, nothing is thrown away, and full message history is preserved. On desktop it no longer fights with OneDrive/Documents sync. + +🎞️ **GIFs everyone can see.** GIFs now send as a **Giphy link** instead of an Offband-only code, so people on **stock MeshCore and other clients can tap and watch them** too. Pasted **Giphy and Tenor** links render inline. + +📡 **Take command of your radio's front end.** On supported hardware, you can now toggle **FEM / LNA** right from Radio Settings, and Device Info reads out your radio's Offband capabilities. + +⚡ Plus a **snappier app** from the new storage engine, **desktop window position** remembered between launches, contact settings from the long-press menu, deeper mesh diagnostics, and you can't accidentally block your own node anymore. + +**📥 Download for your platform:** https://github.com/OffbandMesh/meshcore-client/releases/tag/v1.2.0 +🌐 **Or run it right in your browser, no install:** https://offband.app +🔗 Project home: https://offband.org +📝 Full changelog is in the release notes at the link above. + +❤️ **A few of you have asked how to chip in, so I've put up a donation page:** https://offband.org/donate. Offband is free and open source and always will be. Anything there just helps keep the project going, and helps cover getting it onto the iOS App Store. Thank you all for being part of this. diff --git a/discord/README.md b/discord/README.md new file mode 100644 index 0000000..84bad3e --- /dev/null +++ b/discord/README.md @@ -0,0 +1,14 @@ +# Discord announcement + +One file per release: `discord/.md`, e.g. `discord/1.1.2-rc.4.md`. + +## Style + +Casual, paste-ready community announcement. What landed, in plain excited-but-honest +terms, with the download link. Emoji fine. This is the message the owner posts to Discord. + +**The owner pastes this into Discord manually.** CI validates the file exists and is +non-empty; it never posts. Posting to a community is an external, human-triggered action. + +The release gate (`scripts/release-gate.sh`) fails the build if this file is missing or +empty for the version being tagged. diff --git a/docs/releases/1.2.0/redesign.png b/docs/releases/1.2.0/redesign.png new file mode 100644 index 0000000..7ac7ff7 Binary files /dev/null and b/docs/releases/1.2.0/redesign.png differ diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 6fba8e9..c8e434b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -22,6 +22,7 @@ import '../helpers/time_anomaly.dart'; import '../helpers/cyr2lat.dart'; import '../helpers/smaz.dart'; import '../helpers/cayenne_lpp.dart'; +import '../helpers/path_helper.dart'; import '../services/app_debug_log_service.dart'; import '../services/ble_debug_log_service.dart'; import '../services/linux_ble_error_classifier.dart'; @@ -281,6 +282,7 @@ class MeshCoreConnector extends ChangeNotifier { String? _firmwareVersion; String? _deviceModel; int? _offbandCaps; + bool? _femLnaEnabled; int _pathHashByteWidth = 1; CompanionRadioStats? _latestRadioStats; Stopwatch? _airtimeBumpStopwatch; @@ -491,6 +493,36 @@ class MeshCoreConnector extends ChangeNotifier { int get pathHashByteWidth => _pathHashByteWidth; + /// Compact path rendering for logs: hop count, applied width, the bytes we + /// actually hold versus the bytes that hop count implies, and the hex. + /// + /// Captures previously logged only a length, which made it impossible to tell + /// a single 2-byte hop from two 1-byte hops. That is the exact question + /// #240/#279 turn on. + /// + /// [pathLenField] is the firmware path-len field's low 6 bits, which is a HOP + /// COUNT, not a byte length: firmware `src/Packet.h:79-84` defines + /// `getPathByteLen() == getPathHashCount() * getPathHashSize()`. Logging the + /// held byte count next to the implied one makes the #309 decode truncation + /// self-evident in any capture, without the device present. + /// + /// One line, existing log sites only, no new per-frame logging. (#298) + /// [pathHashWidth] must be the width the path was CAPTURED at (each Contact + /// carries its own), not the connected device's current global width. Using + /// the global one falsely flags a complete 1-byte-width path as TRUNCATED + /// when the client is connected to a 2-byte device. (#309, Gemini review) + String _pathDiag(List pathBytes, int pathLenField, int pathHashWidth) { + if (pathLenField < 0) return 'flood'; + final w = pathHashWidth < 1 ? 1 : pathHashWidth; + final expectedBytes = pathLenField * w; + final hex = pathBytes.isEmpty + ? 'none' + : PathHelper.formatPathHex(pathBytes, w); + final short = pathBytes.length < expectedBytes ? ' TRUNCATED' : ''; + return 'hops=$pathLenField w=$w ' + 'bytes=${pathBytes.length}/$expectedBytes$short [$hex]'; + } + CompanionRadioStats? get latestRadioStats => _latestRadioStats; bool get supportsCompanionRadioStats => (_firmwareVerCode ?? 0) >= 8; @@ -558,6 +590,48 @@ class MeshCoreConnector extends ChangeNotifier { /// True when the radio's block store hit `MAX_BLOCKED_KEYS` (32) and rejected /// an ADD (ok=0). Local block still applies; those keys just aren't portable. bool get blockOffloadStoreFull => _blockOffloadStoreFull; + + /// Whether this specific radio can control its external FEM LNA (#304). + /// + /// Gated on the capability BIT alone — firmware derives it from a runtime FEM + /// probe, so it is a per-unit answer: two Heltec V4s can legitimately disagree + /// depending on the fitted chip, and `rak3401` reports false by design (its + /// SKY66122 gates LNA and PA off one line, so a toggle would kill TX). + /// Never shortcut this to a model or version check. + bool get supportsOffbandFemLna => firmwareSupportsOffbandFemLna(_offbandCaps); + + /// Current FEM LNA state as last reported by the radio, or null if unknown + /// (pre-v16 firmware). Always reflects hardware truth, never a local guess. + bool? get femLnaEnabled => _femLnaEnabled; + + /// Ask the radio to enable or bypass its FEM LNA. No-op unless the capability + /// bit is set, so a non-capable radio never sees `0xC3` traffic. State is + /// updated from the reply, not optimistically. + Future setFemLna(bool enabled) async { + if (!isConnected || !supportsOffbandFemLna) return; + await sendFrame(buildOffbandFemLnaSetFrame(enabled)); + } + + /// Fallback read; device-info (offset 83) is the primary source on connect. + Future requestFemLnaState() async { + if (!isConnected || !supportsOffbandFemLna) return; + await sendFrame(buildOffbandFemLnaGetFrame()); + } + + /// `[0xC3][sub][value]` — the value is the post-apply hardware state, so it is + /// adopted verbatim rather than assuming a SET took effect (#304). + void _handleOffbandFemLnaReply(Uint8List frame) { + final reply = parseOffbandFemLnaReply(frame); + if (reply == null) return; + if (_femLnaEnabled == reply.enabled) return; + _femLnaEnabled = reply.enabled; + appLogger.info( + 'FEM LNA now ${reply.enabled ? 'enabled' : 'bypassed'}', + tag: 'Connector', + ); + notifyListeners(); + } + Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; int? get storageUsedKb => _storageUsedKb; @@ -625,15 +699,36 @@ class MeshCoreConnector extends ChangeNotifier { if (messages == null) return; final removed = messages.remove(message); if (!removed) return; - await _messageStore.saveMessages(contactKeyHex, messages); + // Explicit delete: saveMessages now merges and never removes (#343). + await _messageStore.removeMessage(contactKeyHex, message); notifyListeners(); } + /// Aggregate cost of the per-contact conversation load, which runs once per + /// contact during a pull. It is fired unawaited from the contact handler, so + /// it escapes the frame-handler timing and has to be measured here. + int _convLoadCount = 0; + int _convLoadStoreMs = 0; + int _convLoadMergeMs = 0; + Future _loadMessagesForContact(String contactKeyHex) async { if (_loadedConversationKeys.contains(contactKeyHex)) return; _loadedConversationKeys.add(contactKeyHex); + final storeWatch = Stopwatch()..start(); final allMessages = await _messageStore.loadMessages(contactKeyHex); + storeWatch.stop(); + final mergeWatch = Stopwatch()..start(); + _convLoadCount++; + _convLoadStoreMs += storeWatch.elapsedMilliseconds; + if (_convLoadCount % 25 == 0) { + appLogger.info( + 'conversation loads: $_convLoadCount contacts, ' + 'store=${_convLoadStoreMs}ms(WALL, spans await - includes event-loop ' + 'queueing, NOT pure work) merge=${_convLoadMergeMs}ms(sync work)', + tag: 'Perf', + ); + } if (allMessages.isNotEmpty) { // Keep only the most recent N messages in memory to bound memory usage final windowedMessages = allMessages.length > _messageWindowSize @@ -674,6 +769,8 @@ class MeshCoreConnector extends ChangeNotifier { _conversations[contactKeyHex] = windowedMergedMessages; notifyListeners(); } + mergeWatch.stop(); + _convLoadMergeMs += mergeWatch.elapsedMilliseconds; } String _messageMergeKey(Message message) { @@ -737,7 +834,10 @@ class MeshCoreConnector extends ChangeNotifier { if (messages == null) return; final removed = messages.remove(message); if (!removed) return; - await _channelMessageStore.saveChannelMessages(channelIndex, messages); + // Explicit delete path: saveChannelMessages now MERGES and never removes + // (#343), so deletion must go through removeChannelMessage or the message + // would be resurrected on the next save. + await _channelMessageStore.removeChannelMessage(channelIndex, message); notifyListeners(); } @@ -1117,10 +1217,17 @@ class MeshCoreConnector extends ChangeNotifier { _contacts ..clear() ..addAll(cached); - for (final contact in cached) { - _ensureContactSmazSettingLoaded(contact.publicKeyHex); - _ensureContactCyr2LatSettingLoaded(contact.publicKeyHex); - } + // Load every contact's settings without notifying per contact, then + // notify once. Per-contact notification rebuilt the whole tree 2x per + // contact (600 rebuilds for 300 contacts), and each rebuild walks the + // contact list, which is what stalled the UI for ~44s on connect. + await Future.wait([ + for (final contact in cached) ...[ + _ensureContactSmazSettingLoaded(contact.publicKeyHex, notify: false), + _ensureContactCyr2LatSettingLoaded(contact.publicKeyHex, notify: false), + ], + ]); + notifyListeners(); } Future _loadDiscoveredContactCache() async { @@ -3165,11 +3272,15 @@ class MeshCoreConnector extends ChangeNotifier { } } + /// Pushes a path to the device. [hopCount] is a HOP count and [hashWidth] the + /// bytes per hop hash; both are needed because the wire path_len byte packs + /// them together, and sending a bare count mislabels the width. (#309) Future setContactPath( Contact contact, Uint8List customPath, - int pathLen, - ) async { + int hopCount, { + int hashWidth = 1, + }) async { // Serialize path operations to prevent interleaved async calls from // leaving in-memory state inconsistent with the device. final prev = _pathOpLock; @@ -3183,7 +3294,8 @@ class MeshCoreConnector extends ChangeNotifier { buildUpdateContactPathFrame( contact.publicKey, customPath, - pathLen, + hopCount, + hashWidth: hashWidth, type: contact.type, flags: contact.flags, name: contact.name, @@ -3198,8 +3310,12 @@ class MeshCoreConnector extends ChangeNotifier { (c) => c.publicKeyHex == contact.publicKeyHex, ); if (idx != -1) { + // pathLength is a HOP count. This wrote customPath.length (a BYTE + // count), which silently redefined the field's unit after any path + // set and doubled it at 2-byte width. (#309) _contacts[idx] = _contacts[idx].copyWith( - pathLength: customPath.length, + pathLength: hopCount, + pathHashWidth: hashWidth, path: customPath, ); notifyListeners(); @@ -3245,6 +3361,7 @@ class MeshCoreConnector extends ChangeNotifier { latestContact.publicKey, latestContact.path, latestContact.pathLength, + hashWidth: latestContact.pathHashWidth, type: latestContact.type, flags: updatedFlags, name: latestContact.name, @@ -3588,6 +3705,7 @@ class MeshCoreConnector extends ChangeNotifier { contact.publicKey, contact.path, contact.pathLength, + hashWidth: contact.pathHashWidth, type: contact.type, flags: contact.flags, name: contact.name, @@ -4257,7 +4375,64 @@ class MeshCoreConnector extends ChangeNotifier { } } + /// Any frame handler that occupies the UI isolate long enough to drop + /// frames is a bug: it stalls rendering, so progress bars freeze and input + /// stops responding while sync appears to do nothing and then finish at + /// once. Logged rather than assumed, so the offending code is named. + static const Duration _slowHandlerThreshold = Duration(milliseconds: 100); + + /// Cumulative synchronous time per frame code. A single handler under the + /// slow threshold can still dominate if it runs hundreds of times, which a + /// per-call threshold cannot see. + final Map _frameCodeMicros = {}; + final Map _frameCodeCount = {}; + int _frameTotalMicros = 0; + void _handleFrame(List data) { + final handlerWatch = Stopwatch()..start(); + _handleFrameInner(data); + handlerWatch.stop(); + if (data.isEmpty) return; + + final code = data[0]; + final micros = handlerWatch.elapsedMicroseconds; + _frameCodeMicros[code] = (_frameCodeMicros[code] ?? 0) + micros; + _frameCodeCount[code] = (_frameCodeCount[code] ?? 0) + 1; + _frameTotalMicros += micros; + + if (handlerWatch.elapsed > _slowHandlerThreshold) { + appLogger.info( + 'slow frame handler: code=$code blocked UI for ' + '${handlerWatch.elapsedMilliseconds}ms', + tag: 'Perf', + ); + } + + // Periodic cumulative report: names the code that owns the most isolate + // time even when no single call is slow. + if (_frameTotalMicros > 2000000) { + final ranked = _frameCodeMicros.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + final top = ranked + .take(4) + .map( + (e) => + 'code=${e.key} ${(e.value / 1000).round()}ms' + '/${_frameCodeCount[e.key]}calls', + ) + .join(' '); + appLogger.info( + 'frame handler cumulative (${(_frameTotalMicros / 1000).round()}ms ' + 'total sync): $top', + tag: 'Perf', + ); + _frameTotalMicros = 0; + _frameCodeMicros.clear(); + _frameCodeCount.clear(); + } + } + + void _handleFrameInner(List data) { if (data.isEmpty) return; _lastRxTime = DateTime.now(); @@ -4281,6 +4456,9 @@ class MeshCoreConnector extends ChangeNotifier { case cmdOffbandBlock: _handleOffbandBlockFrame(frame); break; + case cmdOffbandFemLna: + _handleOffbandFemLnaReply(frame); + break; case respCodeSelfInfo: debugPrint('Got SELF_INFO'); _handleSelfInfo(frame); @@ -4554,16 +4732,10 @@ class MeshCoreConnector extends ChangeNotifier { _channelStore.setPublicKeyHex = selfPublicKeyHex; _unreadStore.setPublicKeyHex = selfPublicKeyHex; - // Now that we have self info, we can load all the persisted data for this node - _loadChannelOrder(); - loadContactCache(); - loadChannelSettings(); - loadCachedChannels(); - - // Load persisted channel messages - loadAllChannelMessages(); - loadUnreadState(); - _loadDiscoveredContactCache(); + // Now that we have self info, we can load all the persisted data for this + // node. Each step is timed: this sequence has stalled the UI isolate for + // ~40s on a large store, and the timings say which step is responsible. + unawaited(_timedStartupLoad()); _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); @@ -4574,6 +4746,26 @@ class MeshCoreConnector extends ChangeNotifier { _maybeStartInitialChannelSync(); } + Future _timedStartupLoad() async { + Future step(String name, Future Function() body) async { + final sw = Stopwatch()..start(); + await body(); + sw.stop(); + appLogger.info( + 'startup-load $name took ${sw.elapsedMilliseconds}ms', + tag: 'Perf', + ); + } + + await step('channelOrder', () async => _loadChannelOrder()); + await step('contactCache', loadContactCache); + await step('channelSettings', () => loadChannelSettings()); + await step('cachedChannels', loadCachedChannels); + await step('channelMessages', () => loadAllChannelMessages()); + await step('unreadState', loadUnreadState); + await step('discoveredContacts', _loadDiscoveredContactCache); + } + /// Extract the additive `offband_caps` byte from a device-info reply. /// /// Offset verified against firmware MyMesh.cpp: the reply tail is three @@ -4585,6 +4777,14 @@ class MeshCoreConnector extends ChangeNotifier { static int? parseOffbandCaps(Uint8List frame) => frame.length >= 83 ? frame[82] : null; + /// FEM LNA state byte, appended immediately after the caps byte in device-info + /// v16+ (firmware #298). Appended **unconditionally** — including on + /// non-capable boards, where it reads 0 — so the byte's presence indicates + /// firmware version, not capability. The capability BIT is what decides + /// whether to render the control. Null on v15 and older (shorter frame). + static bool? parseFemLnaState(Uint8List frame) => + frame.length >= 84 ? frame[83] != femLnaBypass : null; + void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; if (_shouldGateInitialChannelSync) { @@ -4599,7 +4799,8 @@ class MeshCoreConnector extends ChangeNotifier { _firmwareVersion = info.version; _deviceModel = info.model; _appDebugLogService?.info( - 'Device info: ${infoStrings.isEmpty ? '(no strings)' : infoStrings.join(' · ')}', + 'Device info: ${infoStrings.isEmpty ? '(no strings)' : infoStrings.join(' · ')}' + ' (frameLen=${frame.length})', tag: 'Device', ); @@ -4608,15 +4809,42 @@ class MeshCoreConnector extends ChangeNotifier { _clientRepeat = frame[80] != 0; } // Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop + final priorPathHashByteWidth = _pathHashByteWidth; if (frame.length >= 82) { final mode = (frame[81] & 0xFF).clamp(0, 2); _pathHashByteWidth = mode + 1; } else { _pathHashByteWidth = 1; } + // A short frame silently downgrades a known-good width to 1 (#240). Log the + // frame length and the before/after width so any capture shows whether that + // happened, without needing the device. (#298) + final widthChanged = _pathHashByteWidth != priorPathHashByteWidth; + final widthNote = + 'Path hash width: $priorPathHashByteWidth -> $_pathHashByteWidth ' + '(device-info frame len=${frame.length}' + '${frame.length < 82 ? ', SHORT: no byte 81, forced to 1' : ''})'; + if (widthChanged) { + _appDebugLogService?.warn(widthNote, tag: 'Device'); + } else { + _appDebugLogService?.info(widthNote, tag: 'Device'); + } // Offband config capability v14+ (byte 82). Extracted + bounds-checked in a // testable helper; offset verified against firmware (see parseOffbandCaps). _offbandCaps = parseOffbandCaps(frame); + // FEM LNA state rides one byte past the caps byte on v16+ (#304). Primary + // read on connect — a 0xC3 GET is only the fallback. + _femLnaEnabled = parseFemLnaState(frame); + // Capability-gated features are invisible when a bit is clear, which looks + // identical to a bug. Log the raw inputs so "the toggle didn't appear" can + // be told apart from "this radio says it can't". (#304) + _appDebugLogService?.info( + 'Offband caps=0x${(_offbandCaps ?? 0).toRadixString(16).padLeft(2, '0')} ' + 'verCode=${_firmwareVerCode ?? 0} frameLen=${frame.length} ' + 'femLnaByte=${_femLnaEnabled == null ? 'absent' : (_femLnaEnabled! ? '1' : '0')} ' + 'femCapable=$supportsOffbandFemLna blockCapable=$supportsOffbandBlock', + tag: 'Device', + ); // Caps just landed; (re)evaluate GPS polling in case a `gps=1` custom-var // frame arrived before this device-info reply set support. (#144) _reconcileGpsPolling(); @@ -4877,6 +5105,28 @@ class MeshCoreConnector extends ChangeNotifier { return physicsMax; } + /// Coalesces notifications during a bulk contact pull. + /// + /// Outside a pull this notifies immediately, preserving live-update + /// behaviour for adverts arriving one at a time. + DateTime? _lastContactPullNotify; + static const Duration _contactPullNotifyInterval = Duration( + milliseconds: 250, + ); + + void _notifyContactPullThrottled() { + if (!_isLoadingContacts) { + notifyListeners(); + return; + } + final now = DateTime.now(); + final last = _lastContactPullNotify; + if (last == null || now.difference(last) >= _contactPullNotifyInterval) { + _lastContactPullNotify = now; + notifyListeners(); + } + } + void _handleContact(Uint8List frame, {bool isContact = true}) { final contactTmp = Contact.fromFrame(frame); if (contactTmp != null) { @@ -4918,7 +5168,7 @@ class MeshCoreConnector extends ChangeNotifier { : contact.lastMessageAt; appLogger.info( - 'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', + 'Refreshing contact ${contact.name}: devicePath=${_pathDiag(contact.path, contact.pathLength, contact.pathHashWidth)}, existingOverride=${existing.pathOverride}', tag: 'Connector', ); @@ -4933,7 +5183,7 @@ class MeshCoreConnector extends ChangeNotifier { ); appLogger.info( - 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_pathDiag(_contacts[existingIndex].path, _contacts[existingIndex].pathLength, _contacts[existingIndex].pathHashWidth)}', tag: 'Connector', ); } else { @@ -4944,7 +5194,7 @@ class MeshCoreConnector extends ChangeNotifier { isContact) { _contacts.add(contact); appLogger.info( - 'Added new contact ${contact.name}: pathLen=${contact.pathLength}', + 'Added new contact ${contact.name}: pathLen=${_pathDiag(contact.path, contact.pathLength, contact.pathHashWidth)}', tag: 'Connector', ); } else { @@ -4964,7 +5214,14 @@ class MeshCoreConnector extends ChangeNotifier { _pathHistoryService!.handlePathUpdated(contact); } - notifyListeners(); + // During a bulk pull this fired once per contact, and each notification + // is a synchronous full-tree rebuild that itself walks the contact list. + // Measured at ~19ms per contact, which never tripped a per-call slow + // threshold but dominated total isolate time. The channel handler + // already guards its notify with _isLoadingChannels; this is the same + // guard, throttled rather than suppressed so the sync progress bar still + // advances while the pull runs. + _notifyContactPullThrottled(); // Show notification for new contact (advertisement) if (isNewContact && _appSettingsService != null) { @@ -5032,7 +5289,7 @@ class MeshCoreConnector extends ChangeNotifier { ); appLogger.info( - 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_pathDiag(_contacts[existingIndex].path, _contacts[existingIndex].pathLength, _contacts[existingIndex].pathHashWidth)}', tag: 'Connector', ); } else { @@ -5497,22 +5754,32 @@ class MeshCoreConnector extends ChangeNotifier { return frame.sublist(prefixOffset, prefixOffset + prefixLen); } - void _ensureContactSmazSettingLoaded(String contactKeyHex) { + /// [notify] is false for bulk loads, which notify once at the end instead. + /// Notifying per contact rebuilds the whole tree once per contact, and each + /// rebuild itself walks the contact list, so a large address book turns this + /// into O(contacts^2) work on the UI isolate. + Future _ensureContactSmazSettingLoaded( + String contactKeyHex, { + bool notify = true, + }) async { if (_contactSmazEnabled.containsKey(contactKeyHex)) return; - _contactSettingsStore.loadSmazEnabled(contactKeyHex).then((enabled) { - if (_contactSmazEnabled[contactKeyHex] == enabled) return; - _contactSmazEnabled[contactKeyHex] = enabled; - notifyListeners(); - }); + final enabled = await _contactSettingsStore.loadSmazEnabled(contactKeyHex); + if (_contactSmazEnabled[contactKeyHex] == enabled) return; + _contactSmazEnabled[contactKeyHex] = enabled; + if (notify) notifyListeners(); } - void _ensureContactCyr2LatSettingLoaded(String contactKeyHex) { + Future _ensureContactCyr2LatSettingLoaded( + String contactKeyHex, { + bool notify = true, + }) async { if (_contactCyr2LatEnabled.containsKey(contactKeyHex)) return; - _contactSettingsStore.loadCyr2LatEnabled(contactKeyHex).then((enabled) { - if (_contactCyr2LatEnabled[contactKeyHex] == enabled) return; - _contactCyr2LatEnabled[contactKeyHex] = enabled; - notifyListeners(); - }); + final enabled = await _contactSettingsStore.loadCyr2LatEnabled( + contactKeyHex, + ); + if (_contactCyr2LatEnabled[contactKeyHex] == enabled) return; + _contactCyr2LatEnabled[contactKeyHex] = enabled; + if (notify) notifyListeners(); } void _ensureContactCyr2LatProfileLoaded(String contactKeyHex) { @@ -7154,7 +7421,7 @@ class MeshCoreConnector extends ChangeNotifier { : existing.lastMessageAt; appLogger.info( - 'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}', + 'Refreshing contact ${existing.name}: devicePath=${_pathDiag(existing.path, existing.pathLength, existing.pathHashWidth)}, existingOverride=${existing.pathOverride}', tag: 'Connector', ); @@ -7180,7 +7447,7 @@ class MeshCoreConnector extends ChangeNotifier { _updateDirectRepeater(_contacts[existingIndex], snr, path); appLogger.info( - 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_pathDiag(_contacts[existingIndex].path, _contacts[existingIndex].pathLength, _contacts[existingIndex].pathHashWidth)}', tag: 'Connector', ); } diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 13e3a05..f486e79 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -263,6 +263,54 @@ const int respCodeOffbandGps = 0xC1; /// Request frame for [cmdOffbandGps] — a bare 1-byte command, no payload. (#135) Uint8List buildOffbandGpsRequestFrame() => Uint8List.fromList([cmdOffbandGps]); +// --- Offband FEM LNA command (0xC3) — capability-gated. Heltec V4 external +// FEM LNA control; firmware counterpart OffbandMesh/meshcore-firmware#298. +// +// Deliberately a fork-private command rather than an extra byte on the stock +// CMD_SET_OTHER_PARAMS (38): that frame is shared with upstream MeshCore and is +// sent to every radio regardless of fork, so widening it would perturb stock +// firmware. Nothing here is emitted unless the capability bit is set. (#304) +const int cmdOffbandFemLna = 0xC3; +const int offbandFemLnaSet = 0x01; +const int offbandFemLnaGet = 0x02; + +/// `0` = FEM LNA bypassed, `1` = enabled. Firmware default is enabled. +const int femLnaBypass = 0x00; +const int femLnaEnabled = 0x01; + +Uint8List buildOffbandFemLnaSetFrame(bool enabled) => Uint8List.fromList([ + cmdOffbandFemLna, + offbandFemLnaSet, + enabled ? femLnaEnabled : femLnaBypass, +]); + +Uint8List buildOffbandFemLnaGetFrame() => + Uint8List.fromList([cmdOffbandFemLna, offbandFemLnaGet]); + +/// Reply to a `0xC3` request: `[0xC3][sub][value]`. +/// +/// The value is the **post-apply hardware state, not an echo of the request** +/// (firmware #298 as-built): if the FEM ever refused a write, this reports the +/// truth rather than confirming a change that didn't take. Always render from +/// this value; never assume the written value stuck. +/// +/// Error replies are never 0xC3-prefixed, so they don't reach this parser: +/// malformed → `[respCodeErr][errCodeIllegalArg]`; a request to a non-capable +/// board → `[respCodeErr][errCodeUnsupportedCmd]` (unreachable when gated on +/// the capability bit, but firmware answers it defensively). +class OffbandFemLnaReply { + const OffbandFemLnaReply(this.subType, this.value); + final int subType; + final int value; + + bool get enabled => value != femLnaBypass; +} + +OffbandFemLnaReply? parseOffbandFemLnaReply(Uint8List frame) { + if (frame.length < 3 || frame[0] != cmdOffbandFemLna) return null; + return OffbandFemLnaReply(frame[1], frame[2]); +} + // --- Offband block command (0xC2) — capability-gated; see // docs/architecture/block-contract-as-built.md §8. Firmware as-built PR #247. --- const int cmdOffbandBlock = 0xC2; @@ -276,6 +324,12 @@ const int offbandBlockClear = 0x04; /// app must recognise the 2-byte error frame and not wait for a 0xC2 echo. const int errCodeIllegalArg = 6; +/// `ERR_CODE_UNSUPPORTED_CMD` — returned for an Offband command the connected +/// board can't service (e.g. a `0xC3` FEM LNA request to a board without FEM +/// control). Unreachable when the capability bit is respected; firmware answers +/// it defensively against a stale or mis-gated client. (#304) +const int errCodeUnsupportedCmd = 1; + Uint8List buildOffbandBlockAddFrame(Uint8List pubKey) => Uint8List.fromList([cmdOffbandBlock, offbandBlockAdd, ...pubKey]); Uint8List buildOffbandBlockRemoveFrame(Uint8List pubKey) => @@ -319,6 +373,21 @@ bool firmwareSupportsOffbandGps(int? offbandCaps) => offbandCaps != null; /// `FIRMWARE_VER_CODE >= 15`; absent → app-only mode (no sync, no firmware drop). const int offbandCapBlock = 0x02; +/// `OFFBAND_CAP_FEM_LNA` bit (bit 2) in the `offband_caps` byte: this radio can +/// control its external FEM LNA (firmware #298). +/// +/// PROVISIONAL — firmware owns the caps byte and has not yet confirmed 0x04 as +/// free. Do not ship against this without that confirmation (#304). +/// +/// Gate on the BIT ONLY, never on model or version: firmware derives it at +/// runtime from the auto-detected FEM chip (KCT8103L vs GC1109), so it is a +/// per-unit answer — two Heltec V4s can legitimately disagree, and other +/// FEM-bearing boards report false today. +const int offbandCapFemLna = 0x04; + +bool firmwareSupportsOffbandFemLna(int? offbandCaps) => + offbandCaps != null && (offbandCaps & offbandCapFemLna) != 0; + bool firmwareSupportsOffbandBlock(int? offbandCaps, int? firmwareVerCode) => offbandCaps != null && (offbandCaps & offbandCapBlock) != 0 && @@ -514,24 +583,50 @@ int readInt32LE(Uint8List data, int offset) { return val; } -// Path-length byte from the firmware. Low 6 bits = the path length field -// (a BYTE count of the hop-hash array, 0-63); high 2 bits carry an optional -// hash-size mode hint (0..2 -> 1..3 bytes/hop) that is not reliably populated, -// so the device's configured hash width (MeshCoreConnector.pathHashByteWidth) -// is authoritative for slicing the path. realHopCount() converts the byte -// length to a true hop count at that width. (#112) -// TX counterpart: buildSetPathHashModeFrame (CMD_SET_PATH_HASH_MODE). +// Path-length byte from the firmware. This is a PACKED field, and both halves +// are authoritative — the path is self-describing on the wire: +// +// high 2 bits = hash size - 1 (0..2 -> 1..3 bytes per hop hash) +// low 6 bits = hash COUNT (the number of HOPS, 0-63) +// byte length = count * size +// +// Verified against firmware `src/Packet.h:79-84`: +// getPathHashSize() == (path_len >> 6) + 1 +// getPathHashCount() == path_len & 63 +// getPathByteLen() == getPathHashCount() * getPathHashSize() +// setPathHashSizeAndCount(sz, n) { path_len = ((sz - 1) << 6) | (n & 63); } +// +// The high bits are set deliberately by that setter, so the per-path width +// travels with the path. The companion contact frame carries this same encoded +// byte verbatim: `Packet::copyPath()` returns path_len unchanged +// (`src/Packet.cpp:32-35`) into `ContactInfo.out_path_len` +// (`src/helpers/BaseChatMesh.cpp:319`), which `writeContactRespFrame` emits +// as-is (`examples/companion_radio/MyMesh.cpp:205-212`). +// +// A prior comment here claimed the low 6 bits were a BYTE count and that the +// high bits were "not reliably populated". Both were wrong, and #222 plus the +// Contact decode were built on them: the app read `count` bytes where firmware +// meant `count` hops, keeping half of every path at 2-byte width. (#309) +// +// TX counterparts: buildSetPathHashModeFrame (CMD_SET_PATH_HASH_MODE) sets the +// device-wide default width; encodePathLen() packs a per-path value to send. int pathHopCount(int pathLenRaw) => pathLenRaw & 0x3F; int pathHashSizeBytes(int pathLenRaw) => ((pathLenRaw >> 6) & 0x03) + 1; -/// Real hop count from the firmware path byte-length and the device hash width. -/// `pathHopCount` returns the path BYTE length, so at a 2-byte hash width a -/// single 2-byte hop reports 2 — divide by the width to get the true hop count. -/// Null (unknown) and negative (flood) sentinels pass through unchanged. (#112) -int? realHopCount(int? rawByteLen, int hashWidth) { - if (rawByteLen == null || rawByteLen < 0) return rawByteLen; - final w = hashWidth < 1 ? 1 : hashWidth; - return rawByteLen ~/ w; +/// Byte length of the hop-hash array described by a raw path-length byte. +int pathByteLength(int pathLenRaw) => + pathHopCount(pathLenRaw) * pathHashSizeBytes(pathLenRaw); + +/// Packs a hop count and per-hop hash width into the firmware path-length byte. +/// +/// Mirrors firmware `Packet::setPathHashSizeAndCount`. Sending a bare hop count +/// (mode bits 00) tells the radio "1-byte hashes", so a 2-byte path routed that +/// way is read one byte per hop and goes to nodes that were never on the route. +/// Width is clamped to 1..3; mode 3 is reserved by firmware +/// (`isValidPathLen` rejects hash_size == 4). (#309) +int encodePathLen(int hopCount, int hashWidth) { + final w = hashWidth.clamp(1, 3); + return ((w - 1) << 6) | (hopCount & 0x3F); } /// Readable strings from a RESP_CODE_DEVICE_INFO frame: build date, model, and @@ -824,10 +919,19 @@ Uint8List buildResetPathFrame(Uint8List pubKey) { // Build CMD_ADD_UPDATE_CONTACT frame to set custom path // Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4] +// +// [hopCount] is a HOP count and [hashWidth] the bytes per hop hash; the two are +// packed into the single wire path_len byte via encodePathLen(). +// +// This previously wrote the count raw, leaving the mode bits 00 — which tells +// the radio "1-byte hashes". On a 2-byte net that handed the firmware 2-byte +// hash data labelled as 1-byte hops, so it routed to nodes that were never on +// the route. That is the send-side half of #240's misrouting. (#309) Uint8List buildUpdateContactPathFrame( Uint8List pubKey, Uint8List path, - int pathLen, { + int hopCount, { + int hashWidth = 1, int type = 1, // ADV_TYPE_CHAT int flags = 0, String name = '', @@ -840,7 +944,8 @@ Uint8List buildUpdateContactPathFrame( writer.writeBytes(pubKey); writer.writeByte(type); writer.writeByte(flags); - writer.writeByte(pathLen); + // Negative = flood sentinel, passed through as the firmware's 0xFF. + writer.writeByte(hopCount < 0 ? 0xFF : encodePathLen(hopCount, hashWidth)); writer.writeBytesPadded(path, maxPathSize); diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart index f97ee3a..5cbf244 100644 --- a/lib/helpers/gif_helper.dart +++ b/lib/helpers/gif_helper.dart @@ -31,7 +31,14 @@ class GifHelper { final pageMatch = RegExp( r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$', ).firstMatch(trimmed); - return pageMatch?.group(1); + if (pageMatch != null) { + return pageMatch.group(1); + } + // The CDN form people actually paste out of a browser. (#283) + final cdnMatch = RegExp( + r'^(?:https?:\/\/)?i\.giphy\.com\/([A-Za-z0-9_-]+)\.(?:gif|webp)$', + ).firstMatch(trimmed); + return cdnMatch?.group(1); } /// Encode a GIF as a Giphy page URL, a format parseGif() also parses. @@ -43,4 +50,31 @@ class GifHelper { static String encodeGif(String gifId) { return 'https://giphy.com/gifs/$gifId'; } + + /// Tenor's own CDN, the only Tenor form that is directly renderable. + /// + /// A `tenor.com/view/-` page URL is deliberately NOT matched: the + /// modern CDN path uses an opaque hash that cannot be derived from the page + /// id, so resolving one needs the Tenor API. Such a link stays plain text + /// rather than silently rendering the wrong image. (#283) + static final RegExp _tenorMedia = RegExp( + r'^(?:https?:\/\/)?(?:media|c)\.tenor\.com\/[A-Za-z0-9_\-\/]+\.(?:gif|webp)$', + ); + + /// The URL that renders [text], or null if it is not a GIF we will display. + /// + /// Rendering is restricted to an allowlist of curated GIF platforms (Giphy + /// and Tenor). Anything off-list returns null and stays a plain tap-to-open + /// link: auto-fetching an arbitrary URL leaks the viewer's IP and enables + /// tracking-pixel abuse, and inline-rendering unmoderated hosts is a malware + /// and inappropriate-content vector. Widening the allowlist is a deliberate + /// decision, not a convenience. (#283) + static String? resolveGifUrl(String text) { + final gifId = parseGif(text); + if (gifId != null) { + return 'https://media.giphy.com/media/$gifId/giphy.gif'; + } + final trimmed = text.trim().replaceFirst(RegExp(r'^@\[[^\]]+\]\s+'), ''); + return _tenorMedia.hasMatch(trimmed) ? trimmed : null; + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 88673f8..a7b3413 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -236,6 +236,7 @@ "settings_infoStatus": "Status", "settings_infoFirmware": "Firmware", "settings_infoModel": "Model", + "settings_infoOffbandCaps": "Offband capabilities", "settings_infoBattery": "Battery", "settings_infoPublicKey": "Public Key", "settings_publicKeyCopied": "Public key copied", @@ -253,6 +254,8 @@ "settings_txPowerInvalid": "Invalid TX power (0-22 dBm)", "settings_clientRepeat": "Off-Grid Repeat", "settings_clientRepeatSubtitle": "Allow this device to repeat mesh packets for others", + "settings_femLna": "Receive amplifier (FEM LNA)", + "settings_femLnaSubtitle": "Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.", "settings_clientRepeatFreqWarning": "Off-grid repeat requires 433, 869, or 918 MHz frequency", "settings_error": "Error: {message}", "@settings_error": { @@ -973,6 +976,7 @@ "map_repeaters": "Repeaters", "map_otherNodes": "Other Nodes", "map_showOverlaps": "Repeater Key Overlaps", + "map_alwaysShowNames": "Always show names", "map_keyPrefix": "Key Prefix", "map_filterByKeyPrefix": "Filter by key prefix", "map_publicKeyPrefix": "Public key prefix", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5ddefeb..0ad7394 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1174,6 +1174,12 @@ abstract class AppLocalizations { /// **'Model'** String get settings_infoModel; + /// No description provided for @settings_infoOffbandCaps. + /// + /// In en, this message translates to: + /// **'Offband capabilities'** + String get settings_infoOffbandCaps; + /// No description provided for @settings_infoBattery. /// /// In en, this message translates to: @@ -1276,6 +1282,18 @@ abstract class AppLocalizations { /// **'Allow this device to repeat mesh packets for others'** String get settings_clientRepeatSubtitle; + /// No description provided for @settings_femLna. + /// + /// In en, this message translates to: + /// **'Receive amplifier (FEM LNA)'** + String get settings_femLna; + + /// No description provided for @settings_femLnaSubtitle. + /// + /// In en, this message translates to: + /// **'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'** + String get settings_femLnaSubtitle; + /// No description provided for @settings_clientRepeatFreqWarning. /// /// In en, this message translates to: @@ -3394,6 +3412,12 @@ abstract class AppLocalizations { /// **'Repeater Key Overlaps'** String get map_showOverlaps; + /// No description provided for @map_alwaysShowNames. + /// + /// In en, this message translates to: + /// **'Always show names'** + String get map_alwaysShowNames; + /// No description provided for @map_keyPrefix. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index f1f49ef..a379cc6 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -590,6 +590,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Батерия'; @@ -642,6 +645,13 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; @@ -1880,6 +1890,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_showOverlaps => 'Покриване на ключа на повтаряча'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Префикс на ключа'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index eac244b..b60bea3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -585,6 +585,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Akku'; @@ -637,6 +640,13 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.'; @@ -1877,6 +1887,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_showOverlaps => 'Überlappungen der Repeater-Taste'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Schlüsselpräfix'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3f25139..db7573f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -575,6 +575,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Battery'; @@ -627,6 +630,13 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Allow this device to repeat mesh packets for others'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Off-grid repeat requires 433, 869, or 918 MHz frequency'; @@ -1844,6 +1854,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showOverlaps => 'Repeater Key Overlaps'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Key Prefix'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index aee1983..9253a00 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -586,6 +586,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Batería'; @@ -638,6 +641,13 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Permita que este dispositivo repita los paquetes de red para otros usuarios.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; @@ -1875,6 +1885,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_showOverlaps => 'Superposiciones de tecla repetidora'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefijo de clave'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 04cd4e9..19bd894 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -590,6 +590,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Batterie'; @@ -642,6 +645,13 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; @@ -1885,6 +1895,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_showOverlaps => 'Chevauchement de la touche répétitive'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Préfixe clé'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 48f7f97..98933ef 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -588,6 +588,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Akku'; @@ -642,6 +645,13 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Engedje, hogy ez a eszköz mások számára is ismételje a hálózati csomagokat.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'A hálózat nélküli kommunikációhoz 433, 869 vagy 918 MHz frekvenciát igényel.'; @@ -1886,6 +1896,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get map_showOverlaps => 'Az ismétlő kulcsok ütköznek'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Kulcsfontosságú előtag'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 875eedc..b0197a8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -588,6 +588,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Batteria'; @@ -640,6 +643,13 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; @@ -1878,6 +1888,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefisso Chiave'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4546578..b244d39 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -561,6 +561,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'バッテリー'; @@ -613,6 +616,13 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_clientRepeatSubtitle => 'このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。'; @@ -1801,6 +1811,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get map_showOverlaps => 'リピーターキーの重複'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => '主要なプレフィックス'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index d45ac1b..90228a2 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -561,6 +561,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => '배터리'; @@ -613,6 +616,13 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_clientRepeatSubtitle => '이 장치가 다른 사람들을 위해 메시 패킷을 반복하도록 허용합니다.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => '오프그리드(무전력) 시스템 재연결에는 433MHz, 869MHz, 또는 918MHz 주파수가 필요합니다.'; @@ -1797,6 +1807,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get map_showOverlaps => '반복 키 중복'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => '핵심 접두사'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 5d857ce..440530b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -582,6 +582,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Batterij'; @@ -634,6 +637,13 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Laat dit apparaat de berichten van andere apparaten doorsturen.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.'; @@ -1864,6 +1874,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_showOverlaps => 'Herhalingssleutel overlapt'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefix sleutel'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 72d7782..be65e06 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -590,6 +590,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Bateria'; @@ -643,6 +646,13 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.'; @@ -1890,6 +1900,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_showOverlaps => 'Nakładające się klucze przekaźników'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefiks klucza'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index f7f33a4..efcfad0 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -588,6 +588,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Bateria'; @@ -640,6 +643,13 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Permita que este dispositivo repita pacotes de rede para outros dispositivos.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; @@ -1875,6 +1885,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_showOverlaps => 'Sobreposições da Chave Repeater'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefixo Chave'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c218e2c..63b5679 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -587,6 +587,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Батарея'; @@ -640,6 +643,13 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Позвольте этому устройству повторять пакеты данных для других устройств.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; @@ -1879,6 +1889,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_showOverlaps => 'Перекрытия ключа повтора'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Префикс ключа'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 14e8a14..be75989 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -582,6 +582,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Batéria'; @@ -634,6 +637,13 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; @@ -1866,6 +1876,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_showOverlaps => 'Prekrývanie opakovača kľúča'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Päťciferné predpona'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 518d551..4b9518c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -579,6 +579,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Baterija'; @@ -631,6 +634,13 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; @@ -1861,6 +1871,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Predpona ključa'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c6f9fed..ab1047d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -577,6 +577,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Batteri'; @@ -629,6 +632,13 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Låt enheten repetera nätpaket för andra användare.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; @@ -1854,6 +1864,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_showOverlaps => 'Repeater-nyckelöverlappningar'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Nyckelprefix'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f0dab52..ee32259 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -585,6 +585,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => 'Батарея'; @@ -637,6 +640,13 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_clientRepeatSubtitle => 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; @@ -1874,6 +1884,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_showOverlaps => 'Перекриття ключів ретрансляторів'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Префікс ключа'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d39404c..7ae7266 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -553,6 +553,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_infoModel => 'Model'; + @override + String get settings_infoOffbandCaps => 'Offband capabilities'; + @override String get settings_infoBattery => '电池'; @@ -604,6 +607,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_clientRepeatSubtitle => '允许此设备重复发送网状数据包给其他设备'; + @override + String get settings_femLna => 'Receive amplifier (FEM LNA)'; + + @override + String get settings_femLnaSubtitle => + 'Boosts receive sensitivity. Leave on unless you are troubleshooting a strong nearby signal.'; + @override String get settings_clientRepeatFreqWarning => '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; @@ -1768,6 +1778,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showOverlaps => '重复键重叠'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => '关键字前缀'; diff --git a/lib/main.dart b/lib/main.dart index 52f3a6f..481334c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,6 +27,8 @@ import 'services/ui_view_state_service.dart'; import 'services/timeout_prediction_service.dart'; import 'services/observer_config_service.dart'; import 'services/block_service.dart'; +import 'services/window_geometry_service.dart'; +import 'storage/drift/blob_store.dart'; import 'storage/prefs_manager.dart'; import 'utils/app_logger.dart'; @@ -36,9 +38,20 @@ void main() async { // Initialize SharedPreferences cache await PrefsManager.initialize(); + // Move bulk data (message history, contacts) out of SharedPreferences into + // drift (#335). Must run after prefs are up and BEFORE any store reads, so + // no code sees a half-migrated state. Idempotent: a no-op once done. + // + // Deliberately awaited: the alternative is stores racing the migration, and + // this is the failure mode that produced #333. + await BlobStore.instance.migrateFromPrefs(); + // Start always-on file logging (#97); no-op on web. await FileLogService.instance.init(); + // Restore the desktop window's saved position/size (#349); no-op off desktop. + await WindowGeometryService.instance.initialize(); + // Initialize services final storage = StorageService(); final connector = MeshCoreConnector(); diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 005197f..782e78a 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -112,6 +112,9 @@ class AppSettings { final bool mapShowChatNodes; final bool mapShowOtherNodes; final bool mapShowOverlaps; + + /// Show node names at any zoom, instead of only past the zoom threshold. + final bool mapAlwaysShowNames; final double mapTimeFilterHours; // 0 = all time final bool mapKeyPrefixEnabled; final String mapKeyPrefix; @@ -188,6 +191,7 @@ class AppSettings { this.mapShowChatNodes = true, this.mapShowOtherNodes = true, this.mapShowOverlaps = false, + this.mapAlwaysShowNames = false, this.mapTimeFilterHours = 0, // Default to all time this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', @@ -252,6 +256,7 @@ class AppSettings { 'map_show_chat_nodes': mapShowChatNodes, 'map_show_other_nodes': mapShowOtherNodes, 'map_show_overlaps': mapShowOverlaps, + 'map_always_show_names': mapAlwaysShowNames, 'map_time_filter_hours': mapTimeFilterHours, 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, @@ -338,6 +343,7 @@ class AppSettings { mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true, mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true, mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false, + mapAlwaysShowNames: json['map_always_show_names'] as bool? ?? false, mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0, mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, @@ -462,6 +468,7 @@ class AppSettings { bool? mapShowChatNodes, bool? mapShowOtherNodes, bool? mapShowOverlaps, + bool? mapAlwaysShowNames, double? mapTimeFilterHours, bool? mapKeyPrefixEnabled, String? mapKeyPrefix, @@ -510,6 +517,7 @@ class AppSettings { mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes, mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes, mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps, + mapAlwaysShowNames: mapAlwaysShowNames ?? this.mapAlwaysShowNames, mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours, mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 7c690d3..07e2d1c 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -8,8 +8,22 @@ class Contact { final String name; final int type; final int flags; - final int pathLength; // -1 = flood, 0+ = direct hops (from device) - final Uint8List path; // Path bytes from device + + /// Hop count for [path]: -1 = flood, 0+ = number of hops. + /// + /// This is the firmware path-len field's low 6 bits, which firmware defines + /// as a hash COUNT, not a byte length (`src/Packet.h:79-84`). It was + /// previously treated as a byte count, which truncated every path at widths + /// above 1. (#309) + final int pathLength; + + /// Bytes per hop hash in [path] (1..3), from the path-len byte's high 2 bits. + /// + /// Carried per-path rather than read from the connector's global width, so a + /// stored path can never be re-sliced at a width it was not captured at. + final int pathHashWidth; + + final Uint8List path; // Path bytes from device (pathLength * pathHashWidth) final int? pathOverride; // User's path override: -1 = force flood, null = auto final Uint8List? pathOverrideBytes; // User's path override bytes @@ -28,6 +42,7 @@ class Contact { required this.type, this.flags = 0, required this.pathLength, + this.pathHashWidth = 1, required this.path, this.pathOverride, this.pathOverrideBytes, @@ -80,6 +95,7 @@ class Contact { int? type, int? flags, int? pathLength, + int? pathHashWidth, Uint8List? path, int? pathOverride, Uint8List? pathOverrideBytes, @@ -98,6 +114,7 @@ class Contact { type: type ?? this.type, flags: flags ?? this.flags, pathLength: pathLength ?? this.pathLength, + pathHashWidth: pathHashWidth ?? this.pathHashWidth, path: path ?? this.path, pathOverride: clearPathOverride ? null @@ -133,8 +150,8 @@ class Contact { return parts.join(','); } - /// Default grouping uses legacy single-byte hop hash width. - String get pathIdList => pathFormattedIdList(pathHashSize); + /// Groups by this path's own captured width, not a global or legacy default. + String get pathIdList => pathFormattedIdList(pathHashWidth); String get shortPubKeyHex { return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; @@ -166,14 +183,20 @@ class Contact { final type = reader.readByte(); final flags = reader.readByte(); final pathLen = reader.readByte(); - // The firmware path-len byte packs a hash-mode hint in the high 2 bits and - // the path BYTE length in the low 6 (#222). Decode before use: a direct - // node at 2-byte mode sends 0x40, whose raw value would otherwise read as - // 64 hops and pull 64 junk bytes into the path. 0xFF stays the flood - // sentinel; the device-configured pathHashByteWidth converts bytes->hops - // at display time. - final byteLen = pathLen == 0xFF ? -1 : pathHopCount(pathLen); - final safePathLen = byteLen > 0 ? byteLen : 0; + // The firmware path-len byte packs hash size in the high 2 bits and hash + // COUNT (hops) in the low 6; the byte length is count * size + // (`src/Packet.h:79-84`). 0xFF stays the flood sentinel. + // + // This previously read `count` BYTES, so at 2-byte width it kept half of + // every path and discarded the rest — the truncation behind #240's failed + // repeater logins. The width is taken from the path itself rather than the + // connector's global width, so a path is always sliced at the width it was + // captured at. (#309) + final isFlood = pathLen == 0xFF; + final hopCount = isFlood ? -1 : pathHopCount(pathLen); + final hashWidth = isFlood ? 1 : pathHashSizeBytes(pathLen); + final byteLen = isFlood ? 0 : (hopCount * hashWidth); + final safePathLen = byteLen.clamp(0, maxPathSize); final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); final name = reader.readCStringGreedy(maxNameSize); @@ -218,7 +241,9 @@ class Contact { name: name.isEmpty ? 'Unknown' : name, type: type, flags: flags, - pathLength: byteLen, // decoded low-6-bit byte length; -1 = flood (#222) + pathLength: + hopCount, // hop count from the low 6 bits; -1 = flood (#309) + pathHashWidth: hashWidth, // bytes/hop from the high 2 bits (#309) path: pathBytes, latitude: lat, longitude: lon, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index fb9ff0d..6917817 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -29,6 +29,8 @@ import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; import '../utils/emoji_utils.dart'; import '../utils/route_transitions.dart'; +import 'settings_screen.dart'; +import '../utils/dialog_utils.dart'; import '../widgets/app_shell.dart'; import '../widgets/channel_drawer_list.dart'; import '../widgets/mention_autocomplete.dart'; @@ -287,6 +289,11 @@ class _ChannelChatScreenState extends State { ); } + Future _disconnect(BuildContext context) async { + final connector = context.read(); + await showDisconnectDialog(context, connector); + } + /// Bottom-bar navigation out of an open channel. /// /// Tapping Channels returns to the channel list. Tapping Contacts or Map @@ -351,6 +358,13 @@ class _ChannelChatScreenState extends State { currentChannelIndex: _currentChannel.index, onChannelSelected: _switchChannel, ), + // Present here too: the footer is app-level, so it belongs on every + // screen carrying the panel, not just the primary views. + onDisconnect: () => _disconnect(context), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), appBarBuilder: (context, pinned) => AppBar( // Pinned: the panel is already docked, so no hamburger is needed. // Unpinned: this is a pushed route, so Scaffold would put a back arrow @@ -642,8 +656,8 @@ class _ChannelChatScreenState extends State { final enableTracing = settingsService.settings.enableMessageTracing; final hashWidth = context.read().pathHashByteWidth; final isOutgoing = message.isOutgoing; - final gifId = GifHelper.parseGif(message.text); - final gifReplyName = gifId != null + final gifUrl = GifHelper.resolveGifUrl(message.text); + final gifReplyName = gifUrl != null ? TranslatedMessageContent.leadingReplyName(message.text) : null; final poi = parseMarkerText(message.text); @@ -689,7 +703,7 @@ class _ChannelChatScreenState extends State { ? (_) => _showMessageActions(message) : null, child: Container( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.all(4) : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( @@ -706,7 +720,7 @@ class _ChannelChatScreenState extends State { children: [ if (!isOutgoing) ...[ Padding( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.only( left: 8, top: 4, @@ -722,7 +736,7 @@ class _ChannelChatScreenState extends State { ), ), ), - if (gifId == null) const SizedBox(height: 4), + if (gifUrl == null) const SizedBox(height: 4), ], if (poi != null) _buildPoiMessage( @@ -732,7 +746,7 @@ class _ChannelChatScreenState extends State { textScale, message.senderName, ) - else if (gifId != null) + else if (gifUrl != null) Column( crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end @@ -752,8 +766,7 @@ class _ChannelChatScreenState extends State { ClipRRect( borderRadius: BorderRadius.circular(8), child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', + url: gifUrl, backgroundColor: Colors.transparent, fallbackTextColor: isOutgoing ? Theme.of(context) @@ -797,7 +810,7 @@ class _ChannelChatScreenState extends State { if (enableTracing && displayPath.isNotEmpty) ...[ const SizedBox(height: 2), Padding( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.symmetric(horizontal: 8) : EdgeInsets.zero, child: Text( @@ -1161,8 +1174,8 @@ class _ChannelChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = GifHelper.parseGif(value.text); - if (gifId != null) { + final gifUrl = GifHelper.resolveGifUrl(value.text); + if (gifUrl != null) { return Focus( autofocus: true, onKeyEvent: (node, event) { @@ -1181,8 +1194,7 @@ class _ChannelChatScreenState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', + url: gifUrl, backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerHighest, @@ -1530,8 +1542,9 @@ class _ChannelChatScreenState extends State { } Widget _buildHopBadge(BuildContext context, ChannelMessage message) { - final hashWidth = context.read().pathHashByteWidth; - final hops = realHopCount(message.hopCount, hashWidth) ?? 0; + // message.hopCount is already a hop count (path-len low 6 bits). It used to + // be divided by the device width, which halved it at 2-byte width. (#309) + final hops = message.hopCount ?? 0; final color = Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[600]; diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index b328503..274e004 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -43,9 +43,11 @@ class ChannelMessagePathScreen extends StatelessWidget { final hashWidth = connector.pathHashByteWidth; final hops = _buildPathHops(primaryPath, connector, l10n, hashWidth); final hasHopDetails = primaryPath.isNotEmpty; + // Observed = bytes we hold / this path's width. Claimed = the hop count + // the path-len byte already carries; dividing it again halved it. (#309) final observedLabel = _formatObservedHops( - primaryPath.length ~/ hashWidth, - realHopCount(message.hopCount, hashWidth), + primaryPath.length ~/ message.pathHashSize, + message.hopCount, l10n, ); final extraPaths = _otherPaths(primaryPath, message.pathVariants); @@ -121,7 +123,6 @@ class ChannelMessagePathScreen extends StatelessWidget { Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) { final l10n = context.l10n; - final hashWidth = context.read().pathHashByteWidth; return Card( child: Padding( padding: const EdgeInsets.all(12), @@ -145,7 +146,7 @@ class ChannelMessagePathScreen extends StatelessWidget { ), _buildDetailRow( l10n.channelPath_pathLabelTitle, - _formatPathLabel(realHopCount(message.hopCount, hashWidth), l10n), + _formatPathLabel(message.hopCount, l10n), ), if (observedLabel != null) _buildDetailRow(l10n.channelPath_observedLabel, observedLabel), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index f7deb8b..eedfd71 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -97,251 +97,229 @@ class _ChannelsScreenState extends State return const SizedBox.shrink(); } - final allowBack = !connector.isConnected; - - return PopScope( - canPop: allowBack, - child: AppShell( - selectedIndex: 1, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), - contactsUnreadCount: connector.getTotalContactsUnreadCount(), - channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - drawerContent: ChannelDrawerList( - onChannelSelected: (channel) => _openChannel(context, channel), - ), - appBar: AppBar( - title: AppBarTitle(context.l10n.channels_title), - centerTitle: true, - bottom: const SyncProgressAppBarBottom(), - actions: [ + return AppShell( + selectedIndex: 1, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: ChannelDrawerList( + onChannelSelected: (channel) => _openChannel(context, channel), + ), + onDisconnect: () => _disconnect(context), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + appBar: AppBar( + title: AppBarTitle(context.l10n.channels_title), + centerTitle: true, + bottom: const SyncProgressAppBarBottom(), + actions: [ + // Disconnect and Settings moved to the panel footer (#290); only + // the screen-level Communities entry remains, and it already only + // appears once a community has been joined. + if (_communities.isNotEmpty) PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( child: Row( children: [ - const Icon(Icons.logout, color: Colors.red), + const Icon(Icons.groups), const SizedBox(width: 8), - Text(context.l10n.common_disconnect), + Text(context.l10n.community_manageCommunities), ], ), - onTap: () => _disconnect(context), - ), - if (_communities.isNotEmpty) - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.groups), - const SizedBox(width: 8), - Text(context.l10n.community_manageCommunities), - ], - ), - onTap: () => _showManageCommunitiesDialog(context), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 8), - Text(context.l10n.settings_title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), - ), + onTap: () => _showManageCommunitiesDialog(context), ), ], icon: const Icon(Icons.more_vert), ), - ], - ), - body: RefreshIndicator( - onRefresh: () async { - await context.read().getChannels(force: true); - }, - child: () { - final channels = connector.channels; - final waitingForFirstChannel = - connector.isLoadingChannels && channels.isEmpty; - - // Only block the list while the first channel is actively loading. - // If the initial sync aborts, show cached/partial channels instead - // of trapping the user behind an idle spinner. - if (waitingForFirstChannel) { - return const Center(child: CircularProgressIndicator()); - } + ], + ), + body: RefreshIndicator( + onRefresh: () async { + await context.read().getChannels(force: true); + }, + child: () { + final channels = connector.channels; + final waitingForFirstChannel = + connector.isLoadingChannels && channels.isEmpty; + + // Only block the list while the first channel is actively loading. + // If the initial sync aborts, show cached/partial channels instead + // of trapping the user behind an idle spinner. + if (waitingForFirstChannel) { + return const Center(child: CircularProgressIndicator()); + } - if (channels.isEmpty) { - return ListView( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height - 200, - child: EmptyState( - icon: Icons.tag, - title: context.l10n.channels_noChannelsConfigured, - action: FilledButton.icon( - onPressed: () => _addPublicChannel(context, connector), - icon: const Icon(Icons.public), - label: Text(context.l10n.channels_addPublicChannel), - ), + if (channels.isEmpty) { + return ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height - 200, + child: EmptyState( + icon: Icons.tag, + title: context.l10n.channels_noChannelsConfigured, + action: FilledButton.icon( + onPressed: () => _addPublicChannel(context, connector), + icon: const Icon(Icons.public), + label: Text(context.l10n.channels_addPublicChannel), ), ), - ], - ); - } - - final filteredChannels = _filterAndSortChannels( - channels, - connector, - viewState, + ), + ], ); + } - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: context.l10n.channels_searchChannels, - prefixIcon: const Icon(Icons.search), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (viewState.channelsSearchText.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchDebounce?.cancel(); - _searchDebounce = null; - _searchController.clear(); - context - .read() - .setChannelsSearchText(''); - }, - ), - _buildFilterButton(viewState), - ], - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), + final filteredChannels = _filterAndSortChannels( + channels, + connector, + viewState, + ); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: context.l10n.channels_searchChannels, + prefixIcon: const Icon(Icons.search), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (viewState.channelsSearchText.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchDebounce?.cancel(); + _searchDebounce = null; + _searchController.clear(); + context + .read() + .setChannelsSearchText(''); + }, + ), + _buildFilterButton(viewState), + ], + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), - onChanged: (value) { - _searchDebounce?.cancel(); - _searchDebounce = Timer( - const Duration(milliseconds: 300), - () { - if (!mounted) return; - context - .read() - .setChannelsSearchText(value); - }, - ); - }, ), + onChanged: (value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer( + const Duration(milliseconds: 300), + () { + if (!mounted) return; + context + .read() + .setChannelsSearchText(value); + }, + ); + }, ), - Expanded( - child: filteredChannels.isEmpty - ? ListView( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height - 300, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.search_off, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - context.l10n.channels_noChannelsFound, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + ), + Expanded( + child: filteredChannels.isEmpty + ? ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height - 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + context.l10n.channels_noChannelsFound, + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], ), - ], - ), + ), + ], ), ), - ], - ) - : (viewState.channelsSortOption == - ChannelSortOption.manual && - viewState.channelsSearchText.isEmpty) - ? ReorderableListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, ), - buildDefaultDragHandles: false, - itemCount: filteredChannels.length, - onReorderItem: (oldIndex, newIndex) { - // onReorderItem already adjusts newIndex after the - // removed item, unlike the deprecated onReorder. - final reordered = List.from( - filteredChannels, - ); - final item = reordered.removeAt(oldIndex); - reordered.insert(newIndex, item); - unawaited( - connector.setChannelOrder( - reordered.map((c) => c.index).toList(), - ), - ); - }, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile( - context, - connector, - channelMessageStore, - channel, - showDragHandle: true, - dragIndex: index, - ); - }, - ) - : ListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, - ), - itemCount: filteredChannels.length, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile( - context, - connector, - channelMessageStore, - channel, - ); - }, + ], + ) + : (viewState.channelsSortOption == + ChannelSortOption.manual && + viewState.channelsSearchText.isEmpty) + ? ReorderableListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, ), - ), - ], - ); - }(), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showAddChannelDialog(context), - tooltip: context.l10n.channels_addChannel, - child: const Icon(Icons.add), - ), + buildDefaultDragHandles: false, + itemCount: filteredChannels.length, + onReorderItem: (oldIndex, newIndex) { + // onReorderItem already adjusts newIndex after the + // removed item, unlike the deprecated onReorder. + final reordered = List.from( + filteredChannels, + ); + final item = reordered.removeAt(oldIndex); + reordered.insert(newIndex, item); + unawaited( + connector.setChannelOrder( + reordered.map((c) => c.index).toList(), + ), + ); + }, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channelMessageStore, + channel, + showDragHandle: true, + dragIndex: index, + ); + }, + ) + : ListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, + ), + itemCount: filteredChannels.length, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channelMessageStore, + channel, + ); + }, + ), + ), + ], + ); + }(), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showAddChannelDialog(context), + tooltip: context.l10n.channels_addChannel, + child: const Icon(Icons.add), ), ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index d8c743a..0e73350 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -32,6 +32,7 @@ import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; import '../services/translation_service.dart'; import '../widgets/chat_zoom_wrapper.dart'; +import '../widgets/contact_settings_dialog.dart'; import '../widgets/elements_ui.dart'; import '../widgets/mention_autocomplete.dart'; import '../helpers/emoji_shortcodes.dart'; @@ -621,8 +622,8 @@ class _ChatScreenState extends State { child: ValueListenableBuilder( valueListenable: _textController, builder: (context, value, child) { - final gifId = GifHelper.parseGif(value.text); - if (gifId != null) { + final gifUrl = GifHelper.resolveGifUrl(value.text); + if (gifUrl != null) { return Focus( autofocus: true, onKeyEvent: (node, event) { @@ -641,8 +642,7 @@ class _ChatScreenState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', + url: gifUrl, backgroundColor: colorScheme.surfaceContainerHighest, fallbackTextColor: colorScheme.onSurface @@ -1274,15 +1274,11 @@ class _ChatScreenState extends State { return context.l10n.chat_hopsForced(contact.pathOverride!); } - // Use device's path. pathLength is a BYTE length; divide by the device's - // configured hash width to get the true hop count (#222). + // Use device's path. pathLength is a HOP count straight from the path-len + // byte's low 6 bits; it used to be divided by the device width, which + // halved it at 2-byte width. (#309) if (contact.pathLength < 0) return context.l10n.chat_floodAuto; - final hops = - realHopCount( - contact.pathLength, - context.read().pathHashByteWidth, - ) ?? - 0; + final hops = contact.pathLength; if (hops == 0) return context.l10n.chat_direct; return context.l10n.chat_hopsCount(hops); } @@ -1354,166 +1350,7 @@ class _ChatScreenState extends State { } void _showContactSettings(BuildContext context) { - final connector = Provider.of(context, listen: false); - final appSettingsService = Provider.of( - context, - listen: false, - ); - connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex); - connector.ensureContactCyr2LatSettingLoaded(widget.contact.publicKeyHex); - final contact = widget.contact; - bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); - bool cyr2latEnabled = connector.isContactCyr2LatEnabled( - contact.publicKeyHex, - ); - String? selectedCyr2LatProfileId = connector.getContactCyr2LatProfileId( - contact.publicKeyHex, - ); - bool teleBaseEnabled = contact.teleBaseEnabled; - bool teleLocEnabled = contact.teleLocEnabled; - bool teleEnvEnabled = contact.teleEnvEnabled; - showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => AlertDialog( - title: Text(context.l10n.contact_settings), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (contact.hasLocation) ...[ - _buildInfoRow( - context.l10n.chat_location, - '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', - ), - const Divider(height: 8), - ], - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.channels_smazCompression), - subtitle: Text(context.l10n.chat_compressOutgoingMessages), - value: smazEnabled, - onChanged: (value) { - connector.setContactSmazEnabled( - contact.publicKeyHex, - value, - ); - connector.setContactCyr2LatEnabled( - contact.publicKeyHex, - false, - ); - setDialogState(() { - smazEnabled = value; - if (smazEnabled) { - cyr2latEnabled = false; - } - }); - }, - ), - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.channels_cyr2latCompression), - subtitle: Text(context.l10n.channels_cyr2latCompressionDscr), - value: cyr2latEnabled, - onChanged: (value) { - connector.setContactCyr2LatEnabled( - contact.publicKeyHex, - value, - ); - connector.setContactSmazEnabled( - contact.publicKeyHex, - false, - ); - setDialogState(() { - cyr2latEnabled = value; - if (cyr2latEnabled) { - smazEnabled = false; - } - }); - }, - ), - if (cyr2latEnabled) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), - child: DropdownButtonFormField( - initialValue: selectedCyr2LatProfileId, - decoration: InputDecoration( - labelText: - context.l10n.channels_cyr2latSettingsSubheading, - border: const OutlineInputBorder(), - ), - items: appSettingsService.settings.cyr2latProfiles.map(( - profile, - ) { - return DropdownMenuItem( - value: profile.id, - child: Text(profile.name), - ); - }).toList(), - onChanged: (value) { - connector.setContactCyr2LatProfileId( - contact.publicKeyHex, - value, - ); - setDialogState(() { - selectedCyr2LatProfileId = value; - }); - }, - ), - ), - ], - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.contact_teleBase), - subtitle: Text(context.l10n.contact_teleBaseSubtitle), - value: teleBaseEnabled, - onChanged: (value) { - setDialogState(() => teleBaseEnabled = value); - }, - ), - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.contact_teleLoc), - subtitle: Text(context.l10n.contact_teleLocSubtitle), - value: teleLocEnabled, - onChanged: (value) { - setDialogState(() => teleLocEnabled = value); - }, - ), - const Divider(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.contact_teleEnv), - subtitle: Text(context.l10n.contact_teleEnvSubtitle), - value: teleEnvEnabled, - onChanged: (value) { - setDialogState(() => teleEnvEnabled = value); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - connector.setContactFlags( - contact, - teleBase: teleBaseEnabled, - teleLoc: teleLocEnabled, - teleEnv: teleEnvEnabled, - ); - Navigator.pop(context); - }, - child: Text(context.l10n.common_close), - ), - ], - ), - ), - ); + showContactSettingsDialog(context, widget.contact); } Widget _buildInfoRow(String label, String value) { @@ -1870,7 +1707,7 @@ class _MessageBubble extends StatelessWidget { final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; - final gifId = GifHelper.parseGif(message.text); + final gifUrl = GifHelper.resolveGifUrl(message.text); final poi = parseMarkerText(message.text); final isFailed = message.status == MessageStatus.failed; final bubbleColor = isFailed @@ -1919,7 +1756,7 @@ class _MessageBubble extends StatelessWidget { ], Flexible( child: Container( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.all(4) : const EdgeInsets.symmetric( horizontal: 12, @@ -1937,7 +1774,7 @@ class _MessageBubble extends StatelessWidget { children: [ if (!isOutgoing) ...[ Padding( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.only( left: 8, top: 4, @@ -1953,7 +1790,7 @@ class _MessageBubble extends StatelessWidget { ), ), ), - if (gifId == null) const SizedBox(height: 4), + if (gifUrl == null) const SizedBox(height: 4), ], if (poi != null) _buildPoiMessage( @@ -1978,14 +1815,13 @@ class _MessageBubble extends StatelessWidget { ) : null, ) - else if (gifId != null) + else if (gifUrl != null) Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', + url: gifUrl, backgroundColor: Colors.transparent, fallbackTextColor: textColor.withValues( alpha: 0.7, @@ -2057,7 +1893,7 @@ class _MessageBubble extends StatelessWidget { if (isOutgoing && message.retryCount > 0) ...[ const SizedBox(height: 4), Padding( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.symmetric(horizontal: 8) : EdgeInsets.zero, child: Text( @@ -2078,7 +1914,7 @@ class _MessageBubble extends StatelessWidget { ], const SizedBox(height: 4), Padding( - padding: gifId != null + padding: gifUrl != null ? const EdgeInsets.only( left: 8, right: 8, diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 87fc7e4..9ffbae6 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -29,6 +29,7 @@ import '../widgets/empty_state.dart'; import '../widgets/blocked_badge.dart'; import '../widgets/app_shell.dart'; import '../widgets/contact_filter_rail.dart'; +import '../widgets/contact_settings_dialog.dart'; import '../widgets/path_selection_dialog.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; @@ -317,125 +318,103 @@ class _ContactsScreenState extends State return const SizedBox.shrink(); } - final allowBack = !connector.isConnected; - return PopScope( - canPop: allowBack, - child: AppShell( - selectedIndex: 0, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), - contactsUnreadCount: connector.getTotalContactsUnreadCount(), - channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - drawerContent: const ContactFilterRail(), - appBar: AppBar( - title: AppBarTitle(context.l10n.contacts_title), - bottom: const SyncProgressAppBarBottom(), - actions: [ - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.connect_without_contact), - const SizedBox(width: 8), - Text(context.l10n.contacts_zeroHopAdvert), - ], - ), - onTap: () => { - connector.sendSelfAdvert(flood: false), - showDismissibleSnackBar( - context, - content: Text(context.l10n.settings_advertisementSent), - ), - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.cell_tower), - const SizedBox(width: 8), - Text(context.l10n.contacts_floodAdvert), - ], - ), - onTap: () => { - connector.sendSelfAdvert(flood: true), - showDismissibleSnackBar( - context, - content: Text(context.l10n.settings_advertisementSent), - ), - }, + return AppShell( + selectedIndex: 0, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: const ContactFilterRail(), + onDisconnect: () => _disconnect(context, connector), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + appBar: AppBar( + title: AppBarTitle(context.l10n.contacts_title), + bottom: const SyncProgressAppBarBottom(), + actions: [ + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.connect_without_contact), + const SizedBox(width: 8), + Text(context.l10n.contacts_zeroHopAdvert), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.copy), - const SizedBox(width: 8), - Text(context.l10n.contacts_copyAdvertToClipboard), - ], + onTap: () => { + connector.sendSelfAdvert(flood: false), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), - onTap: () => _contactExport(Uint8List.fromList([])), + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.cell_tower), + const SizedBox(width: 8), + Text(context.l10n.contacts_floodAdvert), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.paste), - const SizedBox(width: 8), - Text(context.l10n.contacts_addContactFromClipboard), - ], + onTap: () => { + connector.sendSelfAdvert(flood: true), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), - onTap: () => _contactImport(), + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: 8), + Text(context.l10n.contacts_copyAdvertToClipboard), + ], ), - ], - icon: const Icon(Icons.connect_without_contact), - ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - Text(context.l10n.common_disconnect), - ], - ), - onTap: () => _disconnect(context, connector), + onTap: () => _contactExport(Uint8List.fromList([])), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text(context.l10n.contacts_addContactFromClipboard), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.person_add_rounded), - const SizedBox(width: 8), - Text(context.l10n.discoveredContacts_Title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DiscoveryScreen(), - ), - ), + onTap: () => _contactImport(), + ), + ], + icon: const Icon(Icons.connect_without_contact), + ), + // Disconnect and Settings moved to the panel footer (#290). + // Discovered contacts is screen-level and stays here. + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.person_add_rounded), + const SizedBox(width: 8), + Text(context.l10n.discoveredContacts_Title), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 8), - Text(context.l10n.settings_title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), ), ), - ], - icon: const Icon(Icons.more_vert), - ), - ], - ), - body: _buildContactsBody(context, connector), + ), + ], + icon: const Icon(Icons.more_vert), + ), + ], ), + body: _buildContactsBody(context, connector), ); } @@ -1274,6 +1253,16 @@ class _ContactsScreenState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ + // Same destination as the chat ellipsis -> Contact settings (#351). + // First item, so the destructive Block tile stays separated below. + ListTile( + leading: const Icon(Icons.settings_outlined), + title: Text(context.l10n.contact_settings), + onTap: () { + Navigator.pop(sheetContext); + showContactSettingsDialog(context, contact); + }, + ), // Blocking your own node is never meaningful (#250). if (!isSelf) ListTile( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 1444f5a..2b9ade1 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -24,6 +24,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_shell.dart'; +import '../widgets/map_layer_panel.dart'; import '../widgets/sync_progress_overlay.dart'; import '../icons/los_icon.dart'; import 'channels_screen.dart'; @@ -398,7 +399,8 @@ class _MapScreenState extends State { // Re center map after removed markers have loaded if (!_hasInitializedMap && _removedMarkersLoaded) { _hasInitializedMap = true; - _showNodeLabels = initialZoom >= _labelZoomThreshold; + _showNodeLabels = + settings.mapAlwaysShowNames || initialZoom >= _labelZoomThreshold; if (hasMapContent) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -408,281 +410,253 @@ class _MapScreenState extends State { } } - final allowBack = !connector.isConnected; - - return PopScope( - canPop: allowBack, - child: AppShell( - selectedIndex: 2, - onDestinationSelected: (index) => - _handleQuickSwitch(index, context), - contactsUnreadCount: connector.getTotalContactsUnreadCount(), - channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - appBar: AppBar( - title: AppBarTitle(context.l10n.map_title), - centerTitle: true, - bottom: const SyncProgressAppBarBottom(), - actions: [ - if (!_isBuildingPathTrace) - IconButton( - icon: const Icon(Icons.radar), - onPressed: () => _startPath( - LatLng(connector.selfLatitude!, connector.selfLongitude!), - ), - tooltip: context.l10n.contacts_pathTrace, + return AppShell( + selectedIndex: 2, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: const MapLayerPanel(), + onDisconnect: () => _disconnect(context, connector), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + appBar: AppBar( + title: AppBarTitle(context.l10n.map_title), + centerTitle: true, + bottom: const SyncProgressAppBarBottom(), + actions: [ + if (!_isBuildingPathTrace) + IconButton( + icon: const Icon(Icons.radar), + onPressed: () => _startPath( + LatLng(connector.selfLatitude!, connector.selfLongitude!), ), - if (!_isBuildingPathTrace) - IconButton( - icon: const LosIcon(), - onPressed: () { - final candidates = []; - if (connector.selfLatitude != null && - connector.selfLongitude != null) { - candidates.add( - LineOfSightEndpoint( - label: context.l10n.pathTrace_you, - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - color: Colors.teal, - icon: Icons.person_pin_circle, - ), - ); - } - for (final c in contactsWithLocation) { - candidates.add( - LineOfSightEndpoint( - label: c.name, - point: LatLng(c.latitude!, c.longitude!), - color: _getNodeColor(c.type), - icon: _getNodeIcon(c.type), - ), - ); - } - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LineOfSightMapScreen( - title: context.l10n.map_losScreenTitle, - candidates: candidates, + tooltip: context.l10n.contacts_pathTrace, + ), + if (!_isBuildingPathTrace) + IconButton( + icon: const LosIcon(), + onPressed: () { + final candidates = []; + if (connector.selfLatitude != null && + connector.selfLongitude != null) { + candidates.add( + LineOfSightEndpoint( + label: context.l10n.pathTrace_you, + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, ), + color: Colors.teal, + icon: Icons.person_pin_circle, ), ); - }, - tooltip: context.l10n.map_lineOfSight, - ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - Text(context.l10n.common_disconnect), - ], - ), - onTap: () => _disconnect(context, connector), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 8), - Text(context.l10n.settings_title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), + } + for (final c in contactsWithLocation) { + candidates.add( + LineOfSightEndpoint( + label: c.name, + point: LatLng(c.latitude!, c.longitude!), + color: _getNodeColor(c.type), + icon: _getNodeIcon(c.type), + ), + ); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LineOfSightMapScreen( + title: context.l10n.map_losScreenTitle, + candidates: candidates, ), ), - ), - ], - icon: const Icon(Icons.more_vert), + ); + }, + tooltip: context.l10n.map_lineOfSight, ), - ], - ), - body: Stack( - children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: center, - initialZoom: initialZoom, - minZoom: _mapMinZoom, - maxZoom: _mapMaxZoom, - interactionOptions: InteractionOptions( - flags: ~InteractiveFlag.rotate, - scrollWheelVelocity: isDesktop ? 0.012 : 0.005, - cursorKeyboardRotationOptions: - CursorKeyboardRotationOptions.disabled(), - keyboardOptions: isDesktop - ? const KeyboardOptions( - enableArrowKeysPanning: true, - enableWASDPanning: true, - enableRFZooming: true, - ) - : const KeyboardOptions.disabled(), - ), - onTap: (_, latLng) { - if (_isSelectingPoi) { - setState(() { - _isSelectingPoi = false; - }); - _shareMarker( - context: context, - connector: connector, - position: latLng, - defaultLabel: context.l10n.map_pointOfInterest, - flags: 'poi', - ); - } - }, - onLongPress: (_, latLng) { - if (_isSelectingPoi) { - setState(() { - _isSelectingPoi = false; - }); - _shareMarker( - context: context, - connector: connector, - position: latLng, - defaultLabel: context.l10n.map_pointOfInterest, - flags: 'poi', - ); - return; - } - _showShareMarkerAtPositionSheet( + ], + ), + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: center, + initialZoom: initialZoom, + minZoom: _mapMinZoom, + maxZoom: _mapMaxZoom, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + scrollWheelVelocity: isDesktop ? 0.012 : 0.005, + cursorKeyboardRotationOptions: + CursorKeyboardRotationOptions.disabled(), + keyboardOptions: isDesktop + ? const KeyboardOptions( + enableArrowKeysPanning: true, + enableWASDPanning: true, + enableRFZooming: true, + ) + : const KeyboardOptions.disabled(), + ), + onTap: (_, latLng) { + if (_isSelectingPoi) { + setState(() { + _isSelectingPoi = false; + }); + _shareMarker( context: context, connector: connector, position: latLng, + defaultLabel: context.l10n.map_pointOfInterest, + flags: 'poi', ); - }, - onPositionChanged: (camera, hasGesture) { - final shouldShow = camera.zoom >= _labelZoomThreshold; - if (shouldShow != _showNodeLabels && mounted) { - setState(() { - _showNodeLabels = shouldShow; - }); - } - }, + } + }, + onLongPress: (_, latLng) { + if (_isSelectingPoi) { + setState(() { + _isSelectingPoi = false; + }); + _shareMarker( + context: context, + connector: connector, + position: latLng, + defaultLabel: context.l10n.map_pointOfInterest, + flags: 'poi', + ); + return; + } + _showShareMarkerAtPositionSheet( + context: context, + connector: connector, + position: latLng, + ); + }, + onPositionChanged: (camera, hasGesture) { + final shouldShow = + settings.mapAlwaysShowNames || + camera.zoom >= _labelZoomThreshold; + if (shouldShow != _showNodeLabels && mounted) { + setState(() { + _showNodeLabels = shouldShow; + }); + } + }, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, + maxZoom: 19, ), - children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), - if (_polylines.isNotEmpty && _isBuildingPathTrace) - PolylineLayer(polylines: _polylines), - if (sharedMarkerPolylines.isNotEmpty) - PolylineLayer(polylines: sharedMarkerPolylines), - MarkerLayer( - markers: [ - if (highlightPosition != null) - Marker( - point: highlightPosition, - width: 40, - height: 40, - child: IgnorePointer( - child: Icon( - Icons.location_on_outlined, - color: Colors.red[600], - size: 34, - ), + if (_polylines.isNotEmpty && _isBuildingPathTrace) + PolylineLayer(polylines: _polylines), + if (sharedMarkerPolylines.isNotEmpty) + PolylineLayer(polylines: sharedMarkerPolylines), + MarkerLayer( + markers: [ + if (highlightPosition != null) + Marker( + point: highlightPosition, + width: 40, + height: 40, + child: IgnorePointer( + child: Icon( + Icons.location_on_outlined, + color: Colors.red[600], + size: 34, ), ), - if (!settings.mapShowOverlaps) - ..._buildGuessedMarker( - guessedLocations, - showLabels: _showNodeLabels, - ), - ..._buildMarkers( - contactsWithLocation, - settings, - showLabels: _showNodeLabels, ), - ...sharedMarkers.map(_buildSharedMarker), - if (connector.selfLatitude != null && - connector.selfLongitude != null) - Marker( - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - width: 40, - height: 40, - child: IgnorePointer( - ignoring: true, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.teal, - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.3, - ), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: const Icon( - Icons.person_pin_circle, + if (!settings.mapShowOverlaps) + ..._buildGuessedMarker( + guessedLocations, + showLabels: + settings.mapAlwaysShowNames || _showNodeLabels, + ), + ..._buildMarkers( + contactsWithLocation, + settings, + showLabels: + settings.mapAlwaysShowNames || _showNodeLabels, + ), + ...sharedMarkers.map(_buildSharedMarker), + if (connector.selfLatitude != null && + connector.selfLongitude != null) + Marker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + width: 40, + height: 40, + child: IgnorePointer( + ignoring: true, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all( color: Colors.white, - size: 20, + width: 2, ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: const Icon( + Icons.person_pin_circle, + color: Colors.white, + size: 20, ), ), ), - if (_showNodeLabels && - connector.selfLatitude != null && - connector.selfLongitude != null) - _buildNodeLabelMarker( - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - label: context.l10n.pathTrace_you, + ), + if (_showNodeLabels && + connector.selfLatitude != null && + connector.selfLongitude != null) + _buildNodeLabelMarker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, ), - ], - ), - ], - ), - if (!_isBuildingPathTrace) - _buildLegend( - contacts, - contactsWithLocation, - settings, - sharedMarkers.length, - guessedLocations.length, - ), - if (isDesktop) - _buildDesktopMapControls( - context, - center: center, - zoom: initialZoom, - hasPathSelector: _isBuildingPathTrace, + label: context.l10n.pathTrace_you, + ), + ], ), - if (_isBuildingPathTrace) _buildPathTraceOverlay(), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showFilterDialog(context, settingsService), - tooltip: context.l10n.map_filterNodes, - child: const Icon(Icons.filter_list), - ), + ], + ), + if (!_isBuildingPathTrace) + _buildLegend( + contacts, + contactsWithLocation, + settings, + sharedMarkers.length, + guessedLocations.length, + ), + if (isDesktop) + _buildDesktopMapControls( + context, + center: center, + zoom: initialZoom, + hasPathSelector: _isBuildingPathTrace, + ), + if (_isBuildingPathTrace) _buildPathTraceOverlay(), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showFilterDialog(context, settingsService), + tooltip: context.l10n.map_filterNodes, + child: const Icon(Icons.filter_list), ), ); }, diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 3222168..e2f635a 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -25,6 +25,11 @@ class ScannerScreen extends StatefulWidget { class _ScannerScreenState extends State { bool _changedNavigation = false; + + /// Re-entrancy guard for routing into the app. Distinct from + /// [_changedNavigation], which records that we entered at least once and + /// gates the disconnect-on-dispose cleanup. + bool _routingIntoApp = false; late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; @@ -40,14 +45,28 @@ class _ScannerScreenState extends State { if (_connector.state == MeshCoreConnectionState.disconnected) { _changedNavigation = false; } else if (_connector.state == MeshCoreConnectionState.connected && - _connector.activeTransport == MeshCoreTransportType.bluetooth && isCurrentRoute && - !_changedNavigation) { + !_routingIntoApp) { + // Route in whenever this screen is showing while connected, not just + // on the first connect. Otherwise landing back here with a live + // connection is a dead end: Connect does nothing, because there is + // nothing left to connect. + // + // Deliberately transport-agnostic. This was bluetooth-only, which left + // the same dead end for USB and TCP users, who cannot connect their way + // out of it either. The first-connect path for those transports is not + // affected: their own screen sits on top while connecting, so + // isCurrentRoute is false here and they still navigate themselves. _changedNavigation = true; + _routingIntoApp = true; if (mounted) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const ChannelsScreen()), - ); + Navigator.of(context) + .push( + MaterialPageRoute(builder: (context) => const ChannelsScreen()), + ) + .then((_) { + if (mounted) _routingIntoApp = false; + }); } } }; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 819687f..e7a030f 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -403,6 +403,17 @@ class _SettingsScreenState extends State { _buildInfoRow(l10n.settings_infoFirmware, firmwareVersion), if (deviceModel != null) _buildInfoRow(l10n.settings_infoModel, deviceModel), + // Capability-gated controls vanish silently when a bit is clear, + // which is indistinguishable from a bug. Surface the raw inputs so + // "missing feature" can be diagnosed without enabling logging. (#304) + if (connector.offbandCaps != null) + _buildInfoRow( + l10n.settings_infoOffbandCaps, + '0x${connector.offbandCaps!.toRadixString(16).padLeft(2, '0')}' + ' (v${connector.firmwareVerCode ?? 0})' + '${connector.supportsOffbandFemLna ? ' · FEM LNA' : ''}' + '${connector.supportsOffbandBlock ? ' · block' : ''}', + ), _buildBatteryInfoRow(context, connector), if (connector.selfName != null) _buildInfoRow(l10n.settings_nodeName, connector.selfName!), @@ -2192,6 +2203,23 @@ class _RadioSettingsFormState extends State<_RadioSettingsForm> { contentPadding: EdgeInsets.zero, ), ], + // Only this radio's own FEM probe decides whether this appears — never + // model or version (#304). Deliberately not mirrored into local state: + // firmware returns post-apply hardware truth, so the switch renders + // what the radio reports rather than what we asked for. + if (widget.connector.supportsOffbandFemLna) ...[ + const SizedBox(height: 16), + ListenableBuilder( + listenable: widget.connector, + builder: (context, _) => SwitchListTile( + title: Text(l10n.settings_femLna), + subtitle: Text(l10n.settings_femLnaSubtitle), + value: widget.connector.femLnaEnabled ?? true, + onChanged: (value) => widget.connector.setFemLna(value), + contentPadding: EdgeInsets.zero, + ), + ), + ], const SizedBox(height: 16), SizedBox( width: double.infinity, diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 5e3d8c3..d0ea235 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -76,6 +76,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowOverlaps: value)); } + Future setMapAlwaysShowNames(bool value) async { + await updateSettings(_settings.copyWith(mapAlwaysShowNames: value)); + } + Future setMapTimeFilterHours(double value) async { await updateSettings(_settings.copyWith(mapTimeFilterHours: value)); } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 6e41745..f08d88a 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; +import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; import '../l10n/app_localizations.dart'; import '../utils/platform_info.dart'; @@ -154,7 +155,9 @@ class NotificationService { if (reaction != null) { return 'Reacted ${reaction.emoji}'; } - if (RegExp(r'^g:[A-Za-z0-9_-]+$').hasMatch(trimmed)) { + // resolveGifUrl, not parseGif: the tray must summarise exactly the set the + // chat renders inline, or an allowlisted Tenor GIF shows as a raw URL. (#283) + if (GifHelper.resolveGifUrl(trimmed) != null) { return 'Sent a GIF'; } return text; diff --git a/lib/services/window_geometry_service.dart b/lib/services/window_geometry_service.dart new file mode 100644 index 0000000..1d76c78 --- /dev/null +++ b/lib/services/window_geometry_service.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; + +import 'package:screen_retriever/screen_retriever.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../storage/prefs_manager.dart'; +import '../utils/app_logger.dart'; +import '../utils/platform_info.dart'; + +/// Persists and restores the desktop window's position and size (#349). +/// +/// The stock Flutter desktop runner never saved geometry, so the window always +/// reopened at a default frame. This service saves the frame on move/resize and +/// restores it on launch. +/// +/// The restore is clamped against the currently-connected displays: a window +/// last placed on a monitor that is now unplugged must not reopen off-screen +/// where the user cannot reach it. If the saved frame does not overlap any +/// display enough to grab, it is discarded and the window opens at the default. +/// +/// Desktop only (window_manager has no mobile/web backend); a no-op elsewhere. +class WindowGeometryService with WindowListener { + WindowGeometryService._(); + static final WindowGeometryService instance = WindowGeometryService._(); + + static const _prefsKey = 'window_geometry'; + static const _minWidth = 400.0; + static const _minHeight = 300.0; + // A window counts as reachable if at least this much of it overlaps a + // display on both axes, so a sliver still on screen is recoverable. + static const _minVisible = 80.0; + + Timer? _saveDebounce; + + /// Call once in main() before runApp, after PrefsManager.initialize(). + Future initialize() async { + if (!PlatformInfo.isDesktop) return; + + await windowManager.ensureInitialized(); + await windowManager.waitUntilReadyToShow(null, () async { + await _restore(); + await windowManager.show(); + await windowManager.focus(); + }); + windowManager.addListener(this); + // Intercept close so the final geometry is saved even if the user moves + // then closes inside the debounce window; onWindowClose does the save and + // then destroys the window (Gemini review). + await windowManager.setPreventClose(true); + } + + Future _restore() async { + final raw = PrefsManager.instance.getString(_prefsKey); + if (raw == null || raw.isEmpty) return; + + Rect saved; + try { + final m = jsonDecode(raw) as Map; + saved = Rect.fromLTWH( + (m['x'] as num).toDouble(), + (m['y'] as num).toDouble(), + (m['w'] as num).toDouble(), + (m['h'] as num).toDouble(), + ); + } catch (e) { + // SAFELANE 6: never swallow. A corrupt value falls through to the default. + appLogger.error( + 'Failed to decode saved window geometry; using default: $e', + tag: 'Window', + ); + return; + } + + if (saved.width < _minWidth || saved.height < _minHeight) return; + + if (!await _isReachable(saved)) { + appLogger.info( + 'Saved window geometry is off-screen (monitor likely unplugged); ' + 'opening at the default position instead.', + tag: 'Window', + ); + return; + } + + await windowManager.setBounds(saved); + } + + /// True when [saved] overlaps some connected display's visible area by at + /// least [_minVisible] on both axes. + Future _isReachable(Rect saved) async { + final displays = await screenRetriever.getAllDisplays(); + for (final d in displays) { + // Use the visible pair when BOTH are known, else the total pair. Mixing a + // visible origin with a total size makes a rect offset from the real + // area (Gemini review). visiblePosition can be negative for a monitor + // left of / above the primary, which intersect handles correctly. + final Rect display; + if (d.visiblePosition != null && d.visibleSize != null) { + display = d.visiblePosition! & d.visibleSize!; + } else { + display = Offset.zero & d.size; + } + final overlap = saved.intersect(display); + // intersect() can return negative width/height when the rects miss; only + // a genuine overlap on both axes counts. + if (overlap.width >= _minVisible && overlap.height >= _minVisible) { + return true; + } + } + return false; + } + + void _scheduleSave() { + _saveDebounce?.cancel(); + _saveDebounce = Timer(const Duration(milliseconds: 400), _save); + } + + Future _save() async { + try { + final b = await windowManager.getBounds(); + await PrefsManager.instance.setString( + _prefsKey, + jsonEncode({'x': b.left, 'y': b.top, 'w': b.width, 'h': b.height}), + ); + } catch (e) { + appLogger.error('Failed to save window geometry: $e', tag: 'Window'); + } + } + + @override + void onWindowMoved() => _scheduleSave(); + + @override + void onWindowResized() => _scheduleSave(); + + /// Fires because setPreventClose(true) intercepted the close. Save the final + /// geometry, release resources, then actually close the window. + @override + Future onWindowClose() async { + _saveDebounce?.cancel(); + try { + await _save(); + } finally { + windowManager.removeListener(this); + await windowManager.destroy(); + } + } +} diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index dfef8c3..b80016f 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -5,7 +5,7 @@ import 'package:meshcore_open/utils/app_logger.dart'; import '../models/channel_message.dart'; import '../models/translation_support.dart'; import '../helpers/smaz.dart'; -import 'prefs_manager.dart'; +import 'drift/blob_store.dart'; class ChannelMessageStore { static const String _keyPrefix = 'channel_messages_'; @@ -32,11 +32,25 @@ class ChannelMessageStore { String _indexKey(int channelIndex) => '$keyFor$channelIndex'; /// Active storage key: PSK identity when known, else the slot index. + /// + /// The index fallback is legitimate before a channel list has loaded, but it + /// reads a DIFFERENT key: if history was already migrated to the PSK key, the + /// caller gets an empty list that is indistinguishable from data loss. + /// SAFELANE 6 - this must never be silent. Falling back where a resolver + /// exists means the channel list was not ready, which is the #333 race. String _storageKey(int channelIndex) { final pskHex = channelPskResolver?.call(channelIndex); if (pskHex != null && pskHex.isNotEmpty) { return '$keyFor$_pskMarker$pskHex'; } + if (channelPskResolver != null) { + appLogger.warn( + 'Channel $channelIndex has no PSK yet; falling back to the slot-index ' + 'key. Any history already migrated to the PSK key will read as EMPTY. ' + 'This means the channel list was not loaded first (#333).', + tag: 'Storage', + ); + } return _indexKey(channelIndex); } @@ -51,9 +65,89 @@ class ChannelMessageStore { ); return; } - final prefs = PrefsManager.instance; - final jsonList = messages.map((msg) => _messageToJson(msg)).toList(); - await prefs.setString(_storageKey(channelIndex), jsonEncode(jsonList)); + // Merge into the persisted full history rather than overwriting it. The + // in-memory list is windowed to the most recent N for memory, so a plain + // overwrite would truncate the store to N and erode old history (#343). + // Upsert by identity: keep older persisted messages, add new ones, and let + // the in-memory copy win so edits/reactions/status updates are captured. + // Deletion has its own path (removeChannelMessage) so this never + // resurrects a message the user deleted. + final key = _storageKey(channelIndex); + final blobs = BlobStore.instance; + // Serialise the whole read-modify-write against other saves/deletes on this + // key so two concurrent saves cannot clobber each other (Gemini review). + await blobs.synchronized(key, () async { + final byKey = {}; + final existing = await blobs.readWithPrefsFallback(key); + if (existing != null && existing.isNotEmpty) { + try { + for (final e in jsonDecode(existing) as List) { + final m = _messageFromJson(e as Map); + byKey[_mergeKey(m)] = m; + } + } catch (e) { + // SAFELANE 6: never merge into an empty base and truncate silently. + appLogger.error( + 'Failed to decode existing channel $channelIndex history before ' + 'merge; aborting save to avoid truncation: $e', + tag: 'Storage', + ); + return; + } + } + for (final m in messages) { + byKey[_mergeKey(m)] = m; + } + final merged = byKey.values.toList() + ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + await blobs.write(key, jsonEncode(merged.map(_messageToJson).toList())); + }); + } + + /// Stable identity for merge/dedupe. messageId when present, else a composite + /// that distinguishes distinct messages that share no id. + String _mergeKey(ChannelMessage m) { + if (m.messageId.isNotEmpty) return 'id:${m.messageId}'; + // Include the sender: two different senders can post identical text at the + // same timestamp without a messageId, and would otherwise collide and lose + // one (Gemini review, 2026-07-20). + final sender = m.senderKey == null + ? '' + : m.senderKey!.map((b) => b.toRadixString(16)).join(); + return 'x:$sender:${m.packetHash ?? ''}:' + '${m.timestamp.millisecondsSinceEpoch}:${m.text}'; + } + + /// Removes a single message from the persisted history. The explicit delete + /// path (#343): save merges and never removes, so deletion cannot go through + /// save. + Future removeChannelMessage( + int channelIndex, + ChannelMessage message, + ) async { + if (publicKeyHex.isEmpty) return; + final key = _storageKey(channelIndex); + final blobs = BlobStore.instance; + await blobs.synchronized(key, () async { + final existing = await blobs.readWithPrefsFallback(key); + if (existing == null || existing.isEmpty) return; + final List raw; + try { + raw = jsonDecode(existing) as List; + } catch (e) { + appLogger.error( + 'Failed to decode channel $channelIndex history for delete: $e', + tag: 'Storage', + ); + return; + } + final target = _mergeKey(message); + final kept = raw + .map((e) => _messageFromJson(e as Map)) + .where((m) => _mergeKey(m) != target) + .toList(); + await blobs.write(key, jsonEncode(kept.map(_messageToJson).toList())); + }); } /// Load messages for a specific channel @@ -64,9 +158,11 @@ class ChannelMessageStore { ); return []; } - final prefs = PrefsManager.instance; + final blobs = BlobStore.instance; final key = _storageKey(channelIndex); - String? jsonString = prefs.getString(key); + // Bulk data lives in drift (#335); the fallback covers an unmigrated key + // and logs loudly if it fires. + String? jsonString = await blobs.readWithPrefsFallback(key); // One-time migration into the PSK-identity key. Only runs when the PSK is // known (key != index key). Adopts pre-#194 history keyed by slot index — @@ -79,14 +175,37 @@ class ChannelMessageStore { _indexKey(channelIndex), '$_keyPrefix$channelIndex', // pre-device-scoping, unscoped index key ]) { - final legacy = prefs.getString(legacyKey); + // Legacy keys may sit in either backend depending on when this + // install last ran, so check both. + final legacy = await blobs.readWithPrefsFallback(legacyKey); if (legacy != null && legacy.isNotEmpty) { appLogger.info( 'Migrating channel messages $legacyKey -> $key (PSK-keyed, #194)', ); - await prefs.setString(key, legacy); - await prefs.remove(legacyKey); - jsonString = legacy; + // Under the key lock, and MERGE rather than overwrite: a save may + // have landed on the PSK key between the read above and here, and a + // blind write would clobber it (Gemini review). Union keeps both the + // adopted legacy history and any freshly-saved message. + jsonString = await blobs.synchronized(key, () async { + final byKey = {}; + for (final srcJson in [await blobs.read(key), legacy]) { + if (srcJson == null || srcJson.isEmpty) continue; + try { + for (final e in jsonDecode(srcJson) as List) { + final m = _messageFromJson(e as Map); + byKey[_mergeKey(m)] = m; + } + } catch (_) { + // Skip an undecodable source rather than aborting the adoption. + } + } + final merged = byKey.values.toList() + ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + final encoded = jsonEncode(merged.map(_messageToJson).toList()); + await blobs.write(key, encoded); + return encoded; + }); + await blobs.deleteEverywhere(legacyKey); break; } } @@ -108,17 +227,16 @@ class ChannelMessageStore { /// history gone, and setChannel's reuse-clear (#193) needs any stale /// slot-index history gone so it can't be migrated onto the new occupant. Future clearChannelMessages(int channelIndex) async { - final prefs = PrefsManager.instance; - await prefs.remove(_storageKey(channelIndex)); - await prefs.remove(_indexKey(channelIndex)); + final blobs = BlobStore.instance; + await blobs.deleteEverywhere(_storageKey(channelIndex)); + await blobs.deleteEverywhere(_indexKey(channelIndex)); } /// Clear all channel messages Future clearAllChannelMessages() async { - final prefs = PrefsManager.instance; - final keys = prefs.getKeys().where((k) => k.startsWith(keyFor)).toList(); - for (var key in keys) { - await prefs.remove(key); + final blobs = BlobStore.instance; + for (final key in await blobs.keysWithPrefix(keyFor)) { + await blobs.deleteEverywhere(key); } } diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index 88d3f7a..4e268b2 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -29,13 +29,15 @@ class ChannelOrderStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index e0b390f..e6af56b 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -25,13 +25,18 @@ class ChannelSettingsStore { bool? enabled = prefs.getBool(key); if (enabled == null) { // Attempt migration from legacy unscoped key on first load + // Only touch storage when a legacy key actually exists. This ran + // unconditionally, and on Windows shared_preferences re-serialises and + // rewrites the WHOLE prefs file on every mutation, so removing a key + // that was never there still cost a full multi-MB write. Repeated per + // channel/contact on connect, that blocked the UI isolate for ~38s. enabled = prefs.getBool(oldKey); - prefs.remove(oldKey); if (enabled != null) { appLogger.info( 'Migrating channel settings from legacy key $oldKey to scoped key $key', ); await prefs.setBool(key, enabled); + await prefs.remove(oldKey); } } return enabled ?? false; diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index 4f40482..596d3bc 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -22,13 +22,15 @@ class ChannelStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } @@ -45,7 +47,11 @@ class ChannelStore { return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode channels: $e', tag: 'Storage'); return []; } } diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index c69d0b8..732ff47 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -28,13 +28,15 @@ class CommunityStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } @@ -105,7 +107,11 @@ class CommunityStore { final communities = await loadCommunities(); try { return communities.firstWhere((c) => c.id == communityId); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode communities: $e', tag: 'Storage'); return null; } } @@ -116,7 +122,11 @@ class CommunityStore { final communities = await loadCommunities(); try { return communities.firstWhere((c) => c.communityId == cid); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode communities: $e', tag: 'Storage'); return null; } } diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index 3f6f171..7622fdb 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -2,14 +2,16 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/contact.dart'; -import 'prefs_manager.dart'; +import '../utils/app_logger.dart'; +import 'drift/blob_store.dart'; class ContactDiscoveryStore { static const String _keyPrefix = 'discovered_contacts'; Future> loadContacts() async { - final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_keyPrefix); + // Bulk data lives in drift (#335), not SharedPreferences. The fallback + // covers a key the migration has not moved yet and logs loudly if it fires. + final jsonStr = await BlobStore.instance.readWithPrefsFallback(_keyPrefix); if (jsonStr == null) return []; try { @@ -17,15 +19,19 @@ class ContactDiscoveryStore { return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); - } catch (_) { + } catch (e) { + // SAFELANE 6: a decode failure is not "no data". + appLogger.error( + 'Failed to decode discovered contacts: $e', + tag: 'Storage', + ); return []; } } Future saveContacts(List contacts) async { - final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); - await prefs.setString(_keyPrefix, jsonEncode(jsonList)); + await BlobStore.instance.write(_keyPrefix, jsonEncode(jsonList)); } Map _toJson(Contact contact) { diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index ce6a0c6..0813274 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -21,13 +21,15 @@ class ContactGroupStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/contact_settings_store.dart b/lib/storage/contact_settings_store.dart index 682f1af..109f5eb 100644 --- a/lib/storage/contact_settings_store.dart +++ b/lib/storage/contact_settings_store.dart @@ -25,13 +25,18 @@ class ContactSettingsStore { bool? enabled = prefs.getBool(key); if (enabled == null) { // Attempt migration from legacy unscoped key on first load + // Only touch storage when a legacy key actually exists. This ran + // unconditionally, and on Windows shared_preferences re-serialises and + // rewrites the WHOLE prefs file on every mutation, so removing a key + // that was never there still cost a full multi-MB write. Repeated per + // channel/contact on connect, that blocked the UI isolate for ~38s. enabled = prefs.getBool(oldKey); - prefs.remove(oldKey); if (enabled != null) { appLogger.info( 'Migrating contact settings from legacy key $oldKey to scoped key $key', ); await prefs.setBool(key, enabled); + await prefs.remove(oldKey); } } return prefs.getBool(key) ?? false; diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 7883d88..d907752 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import '../models/contact.dart'; import '../utils/app_logger.dart'; +import 'drift/blob_store.dart'; import 'prefs_manager.dart'; class ContactStore { @@ -19,23 +20,27 @@ class ContactStore { appLogger.warn('Public key hex is not set. Cannot load contacts.'); return []; } - final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(keyFor); + // Bulk data lives in drift (#335). The fallback covers a key the migration + // has not moved yet, and logs loudly if it fires. + final blobs = BlobStore.instance; + String? jsonString = await blobs.readWithPrefsFallback(keyFor); + if (jsonString == null || jsonString.isEmpty) { - // Attempt migration from legacy unscoped key on first load - final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); - if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + // Pre-device-scoping data still sits under the legacy unscoped key in + // SharedPreferences. Only touch prefs when it actually exists: an + // unconditional remove costs a full-file rewrite on Windows and a + // 5 MiB-capped synchronous write on web (#306). + final prefs = PrefsManager.instance; + final legacy = prefs.get(_keyPrefix); + if (legacy is String && legacy.isNotEmpty) { appLogger.info( - 'Migrating contacts from legacy key $_keyPrefix to scoped key $keyFor', + 'Migrating contacts from legacy key $_keyPrefix to $keyFor (drift)', ); - await prefs.setString(keyFor, legacyJsonString); - jsonString = legacyJsonString; + await blobs.write(keyFor, legacy); + await prefs.remove(_keyPrefix); + jsonString = legacy; } } - if (jsonString == null || jsonString.isEmpty) { - jsonString = prefs.getString(keyFor); - } if (jsonString == null || jsonString.isEmpty) { return []; } @@ -55,9 +60,8 @@ class ContactStore { appLogger.warn('Public key hex is not set. Cannot save contacts.'); return; } - final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); - await prefs.setString(keyFor, jsonEncode(jsonList)); + await BlobStore.instance.write(keyFor, jsonEncode(jsonList)); } Map _toJson(Contact contact) { @@ -67,6 +71,7 @@ class ContactStore { 'type': contact.type, 'flags': contact.flags, 'pathLength': contact.pathLength, + 'pathHashWidth': contact.pathHashWidth, 'path': base64Encode(contact.path), 'pathOverride': contact.pathOverride, 'pathOverrideBytes': contact.pathOverrideBytes != null @@ -88,13 +93,31 @@ class ContactStore { final lastSeenMs = json['lastSeen'] as int? ?? 0; final lastMessageMs = json['lastMessageAt'] as int?; final lastModifiedMs = json['lastModified'] as int?; + final isLegacyPathRecord = + !json.containsKey('pathHashWidth') && + ((json['pathLength'] as int? ?? -1) > 0); + if (isLegacyPathRecord) { + appLogger.info( + 'Dropping pre-#309 path for contact ${json['name']} ' + '(decoded with the old count-as-bytes rule, so truncated); ' + 'reverting to flood until the device re-supplies it', + ); + } return Contact( publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', type: json['type'] as int? ?? 0, flags: json['flags'] as int? ?? 0, - pathLength: json['pathLength'] as int? ?? -1, - path: json['path'] != null + // Records written before #309 have no 'pathHashWidth' and their path was + // decoded with the old count-as-bytes rule, so at any width above 1 the + // stored bytes are a truncated fragment. Truncated bytes cannot be + // recovered by reinterpreting them, so a legacy record's path is dropped + // and the contact reverts to flood until the radio re-supplies it (the + // device refreshes contacts routinely, so this self-heals). Ben's call: + // re-fetch rather than keep a path we know is wrong. (#309) + pathLength: isLegacyPathRecord ? -1 : (json['pathLength'] as int? ?? -1), + pathHashWidth: json['pathHashWidth'] as int? ?? 1, + path: (!isLegacyPathRecord && json['path'] != null) ? Uint8List.fromList(base64Decode(json['path'] as String)) : Uint8List(0), pathOverride: json['pathOverride'] as int?, diff --git a/lib/storage/drift/blob_store.dart b/lib/storage/drift/blob_store.dart new file mode 100644 index 0000000..76b5034 --- /dev/null +++ b/lib/storage/drift/blob_store.dart @@ -0,0 +1,322 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +import '../../utils/app_logger.dart'; +import '../prefs_manager.dart'; +import 'offband_database.dart'; + +/// Bulk-data store backed by drift, replacing SharedPreferences for anything +/// large (#335). +/// +/// Why this exists: SharedPreferences is a settings store. On Windows every +/// mutation re-encodes and rewrites the WHOLE file synchronously, and on web it +/// is backed by `localStorage`, which is capped at 5 MiB per origin and is also +/// synchronous. This install holds ~5.2 MB of bulk data, so the web build +/// cannot function at all today and Windows stalls for tens of seconds (#306). +/// +/// Each key becomes one row, so a write touches one row rather than the entire +/// store. +class BlobStore { + BlobStore(this._db); + + final OffbandDatabase _db; + + static OffbandDatabase? _sharedDb; + static BlobStore? _override; + + /// Process-wide instance. The database must be opened once; opening it twice + /// is an error on the web backends. + static BlobStore get instance => + _override ?? BlobStore(_sharedDb ??= OffbandDatabase()); + + /// Test seam: point the singleton at an in-memory database. + @visibleForTesting + static void overrideForTest(BlobStore store) => _override = store; + + @visibleForTesting + static void clearTestOverride() => _override = null; + + /// Key families that hold bulk data. Everything else stays in + /// SharedPreferences, which is what it is for. + static const List migratedPrefixes = [ + 'channel_messages_', + 'messages_', + 'contacts', + 'discovered_contacts', + ]; + + static bool isBulkKey(String key) => migratedPrefixes.any(key.startsWith); + + /// Per-key operation chain. Merge-on-save and the legacy-key migration are + /// read-modify-write sequences with an await gap; two of them racing on the + /// same key would let the second clobber the first and silently lose + /// messages (Gemini review, 2026-07-20). Every RMW on a key runs through + /// [synchronized], which serialises operations per key while leaving + /// different keys concurrent. + final Map> _keyChains = {}; + + /// Serialises [action] against other synchronized actions on the same [key]. + Future synchronized(String key, Future Function() action) { + final prior = _keyChains[key] ?? Future.value(); + final result = prior.then((_) => action()); + // Next op waits for this one; swallow errors so one failure does not wedge + // the chain for the key. + _keyChains[key] = result.then((_) {}, onError: (_) {}); + return result; + } + + /// Reads a bulk key, falling back to SharedPreferences if drift does not + /// have it. + /// + /// Belt and braces for the switchover: if migration ever missed a key, the + /// data must still be reachable rather than silently reading as empty, which + /// is exactly how #333 presented. The fallback is LOUD, because a fallback + /// that fires in normal operation means the migration is incomplete and + /// somebody needs to know. + Future readWithPrefsFallback(String key) async { + final fromDrift = await read(key); + if (fromDrift != null) return fromDrift; + + final raw = PrefsManager.instance.get(key); + if (raw is! String || raw.isEmpty) return null; + + appLogger.warn( + 'Blob read for $key fell back to SharedPreferences: it is NOT in drift. ' + 'Migration is incomplete for this key; serving the prefs copy.', + tag: 'Storage', + ); + return raw; + } + + Future read(String key) async { + final row = await (_db.select( + _db.storedBlobs, + )..where((t) => t.key.equals(key))).getSingleOrNull(); + return row?.value; + } + + Future write(String key, String value) async { + await _db + .into(_db.storedBlobs) + .insertOnConflictUpdate( + StoredBlobsCompanion.insert(key: key, value: value), + ); + } + + /// Keys beginning with [prefix], across BOTH backends. + /// + /// Callers that clear a family of keys must see prefs-resident keys too, or + /// a pre-migration copy survives the clear and reappears through the read + /// fallback. + Future> keysWithPrefix(String prefix) async { + // Filtered in Dart rather than SQL: the table holds tens of rows, so the + // cost is irrelevant and it avoids depending on a version-specific LIKE + // API for a pinned dependency. + final rows = await _db.select(_db.storedBlobs).get(); + + return { + ...rows.map((r) => r.key).where((k) => k.startsWith(prefix)), + ...PrefsManager.instance.getKeys().where((k) => k.startsWith(prefix)), + }.toList(); + } + + /// Removes a key from BOTH backends, so a clear cannot be undone by a + /// leftover prefs copy surfacing through the fallback. + Future deleteEverywhere(String key) async { + await delete(key); + await PrefsManager.instance.remove(key); + } + + Future delete(String key) async { + await (_db.delete(_db.storedBlobs)..where((t) => t.key.equals(key))).go(); + } + + /// Moves bulk keys out of SharedPreferences into drift. + /// + /// Ordering is deliberate and non-negotiable: **write, verify by reading + /// back, and only then remove the source.** #333 was caused by a storage path + /// that chose a key silently and made 566 real messages read as empty; a + /// migration that deleted before verifying could do that permanently rather + /// than cosmetically. + /// + /// Idempotent: keys already migrated are skipped, so re-running is a no-op + /// rather than a duplicate or an overwrite of newer data. + /// + /// Every outcome is logged (SAFELANE 6). A failure never silently drops a + /// key: the source is left intact and the error surfaces. + Future migrateFromPrefs() async { + final prefs = PrefsManager.instance; + final report = MigrationReport(); + + final bulkKeys = prefs.getKeys().where(isBulkKey).toList(); + if (bulkKeys.isEmpty) { + appLogger.info( + 'Blob migration: nothing to migrate (already done or fresh install)', + tag: 'Storage', + ); + return report; + } + + appLogger.info( + 'Blob migration: ${bulkKeys.length} key(s) to move out of prefs', + tag: 'Storage', + ); + + for (final key in bulkKeys) { + try { + // Type-check rather than calling getString directly: getString THROWS + // on a non-string value, which would be counted as a migration failure + // and cry wolf. A non-string under a bulk prefix is simply not bulk + // data. + final raw = prefs.get(key); + if (raw is! String || raw.isEmpty) { + report.skipped++; + continue; + } + final source = raw; + + final existing = await read(key); + if (existing != null) { + // Drift already holds this key. Do NOT discard the prefs copy: a + // build that writes to SharedPreferences (a non-drift build, or any + // in-between test build) accumulates NEW messages there, and throwing + // them away silently gaps the history across test cycles (#355). Union + // the prefs copy into drift by message identity, keeping drift's live + // entries, then verify before removing the source. + final merged = _mergeBulk(existing, source); + if (merged == null) { + // Not a JSON list (e.g. a scalar under a bulk prefix): drift's copy + // stands, nothing to union. Safe to drop the prefs duplicate. + report.alreadyPresent++; + await prefs.remove(key); + continue; + } + if (merged == existing) { + // Prefs added nothing new; drift is already a superset. + report.alreadyPresent++; + await prefs.remove(key); + continue; + } + await write(key, merged); + final readBack = await read(key); + if (readBack != merged) { + report.failed++; + appLogger.error( + 'Blob merge FAILED for $key: wrote ${merged.length} chars, ' + 'read back ${readBack?.length ?? "null"}. Prefs copy left intact.', + tag: 'Storage', + ); + continue; + } + await prefs.remove(key); + report.merged++; + continue; + } + + await write(key, source); + + // Verify BEFORE removing the source. Length is compared rather than + // full equality to keep a multi-MB comparison cheap while still + // catching truncation, which is the realistic corruption here. + final readBack = await read(key); + if (readBack == null || readBack.length != source.length) { + report.failed++; + appLogger.error( + 'Blob migration FAILED for $key: wrote ${source.length} chars, ' + 'read back ${readBack?.length ?? "null"}. Source left intact.', + tag: 'Storage', + ); + continue; + } + + await prefs.remove(key); + report.migrated++; + report.bytes += source.length; + } catch (e) { + report.failed++; + appLogger.error( + 'Blob migration FAILED for $key: $e. Source left intact.', + tag: 'Storage', + ); + } + } + + final level = report.failed > 0 ? 'WITH FAILURES' : 'ok'; + appLogger.info( + 'Blob migration complete ($level): ${report.migrated} moved, ' + '${report.merged} merged, ' + '${report.alreadyPresent} already present, ${report.skipped} skipped, ' + '${report.failed} failed, ' + '${(report.bytes / 1024 / 1024).toStringAsFixed(2)} MB', + tag: 'Storage', + ); + if (report.failed > 0) { + appLogger.error( + 'Blob migration left ${report.failed} key(s) in SharedPreferences. ' + 'No data was lost, but those keys still carry the old cost.', + tag: 'Storage', + ); + } + return report; + } + + /// Unions the [prefs] copy of a bulk key into the [drift] copy by element + /// identity, keeping drift's entries where both hold the same one. + /// + /// All bulk families store a JSON list of objects (messages keyed by + /// `messageId`, contacts by `publicKey`); identity falls back to the object's + /// canonical form so an id-less element is never dropped. Order is drift's + /// list followed by the prefs-only elements, which mirrors how the stores + /// append on save. + /// + /// Returns null when either side is not a JSON list of objects (a scalar + /// under a bulk prefix), signalling the caller to leave drift's copy as-is. + /// Returns the drift JSON unchanged when prefs contributes nothing new. + static String? _mergeBulk(String drift, String prefs) { + final List driftList; + final List prefsList; + try { + final d = jsonDecode(drift); + final p = jsonDecode(prefs); + if (d is! List || p is! List) return null; + driftList = d; + prefsList = p; + } catch (_) { + return null; + } + + String idOf(dynamic e) { + if (e is Map) { + final id = e['messageId']; + if (id is String && id.isNotEmpty) return 'm:$id'; + final pk = e['publicKey']; + if (pk is String && pk.isNotEmpty) return 'p:$pk'; + } + // No stable id: fall back to the element's canonical JSON so distinct + // elements stay distinct and true duplicates collapse. + return 'j:${jsonEncode(e)}'; + } + + final seen = {for (final e in driftList) idOf(e)}; + final result = List.from(driftList); + for (final e in prefsList) { + if (seen.add(idOf(e))) result.add(e); + } + if (result.length == driftList.length) return drift; + return jsonEncode(result); + } +} + +/// Mutable tally of a migration run; surfaced in the log and used by tests. +class MigrationReport { + int migrated = 0; + int merged = 0; + int alreadyPresent = 0; + int skipped = 0; + int failed = 0; + int bytes = 0; + + bool get hadFailures => failed > 0; +} diff --git a/lib/storage/drift/offband_database.dart b/lib/storage/drift/offband_database.dart new file mode 100644 index 0000000..2d7d209 --- /dev/null +++ b/lib/storage/drift/offband_database.dart @@ -0,0 +1,177 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import '../../utils/app_logger.dart'; + +part 'offband_database.g.dart'; + +/// Bulk data store (#335). Replaces SharedPreferences for message history, +/// contacts and discovered contacts. +/// +/// SharedPreferences is a settings store, and using it for bulk data is what +/// causes #306: on Windows every mutation re-encodes and rewrites the entire +/// file synchronously, and on web it is backed by `localStorage`, which is +/// capped at 5 MiB per origin and is also synchronous. This install already +/// holds ~7 MB, so the web build cannot function at all today. +/// +/// drift is used because it is the only option verified to cover all six +/// targets: native SQLite on Android, iOS, macOS, Windows and Linux, and +/// SQLite compiled to WebAssembly on web, where it stores through OPFS or +/// IndexedDB and runs in a worker rather than on the UI thread. +/// +/// Settings stay in SharedPreferences. Only bulk data moves here. +@DriftDatabase(tables: [StoredBlobs]) +class OffbandDatabase extends _$OffbandDatabase { + OffbandDatabase([QueryExecutor? executor]) + : super(executor ?? _defaultExecutor()); + + static const String _dbName = 'offband_store'; + + /// `drift_flutter` selects the platform backend: native SQLite on desktop + /// and mobile, WASM on web. `web:` names the assets that must be shipped for + /// the web build (`sqlite3.wasm`, `drift_worker.js`). + static QueryExecutor _defaultExecutor() { + return driftDatabase( + name: _dbName, + // Native default is getApplicationDocumentsDirectory(), which on Windows + // is the user's Documents folder — redirected into OneDrive on most + // machines. A live SQLite file syncing to OneDrive risks lock contention + // and corruption, and it is the wrong place for app data. Use the + // application-support dir instead (%APPDATA% on Windows), where + // SharedPreferences already lives. path_provider has no web + // implementation, so this only applies off web; web ignores + // databaseDirectory and uses OPFS/IndexedDB. + native: DriftNativeOptions( + databaseDirectory: _appSupportDatabaseDirectory, + ), + web: DriftWebOptions( + sqlite3Wasm: Uri.parse('sqlite3.wasm'), + // Filename matches the asset published by the drift release, which is + // `drift_worker.js` — not the `drift_worker.dart.js` the older docs + // name. Both assets are taken from the SAME drift release so they are + // built against each other. + driftWorker: Uri.parse('drift_worker.js'), + ), + ); + } + + /// Resolves the application-support directory, and relocates a database left + /// in the old documents location by an earlier build. + /// + /// Builds before this fix opened the DB under getApplicationDocumentsDirectory + /// (OneDrive on Windows). Existing installs already have data there, so this + /// moves the file - and its `-wal` / `-shm` sidecars - to the new location on + /// first run, once, before drift opens it. Never deletes the source without a + /// successful move; a failed move leaves the old file so no data is stranded. + static Future _appSupportDatabaseDirectory() async { + final support = await getApplicationSupportDirectory(); + final newPath = p.join(support.path, '$_dbName.sqlite'); + + if (!File(newPath).existsSync()) { + try { + final docs = await getApplicationDocumentsDirectory(); + final oldPath = p.join(docs.path, '$_dbName.sqlite'); + if (File(oldPath).existsSync() && oldPath != newPath) { + // Copy ALL files first, verify, and only then delete the sources. + // Deleting per-file mid-loop (Gemini review) could strand the main + // .sqlite in one place and its -wal in another if a later copy + // failed, corrupting the DB. Copy-all-then-delete-all makes a + // partial failure recoverable: the original stays intact and the + // half-written destination is cleaned up. + final suffixes = ['', '-wal', '-shm']; + final present = suffixes + .where((s) => File('$oldPath$s').existsSync()) + .toList(); + try { + for (final s in present) { + File('$oldPath$s').copySync('$newPath$s'); + } + // Verify every copy exists with a matching size before deleting. + for (final s in present) { + final src = File('$oldPath$s'); + final dst = File('$newPath$s'); + if (!dst.existsSync() || dst.lengthSync() != src.lengthSync()) { + throw StateError('copy verification failed for $newPath$s'); + } + } + for (final s in present) { + File('$oldPath$s').deleteSync(); + } + appLogger.info( + 'Relocated drift DB out of the documents dir (OneDrive on ' + 'Windows) into the app-support dir: $oldPath -> $newPath', + tag: 'Storage', + ); + } catch (e) { + // Roll back any partial destination so drift does not open a + // half-copied DB; the original in the documents dir is untouched. + for (final s in present) { + final dst = File('$newPath$s'); + if (dst.existsSync()) { + try { + dst.deleteSync(); + } catch (_) {} + } + } + rethrow; + } + } + } catch (e) { + // Relocation is best-effort. On failure the ORIGINAL DB is left intact + // in the documents dir (the destination was rolled back), so no data is + // lost - drift opens a fresh DB at the support location and the user's + // history remains recoverable from the old path. Loud, never silent. + appLogger.error( + 'Failed to relocate the drift DB from the documents dir: $e. The ' + 'original at the old path is intact; a fresh DB will open at the ' + 'app-support location. Recover the old DB manually if needed.', + tag: 'Storage', + ); + } + } + + return support; + } + + @override + int get schemaVersion => 1; + + /// Step-1 gate for #335: proves the database opens, writes and reads back on + /// the current platform. Deliberately trivial - no user data is involved, so + /// this can be run on every target before any migration is written. + Future verifyReadWrite() async { + const probeKey = '__offband_probe__'; + final probeValue = 'ok-${DateTime.now().microsecondsSinceEpoch}'; + + await into(storedBlobs).insertOnConflictUpdate( + StoredBlobsCompanion.insert(key: probeKey, value: probeValue), + ); + + final row = await (select( + storedBlobs, + )..where((t) => t.key.equals(probeKey))).getSingleOrNull(); + + await (delete(storedBlobs)..where((t) => t.key.equals(probeKey))).go(); + + return row?.value == probeValue; + } +} + +/// One row per stored blob, replacing one SharedPreferences key each. +/// +/// Deliberately key/value for the first migration: it maps 1:1 onto the +/// existing store interfaces, so callers do not change and the migration is +/// verifiable row-for-row against the old keys. Relational tables for messages +/// and contacts are a later step, once this is proven and querying is wanted. +class StoredBlobs extends Table { + TextColumn get key => text()(); + TextColumn get value => text()(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {key}; +} diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index ccaf9ea..37b0575 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -4,6 +4,7 @@ import '../models/message.dart'; import '../models/translation_support.dart'; import '../helpers/smaz.dart'; import '../utils/app_logger.dart'; +import 'drift/blob_store.dart'; import 'prefs_manager.dart'; class MessageStore { @@ -23,10 +24,69 @@ class MessageStore { appLogger.warn('Public key hex is not set. Cannot save messages.'); return; } - final prefs = PrefsManager.instance; + // Merge into the persisted full history rather than overwriting it (#343). + // The in-memory list is windowed for memory, so a plain overwrite would + // truncate the store. Upsert by identity; deletion is explicit + // (removeMessage). final key = '$keyFor$contactKeyHex'; - final jsonList = messages.map(_messageToJson).toList(); - await prefs.setString(key, jsonEncode(jsonList)); + final blobs = BlobStore.instance; + await blobs.synchronized(key, () async { + final byKey = {}; + final existing = await blobs.readWithPrefsFallback(key); + if (existing != null && existing.isNotEmpty) { + try { + for (final e in jsonDecode(existing) as List) { + final m = _messageFromJson(e as Map); + byKey[_mergeKey(m)] = m; + } + } catch (e) { + appLogger.error( + 'Failed to decode existing DM history before merge; aborting save ' + 'to avoid truncation: $e', + tag: 'Storage', + ); + return; + } + } + for (final m in messages) { + byKey[_mergeKey(m)] = m; + } + final merged = byKey.values.toList() + ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + await blobs.write(key, jsonEncode(merged.map(_messageToJson).toList())); + }); + } + + String _mergeKey(Message m) { + if (m.messageId.isNotEmpty) return 'id:${m.messageId}'; + return 'x:${m.senderKeyHex}:${m.timestamp.millisecondsSinceEpoch}:${m.text}'; + } + + /// Explicit delete path (#343): save merges and never removes. + Future removeMessage(String contactKeyHex, Message message) async { + if (publicKeyHex.isEmpty) return; + final key = '$keyFor$contactKeyHex'; + final existing = await BlobStore.instance.readWithPrefsFallback(key); + if (existing == null || existing.isEmpty) return; + final List raw; + try { + raw = jsonDecode(existing) as List; + } catch (e) { + appLogger.error( + 'Failed to decode DM history for delete: $e', + tag: 'Storage', + ); + return; + } + final target = _mergeKey(message); + final kept = raw + .map((e) => _messageFromJson(e as Map)) + .where((m) => _mergeKey(m) != target) + .toList(); + await BlobStore.instance.write( + key, + jsonEncode(kept.map(_messageToJson).toList()), + ); } Future> loadMessages(String contactKeyHex) async { @@ -34,24 +94,30 @@ class MessageStore { appLogger.warn('Public key hex is not set. Cannot load messages.'); return []; } - final prefs = PrefsManager.instance; final key = '$keyFor$contactKeyHex'; final oldKey = '$_keyPrefix$contactKeyHex'; - String? jsonString = prefs.getString(key); + // Bulk data lives in drift (#335); fallback covers an unmigrated key and + // logs loudly if it fires. + final blobs = BlobStore.instance; + String? jsonString = await blobs.readWithPrefsFallback(key); + if (jsonString == null || jsonString.isEmpty) { - // Attempt migration from legacy unscoped key on first load - final legacyJsonString = prefs.getString(oldKey); - prefs.remove(oldKey); - if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + // Only touch prefs when the legacy key actually exists. An unconditional + // remove here ran once per contact and cost a full-file rewrite each + // time on Windows (#306). + final prefs = PrefsManager.instance; + final legacy = prefs.get(oldKey); + if (legacy is String && legacy.isNotEmpty) { appLogger.info( - 'Migrating messages from legacy key $oldKey to scoped key $key', + 'Migrating messages from legacy key $oldKey to $key (drift)', ); - await prefs.setString(key, legacyJsonString); - jsonString = legacyJsonString; + await blobs.write(key, legacy); + await prefs.remove(oldKey); + jsonString = legacy; } } if (jsonString == null || jsonString.isEmpty) { - jsonString = prefs.getString(keyFor); + jsonString = await blobs.readWithPrefsFallback(keyFor); } if (jsonString == null || jsonString.isEmpty) { return []; @@ -70,9 +136,11 @@ class MessageStore { appLogger.warn('Public key hex is not set. Cannot clear messages.'); return; } - final prefs = PrefsManager.instance; final key = '$keyFor$contactKeyHex'; - await prefs.remove(key); + // Clear both backends: drift is authoritative, but a pre-migration prefs + // copy must not survive a clear and reappear via the read fallback. + await BlobStore.instance.delete(key); + await PrefsManager.instance.remove(key); } Map _messageToJson(Message msg) { diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index 3b615b1..a9cd4df 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -35,13 +35,15 @@ class UnreadStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } @@ -55,7 +57,11 @@ class UnreadStore { try { final json = jsonDecode(jsonString) as Map; return json.map((key, value) => MapEntry(key, value as int)); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode unread state: $e', tag: 'Storage'); return {}; } } diff --git a/lib/utils/app_backgrounder.dart b/lib/utils/app_backgrounder.dart new file mode 100644 index 0000000..2a01908 --- /dev/null +++ b/lib/utils/app_backgrounder.dart @@ -0,0 +1,33 @@ +import 'package:flutter/services.dart'; + +import 'platform_info.dart'; + +/// Sends the app to the background without tearing it down. +/// +/// `SystemNavigator.pop()` is NOT this: on Android it calls `finish()` on the +/// activity, which destroys the Flutter engine and drops the radio connection, +/// so reopening the app finds itself disconnected. `moveTaskToBack` leaves the +/// activity alive and simply hands focus back to whatever was in front before. +class AppBackgrounder { + static const MethodChannel _channel = MethodChannel( + 'meshcore_open/app_lifecycle', + ); + + /// Returns true when the app was actually backgrounded. + /// + /// Android only. Desktop windows are closed by their own chrome and iOS + /// forbids programmatic backgrounding, so elsewhere this is a no-op and the + /// caller should leave the press unhandled rather than act on it. + static Future moveToBackground() async { + if (!PlatformInfo.isAndroid) return false; + try { + return await _channel.invokeMethod('moveTaskToBack') ?? false; + } on PlatformException catch (_) { + // Surface nothing to the user: the fallback is simply that back does + // nothing at the root, which is the pre-existing behaviour. + return false; + } on MissingPluginException catch (_) { + return false; + } + } +} diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index cfc8ab9..d07c3ec 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../l10n/l10n.dart'; import '../services/ui_view_state_service.dart'; +import '../utils/app_backgrounder.dart'; import 'quick_switch_bar.dart'; /// Shared shell for the primary views (Contacts / Channels / Map). @@ -11,7 +13,7 @@ import 'quick_switch_bar.dart'; /// dockable pane that can be pinned open on wide ones. /// /// The bottom bar stays a bottom bar at every width; it never becomes a rail. -class AppShell extends StatelessWidget { +class AppShell extends StatefulWidget { static const double wideBreakpoint = 720; static const double _drawerWidth = 300; @@ -36,6 +38,12 @@ class AppShell extends StatelessWidget { /// List content for the active view. Null renders an empty drawer body. final Widget? drawerContent; + /// App-level actions, pinned to the bottom of the panel. These are not + /// contextual, which is why they were duplicated in three screens' overflow + /// menus before. Screen-level actions stay in their own overflow menu. + final VoidCallback? onDisconnect; + final VoidCallback? onSettings; + const AppShell({ super.key, required this.body, @@ -45,48 +53,98 @@ class AppShell extends StatelessWidget { this.appBarBuilder, this.floatingActionButton, this.drawerContent, + this.onDisconnect, + this.onSettings, this.contactsUnreadCount = 0, this.channelsUnreadCount = 0, }); + @override + State createState() => _AppShellState(); +} + +class _AppShellState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + /// System back, in priority order: + /// 1. an open drawer closes, + /// 2. on a detail screen, pop back to the list it came from, + /// 3. on a primary view, send the app to the background so Android + /// returns to the home screen or the previous app. + /// + /// Step 3 must NOT pop, even though the route below can be popped. The + /// primary views sit on top of the scanner, so popping would dump a + /// connected user back onto the radio-connect list. Reaching the scanner is + /// what Disconnect is for, not what Back is for. + /// + /// A primary view is one carrying the bottom bar; a detail screen (a channel + /// chat) has no [selectedIndex] and is genuinely pushed. + Future _handleBack() async { + final scaffold = _scaffoldKey.currentState; + if (scaffold?.isDrawerOpen ?? false) { + scaffold!.closeDrawer(); + return; + } + + final isPrimaryView = widget.selectedIndex != null; + final navigator = Navigator.of(context); + if (!isPrimaryView && navigator.canPop()) { + navigator.pop(); + return; + } + + // Background, do NOT finish. SystemNavigator.pop() would call finish() on + // the activity, tearing down the Flutter engine and dropping the radio + // connection, so reopening the app would show a disconnected radio. + await AppBackgrounder.moveToBackground(); + } + @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isWide = constraints.maxWidth >= wideBreakpoint; - final pinned = - isWide && context.watch().navDrawerPinned; - - return pinned - ? _buildPinned(context) - : _buildTransient(context, isWide); + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (didPop) return; + _handleBack(); }, + child: LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth >= AppShell.wideBreakpoint; + final pinned = + isWide && context.watch().navDrawerPinned; + + return pinned + ? _buildPinned(context) + : _buildTransient(context, isWide); + }, + ), ); } /// Null on detail screens, which show the panel but no bottom bar. Widget? _bottomBar() { - final index = selectedIndex; - final onSelected = onDestinationSelected; + final index = widget.selectedIndex; + final onSelected = widget.onDestinationSelected; if (index == null || onSelected == null) return null; return SafeArea( top: false, child: QuickSwitchBar( selectedIndex: index, onDestinationSelected: onSelected, - contactsUnreadCount: contactsUnreadCount, - channelsUnreadCount: channelsUnreadCount, + contactsUnreadCount: widget.contactsUnreadCount, + channelsUnreadCount: widget.channelsUnreadCount, ), ); } PreferredSizeWidget? _appBar(BuildContext context, bool pinned) { - return appBarBuilder?.call(context, pinned) ?? appBar; + return widget.appBarBuilder?.call(context, pinned) ?? widget.appBar; } /// Wide + pinned: the panel is laid out beside the body, not overlaid. Widget _buildPinned(BuildContext context) { return Scaffold( + key: _scaffoldKey, appBar: _appBar(context, true), body: SafeArea( top: false, @@ -94,15 +152,20 @@ class AppShell extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( - width: _drawerWidth, - child: _NavPanel(isWide: true, content: drawerContent), + width: AppShell._drawerWidth, + child: _NavPanel( + isWide: true, + content: widget.drawerContent, + onDisconnect: widget.onDisconnect, + onSettings: widget.onSettings, + ), ), const VerticalDivider(width: 1), - Expanded(child: body), + Expanded(child: widget.body), ], ), ), - floatingActionButton: floatingActionButton, + floatingActionButton: widget.floatingActionButton, bottomNavigationBar: _bottomBar(), ); } @@ -111,13 +174,19 @@ class AppShell extends StatelessWidget { /// the hamburger into the app bar automatically. Widget _buildTransient(BuildContext context, bool isWide) { return Scaffold( + key: _scaffoldKey, appBar: _appBar(context, false), drawer: Drawer( - width: _drawerWidth, - child: _NavPanel(isWide: isWide, content: drawerContent), + width: AppShell._drawerWidth, + child: _NavPanel( + isWide: isWide, + content: widget.drawerContent, + onDisconnect: widget.onDisconnect, + onSettings: widget.onSettings, + ), ), - body: body, - floatingActionButton: floatingActionButton, + body: widget.body, + floatingActionButton: widget.floatingActionButton, bottomNavigationBar: _bottomBar(), ); } @@ -128,8 +197,15 @@ class AppShell extends StatelessWidget { class _NavPanel extends StatelessWidget { final bool isWide; final Widget? content; + final VoidCallback? onDisconnect; + final VoidCallback? onSettings; - const _NavPanel({required this.isWide, this.content}); + const _NavPanel({ + required this.isWide, + this.content, + this.onDisconnect, + this.onSettings, + }); @override Widget build(BuildContext context) { @@ -164,6 +240,51 @@ class _NavPanel extends StatelessWidget { ), if (isWide) const Divider(height: 1), Expanded(child: content ?? const SizedBox.shrink()), + if (onDisconnect != null || onSettings != null) ...[ + const Divider(height: 1), + _Footer(onDisconnect: onDisconnect, onSettings: onSettings), + ], + ], + ), + ); + } +} + +/// App-level actions pinned to the bottom of the panel. +class _Footer extends StatelessWidget { + final VoidCallback? onDisconnect; + final VoidCallback? onSettings; + + const _Footer({this.onDisconnect, this.onSettings}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final colors = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 8, 8), + child: Row( + children: [ + if (onDisconnect != null) + Expanded( + child: TextButton.icon( + // Kept visually distinct: this drops the radio connection, + // and it was a red menu entry before the move. + style: TextButton.styleFrom(foregroundColor: colors.error), + icon: const Icon(Icons.logout, size: 18), + label: Text(l10n.common_disconnect), + onPressed: onDisconnect, + ), + ), + if (onSettings != null) + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.settings, size: 18), + label: Text(l10n.settings_title), + onPressed: onSettings, + ), + ), ], ), ); diff --git a/lib/widgets/contact_settings_dialog.dart b/lib/widgets/contact_settings_dialog.dart new file mode 100644 index 0000000..d22777e --- /dev/null +++ b/lib/widgets/contact_settings_dialog.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; +import '../models/contact.dart'; +import '../services/app_settings_service.dart'; + +/// The per-contact settings dialog (Smaz/Cyr2Lat compression + telemetry +/// grants). Shared so it can be opened from the chat ellipsis AND the contacts +/// long-press menu without duplicating the body (#351). +void showContactSettingsDialog(BuildContext context, Contact contact) { + final connector = Provider.of(context, listen: false); + final appSettingsService = Provider.of( + context, + listen: false, + ); + connector.ensureContactSmazSettingLoaded(contact.publicKeyHex); + connector.ensureContactCyr2LatSettingLoaded(contact.publicKeyHex); + bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); + bool cyr2latEnabled = connector.isContactCyr2LatEnabled(contact.publicKeyHex); + String? selectedCyr2LatProfileId = connector.getContactCyr2LatProfileId( + contact.publicKeyHex, + ); + bool teleBaseEnabled = contact.teleBaseEnabled; + bool teleLocEnabled = contact.teleLocEnabled; + bool teleEnvEnabled = contact.teleEnvEnabled; + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(context.l10n.contact_settings), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (contact.hasLocation) ...[ + _infoRow( + context.l10n.chat_location, + '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', + ), + const Divider(height: 8), + ], + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.channels_smazCompression), + subtitle: Text(context.l10n.chat_compressOutgoingMessages), + value: smazEnabled, + onChanged: (value) { + connector.setContactSmazEnabled(contact.publicKeyHex, value); + connector.setContactCyr2LatEnabled( + contact.publicKeyHex, + false, + ); + setDialogState(() { + smazEnabled = value; + if (smazEnabled) { + cyr2latEnabled = false; + } + }); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.channels_cyr2latCompression), + subtitle: Text(context.l10n.channels_cyr2latCompressionDscr), + value: cyr2latEnabled, + onChanged: (value) { + connector.setContactCyr2LatEnabled( + contact.publicKeyHex, + value, + ); + connector.setContactSmazEnabled(contact.publicKeyHex, false); + setDialogState(() { + cyr2latEnabled = value; + if (cyr2latEnabled) { + smazEnabled = false; + } + }); + }, + ), + if (cyr2latEnabled) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: DropdownButtonFormField( + initialValue: selectedCyr2LatProfileId, + decoration: InputDecoration( + labelText: + context.l10n.channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), + ), + items: appSettingsService.settings.cyr2latProfiles.map(( + profile, + ) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) { + connector.setContactCyr2LatProfileId( + contact.publicKeyHex, + value, + ); + setDialogState(() { + selectedCyr2LatProfileId = value; + }); + }, + ), + ), + ], + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleBase), + subtitle: Text(context.l10n.contact_teleBaseSubtitle), + value: teleBaseEnabled, + onChanged: (value) { + setDialogState(() => teleBaseEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleLoc), + subtitle: Text(context.l10n.contact_teleLocSubtitle), + value: teleLocEnabled, + onChanged: (value) { + setDialogState(() => teleLocEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleEnv), + subtitle: Text(context.l10n.contact_teleEnvSubtitle), + value: teleEnvEnabled, + onChanged: (value) { + setDialogState(() => teleEnvEnabled = value); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + connector.setContactFlags( + contact, + teleBase: teleBaseEnabled, + teleLoc: teleLocEnabled, + teleEnv: teleEnvEnabled, + ); + Navigator.pop(context); + }, + child: Text(context.l10n.common_close), + ), + ], + ), + ), + ); +} + +Widget _infoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text(label, style: TextStyle(color: Colors.grey[600])), + ), + Expanded(child: SelectableText(value)), + ], + ), + ); +} diff --git a/lib/widgets/map_layer_panel.dart b/lib/widgets/map_layer_panel.dart new file mode 100644 index 0000000..29c18ea --- /dev/null +++ b/lib/widgets/map_layer_panel.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../l10n/l10n.dart'; +import '../services/app_settings_service.dart'; + +/// Map layer toggles for the nav panel. +/// +/// The Map view has no list to show, so its panel carries the layer controls +/// that otherwise live in a modal behind the floating button. Pinned on a wide +/// screen these stay visible and the map updates underneath as they are +/// flipped, instead of a dialog covering the thing being filtered. +/// +/// The same settings back both surfaces, so a change here shows in the dialog +/// and vice versa. +class MapLayerPanel extends StatelessWidget { + const MapLayerPanel({super.key}); + + @override + Widget build(BuildContext context) { + final service = context.watch(); + final settings = service.settings; + final l10n = context.l10n; + final theme = Theme.of(context); + + return ListView( + padding: EdgeInsets.zero, + children: [ + _sectionHeader(theme, l10n.map_nodeTypes), + _toggle( + label: l10n.map_chatNodes, + icon: Icons.person_outline, + value: settings.mapShowChatNodes, + onChanged: service.setMapShowChatNodes, + ), + _toggle( + label: l10n.map_repeaters, + icon: Icons.cell_tower, + value: settings.mapShowRepeaters, + onChanged: service.setMapShowRepeaters, + ), + _toggle( + label: l10n.map_otherNodes, + icon: Icons.help_outline, + value: settings.mapShowOtherNodes, + onChanged: service.setMapShowOtherNodes, + ), + const Divider(height: 1), + _toggle( + label: l10n.map_alwaysShowNames, + icon: Icons.label_outline, + value: settings.mapAlwaysShowNames, + onChanged: service.setMapAlwaysShowNames, + ), + const Divider(height: 1), + _sectionHeader(theme, l10n.map_filterNodes), + _toggle( + label: l10n.map_showDiscoveryContacts, + icon: Icons.person_search, + value: settings.mapShowDiscoveryContacts, + onChanged: service.setMapShowDiscoveryContacts, + ), + _toggle( + label: l10n.map_showGuessedLocations, + icon: Icons.location_searching, + value: settings.mapShowGuessedLocations, + onChanged: service.setMapShowGuessedLocations, + ), + _toggle( + label: l10n.map_showOverlaps, + icon: Icons.layers, + value: settings.mapShowOverlaps, + onChanged: service.setMapShowOverlaps, + ), + ], + ); + } + + Widget _sectionHeader(ThemeData theme, String label) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + label, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w700, + ), + ), + ); + } + + Widget _toggle({ + required String label, + required IconData icon, + required bool value, + required ValueChanged onChanged, + }) { + return SwitchListTile( + dense: true, + secondary: Icon(icon, size: 20), + title: Text(label, maxLines: 2, overflow: TextOverflow.ellipsis), + value: value, + onChanged: onChanged, + ); + } +} diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 0973fae..d31de4e 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -11,6 +11,7 @@ import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../utils/app_logger.dart'; +import '../helpers/path_helper.dart'; import 'path_management_dialog.dart'; class RepeaterLoginDialog extends StatefulWidget { @@ -115,9 +116,19 @@ class _RepeaterLoginDialogState extends State { ); final timeoutSeconds = (timeoutMs / 1000).ceil(); final timeout = Duration(milliseconds: timeoutMs + 2000); + // `selection.hopCount` is ambiguous by construction: a device-discovered + // path carries a HOP count, a user override carries a BYTE count (#279). + // Do not assert hops here. Log the raw field, the width, and the actual + // byte length so a capture discriminates them: + // bytes == field -> byte-count semantics (override) + // bytes == field * w -> hop-count semantics (device) + // (#298) + final routingWidth = _connector.pathHashByteWidth; final selectionLabel = selection.useFlood ? 'flood' - : '${selection.hopCount} hops'; + : 'field=${selection.hopCount} w=$routingWidth ' + 'bytes=${selection.pathBytes.length} ' + '[${PathHelper.formatPathHex(selection.pathBytes, routingWidth)}]'; appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin'); bool? loginResult; bool isAdmin = false; diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 2641c02..a12ea98 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -11,6 +11,7 @@ import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../utils/app_logger.dart'; +import '../helpers/path_helper.dart'; import '../helpers/snack_bar_builder.dart'; import 'path_management_dialog.dart'; @@ -111,9 +112,19 @@ class _RoomLoginDialogState extends State { ); final timeoutSeconds = (timeoutMs / 1000).ceil(); final timeout = Duration(milliseconds: timeoutMs + 2000); + // `selection.hopCount` is ambiguous by construction: a device-discovered + // path carries a HOP count, a user override carries a BYTE count (#279). + // Do not assert hops here. Log the raw field, the width, and the actual + // byte length so a capture discriminates them: + // bytes == field -> byte-count semantics (override) + // bytes == field * w -> hop-count semantics (device) + // (#298) + final routingWidth = _connector.pathHashByteWidth; final selectionLabel = selection.useFlood ? 'flood' - : '${selection.hopCount} hops'; + : 'field=${selection.hopCount} w=$routingWidth ' + 'bytes=${selection.pathBytes.length} ' + '[${PathHelper.formatPathHex(selection.pathBytes, routingWidth)}]'; appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin'); bool? loginResult; bool isAdmin = false; diff --git a/package.json b/package.json index 1684721..1f818b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "meshcore-open", "scripts": { - "build": "dart run build_pipe:build", - "deploy": "bun x wrangler deploy" + "build": "dart run build_pipe:build" } } diff --git a/play/1.1.2-rc.3.txt b/play/1.1.2-rc.3.txt new file mode 100644 index 0000000..1e5ae05 --- /dev/null +++ b/play/1.1.2-rc.3.txt @@ -0,0 +1,9 @@ +What's new in 1.1.2-rc.3 + +- Block or mute contacts and channel senders, with a Blocked management screen +- Per-channel notifications: all, mentions only, or muted +- Better path tracing and mesh topology readout +- Share and add channels by QR code +- Fix: USB no longer wedges some ESP32 boards on connect +- Fix: messages to a blocked contact are no longer sent +- More radios report GPS position reliably diff --git a/play/1.2.0.txt b/play/1.2.0.txt new file mode 100644 index 0000000..92d6e69 --- /dev/null +++ b/play/1.2.0.txt @@ -0,0 +1,8 @@ +What's new in 1.2.0. A major update. + +- Your data now lives in a real database. Existing messages and contacts migrate automatically on first launch, nothing lost. +- New navigation drawer you can pin open, with the channel list built in and in-place channel switching. +- GIFs share as links now, so people on other MeshCore apps can see them. +- Desktop remembers its window size and position. +- Reach contact settings from the long-press menu. +- Back-button, path decoding, and connect-speed fixes. \ No newline at end of file diff --git a/play/README.md b/play/README.md new file mode 100644 index 0000000..e6c339d --- /dev/null +++ b/play/README.md @@ -0,0 +1,16 @@ +# Play Console "What's new" + +One file per release: `play/.txt`, e.g. `play/1.1.2-rc.4.txt`. + +**Hard limit: 500 Unicode characters.** The release gate (`scripts/release-gate.sh`) +fails the build if this file is missing, empty, or over 500 chars — so the limit is caught +here, not as a Console rejection at upload time. + +## Style + +Plain language for end users. No issue numbers, no internal terms. Not promotional +(Google's Metadata policy forbids "free", "#1", "best", etc. and ALL-CAPS except a brand +name). + +**The owner pastes this into Play Console manually.** CI validates the file; it never +uploads. Play distribution is an external, human-triggered action. diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..e31ec5c --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1624 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _discoveryapis_commons: + dependency: transitive + description: + name: _discoveryapis_commons + sha256: "113c4100b90a5b70a983541782431b82168b3cae166ab130649c36eb3559d498" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c + url: "https://pub.dev" + source: hosted + version: "99.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + build_config: + dependency: transitive + description: + name: build_config + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae + url: "https://pub.dev" + source: hosted + version: "1.3.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + url: "https://pub.dev" + source: hosted + version: "4.1.2" + build_pipe: + dependency: "direct main" + description: + name: build_pipe + sha256: cf851728764dd87c0a5011a209be64ac04dbb3836c244dbdb31684d7e4aff79f + url: "https://pub.dev" + source: hosted + version: "0.3.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: "direct main" + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csv: + dependency: transitive + description: + name: csv + sha256: "63ed2871dd6471193dffc52c0e6c76fb86269c00244d244297abbb355c84a86e" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 + url: "https://pub.dev" + source: hosted + version: "3.1.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" + url: "https://pub.dev" + source: hosted + version: "0.7.13" + dinja: + dependency: transitive + description: + name: dinja + sha256: "34d4e569ceb3d900ab061f16cae947223968cc63d2484a8a4710d5ecfa4ae3a2" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + drift: + dependency: "direct main" + description: + name: drift + sha256: "84688491040b0ceb26575709a84d701c84ad4df4d8aff020adf6d85f155fb0dd" + url: "https://pub.dev" + source: hosted + version: "2.34.2" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "9cfff1576b49725da0d32c040651a41ae195e8c4af8d8da301593e41d7abc2f7" + url: "https://pub.dev" + source: hosted + version: "2.34.0" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: "91acf4bee7c3c84467cba46455aa70e5292a3b889f4582645d74f2e5a8c106f2" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flserial: + dependency: "direct main" + description: + path: "." + ref: "48216310061efc8d5d217cc18014fc2cb501646e" + resolved-ref: "48216310061efc8d5d217cc18014fc2cb501646e" + url: "https://github.com/MeshEnvy/flserial.git" + source: git + version: "0.3.5" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "4d9ff93a197cf64925652414f11a22cd160a6e500997020ffec044544b9ddd38" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: d66bdcb0438e643d5de4af914851bdf7322448384795666efe92236626484740 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: bb433d8c614964be3023d63f0460f1e2fe1a436cdbabd9bcc88438b509f4f045 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: c08563ccef620be5a06dd6b895ee51ecd986d1fcab1edd2dbc3fa125518abb21 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_blue_plus_platform_interface: + dependency: "direct main" + description: + name: flutter_blue_plus_platform_interface + sha256: "55abf2bdae442f2ed4cace044d491cbb9dd833729585541354e4ec42c227989c" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: f6ed6bedf7568a3c3f91f0d668ca974c2a6d95f397e58e0d8cd6ad8681355819 + url: "https://pub.dev" + source: hosted + version: "8.2.3" + flutter_blue_plus_winrt: + dependency: transitive + description: + name: flutter_blue_plus_winrt + sha256: dce19eb095c5ed70de997c02911afd255d867627fceff9c68c6f8b88792d597e + url: "https://pub.dev" + source: hosted + version: "0.0.19" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: fc5c01a5e1b8f7bb51d0c737714f0c50440dbdf1aeddc5f8cbba313aa6fd4856 + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_langdetect: + dependency: "direct main" + description: + name: flutter_langdetect + sha256: "93bd865c7d5723eac614744abb32234ee4f593505a293bc17ef097bd55fbdf38" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "40c6a69189a622bda89ddcf50a139f4ba0f0eb9c0fef6718845b1f8b95452ed6" + url: "https://pub.dev" + source: hosted + version: "22.1.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b" + url: "https://pub.dev" + source: hosted + version: "8.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "4dfbae10debab9ac975d8b01943fed94c0c19dad43f825964b29f3c3f9a8a852" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + flutter_local_notifications_web: + dependency: transitive + description: + name: flutter_local_notifications_web + sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: ce1debbb29cade61964334b2a19c7c76e5bb2ef7dacf126c6424ee44036232f8 + url: "https://pub.dev" + source: hosted + version: "8.3.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_cloud: + dependency: transitive + description: + name: google_cloud + sha256: b385e20726ef5315d302c5933bfb728103116c5be2d3d17094b01a82da538c1f + url: "https://pub.dev" + source: hosted + version: "0.5.0" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis: + dependency: transitive + description: + name: googleapis + sha256: "692fb9e90c321b61a7a2123de0353ec8a20691cd979db2553d8d732f710f6535" + url: "https://pub.dev" + source: hosted + version: "15.0.0" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: ea53b290aaf1fdff615c8d79091b02c9150ff81c48f7f3b7460598c444f3c50a + url: "https://pub.dev" + source: hosted + version: "2.3.3" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: "1ab97d83824ce5d853b98c3ad13867ffa7f0a7aa175babb4f5c3d038c156961c" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" + url: "https://pub.dev" + source: hosted + version: "4.9.1" + injector: + dependency: transitive + description: + name: injector + sha256: ed389bed5b48a699d5b9561c985023d0d5cc88dd5ff2237aadcce5a5ab433e4e + url: "https://pub.dev" + source: hosted + version: "3.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + llamadart: + dependency: "direct main" + description: + name: llamadart + sha256: "71d7986483b0e48402bcc8f9e0e32a16eab2983f5cd243e77d1603cb1a8ac669" + url: "https://pub.dev" + source: hosted + version: "0.6.17" + logger: + dependency: transitive + description: + name: logger + sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: "49c532dd0b74544e9d8d93ec0f821d52ec532e7c9263c889ebe71b1be4f34ba7" + url: "https://pub.dev" + source: hosted + version: "4.2951.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: "385e7168ecc77eb545220223c49eef8ab249da7bf57f22781c40a04d23fb196f" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + ml_algo: + dependency: "direct main" + description: + name: ml_algo + sha256: d076bec9f5059c39b2878b046259e13987fa79e03ed81d99af9477708126e09c + url: "https://pub.dev" + source: hosted + version: "16.18.0" + ml_dataframe: + dependency: "direct main" + description: + name: ml_dataframe + sha256: "75434865e7ff85edcf8b006cb7c580e65a7863bb853010f41e12597b69d55db7" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + ml_linalg: + dependency: transitive + description: + name: ml_linalg + sha256: beaebe2b85e9f1f6233a19098883f1f1596ce6ed05a03bc51ac552420507265e + url: "https://pub.dev" + source: hosted + version: "13.12.7" + ml_preprocessing: + dependency: transitive + description: + name: ml_preprocessing + sha256: fdc7bdf65e53bf377db2ac6ad017d88f08d81827e1c6062b9348893c0bab9f26 + url: "https://pub.dev" + source: hosted + version: "7.0.2" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: ce3f059ebd6dbfab7292bba0e893e354b46730636820d3c9ef69005ce2d55bce + url: "https://pub.dev" + source: hosted + version: "7.4.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "8aaead321425bd3f03bd5894aa27c8ea6993eab95531da7e59f5d39c6e5708ec" + url: "https://pub.dev" + source: hosted + version: "0.18.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: "direct main" + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: ddcedc1f7876e62717de43ab3491e2829bdad0b028261805f94aa080967e5859 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" + url: "https://pub.dev" + source: hosted + version: "12.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + url: "https://pub.dev" + source: hosted + version: "2.4.27" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + simple_sparse_list: + dependency: transitive + description: + name: simple_sparse_list + sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f + url: "https://pub.dev" + source: hosted + version: "0.1.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590" + url: "https://pub.dev" + source: hosted + version: "2.5.11" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f + url: "https://pub.dev" + source: hosted + version: "2.4.3+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqlcipher_flutter_libs: + dependency: transitive + description: + name: sqlcipher_flutter_libs + sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929" + url: "https://pub.dev" + source: hosted + version: "0.7.0+eol" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: c73fd75df1332d76a6257f4823ae4df9c791f522b97e4a60cbcad214de1becf4 + url: "https://pub.dev" + source: hosted + version: "3.5.0" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454" + url: "https://pub.dev" + source: hosted + version: "0.6.0+eol" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "40bdddb306a727be9ce510bd2d2b9a6c9db6c586d846ef7b22e3990a2b24f02d" + url: "https://pub.dev" + source: hosted + version: "0.44.5" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" + url: "https://pub.dev" + source: hosted + version: "1.31.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + test_core: + dependency: transitive + description: + name: test_core + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" + url: "https://pub.dev" + source: hosted + version: "0.6.17" + timezone: + dependency: transitive + description: + name: timezone + sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" + url: "https://pub.dev" + source: hosted + version: "0.11.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: a6f7bcfc8ea1d5ce1f6c0b1c39117a9919f4953edd9fd7a64090a9796c499b57 + url: "https://pub.dev" + source: hosted + version: "1.1.9" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: "direct main" + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + xrange: + dependency: transitive + description: + name: xrange + sha256: "17bb75bd5eeae7152680f9b748880bf88a670b61f360266df56f7bd239cb6343" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0415a36..0da6b94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.2-rc.3+60 +version: 1.2.0+61 environment: sdk: ^3.9.2 @@ -62,7 +62,7 @@ dependencies: url_launcher: ^6.3.0 # Launch URLs in system browser flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text gpx: ^2.3.0 - path_provider: ^2.1.5 + path_provider: ^2.1.6 share_plus: ^12.0.1 build_pipe: ^0.3.1 material_symbols_icons: ^4.2906.0 @@ -73,6 +73,13 @@ dependencies: ml_dataframe: ^1.0.0 llamadart: '>=0.6.8 <0.7.0' flutter_langdetect: ^0.0.1 + # PINNED (#335): web ships version-matched sqlite3.wasm + drift_worker.js + # from the drift release. Bump deliberately, then re-download both assets. + drift: 2.34.2 + drift_flutter: 0.3.1 + path: ^1.9.1 + window_manager: ^0.5.2 + screen_retriever: ^0.2.2 hooks: user_defines: @@ -95,6 +102,8 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^6.0.0 flutter_launcher_icons: ^0.14.4 + drift_dev: 2.34.0 + build_runner: ^2.15.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -163,7 +172,7 @@ build_pipe: platforms: web: build: - build_command: flutter build web --release --pwa-strategy=none + build_command: flutter build web --release --pwa-strategy=none --dart-define-from-file=dart_defines.json # Strongly recommended: disables the default service worker which often causes more cache headaches add_version_query_param: true # This is the key flag! It appends ?v= to bootstrap/JS files diff --git a/release-notes/1.1.2-rc.3.md b/release-notes/1.1.2-rc.3.md new file mode 100644 index 0000000..c5c07c2 --- /dev/null +++ b/release-notes/1.1.2-rc.3.md @@ -0,0 +1,19 @@ +## Offband Meshcore 1.1.2-rc.3 + +This release brings a full **block / mute system**, richer **path tracing and topology**, and **per-channel notification control**, along with a batch of connectivity and UX fixes. It combines the rc.2 and rc.3 closed-test builds. + +### Blocking and muting +You can now block a contact or a channel sender from contacts, DMs, discovery, or directly from a channel message. Blocked DMs and adverts are suppressed with a visible marker, a name-only block is promoted to the sender's public key the next time they advert or DM you, and stale blocks age out after 30 days. Everything is managed from Settings → Blocked, and where the radio's firmware supports it, your block list syncs to the device on connect. + +### Notifications, your way +Each channel gets a three-way notification selector — all messages, mentions only, or muted — applied right at the notification gate and remembered per channel. + +### Path tracing and topology +Path traces are now width-matched to the mesh, with a topology readout and a route-builder that groups hops the way the network actually addresses them rather than as raw bytes. Channel QR codes let you share a channel or add one by scanning, using the reference-app format. + +### Fixes worth calling out +- USB no longer wedges ESP32-C6 boards: the open-time DTR pulse is gated by device type, so it only fires where it's needed. +- Outgoing DMs to a blocked contact are dropped at the send path. +- GPS refresh falls back to stock-firmware telemetry when the fork-specific query isn't available, so more radios report position without flooding the link. + +See `CHANGELOG.md` for the full itemized list. diff --git a/release-notes/1.2.0.md b/release-notes/1.2.0.md new file mode 100644 index 0000000..319f7f9 --- /dev/null +++ b/release-notes/1.2.0.md @@ -0,0 +1,105 @@ +## Offband Meshcore 1.2.0 + +Our biggest release yet. A ground-up interface redesign, a real database under the +hood, GIFs that work across every MeshCore client, and hands-on control of your +radio's RF front end. This is also the first Offband release built, signed, and +shipped to every platform automatically. + +![Offband Meshcore 1.2.0 on Windows: the redesigned interface with the navigation rail pinned open alongside a channel](https://raw.githubusercontent.com/OffbandMesh/meshcore-client/3826b7372122f812cc54d4f5bae438581ad78240/docs/releases/1.2.0/redesign.png) + +### One app, every platform + +From this release on, every Offband build ships everywhere at once, signed with +the same key so updates just work: + +- **Android**: install from Google Play or sideload the signed APK +- **Windows**: desktop build +- **Linux**: desktop build +- **Web**: run it right in your browser, no install, at [offband.app](https://offband.app) + +Pick your platform below. + +### A brand-new interface + +The headline feature. Offband has been rebuilt around a modern navigation shell +that finally gets out of your way. + +- A **left rail** you open from the hamburger button, and can **pin open** so it + lives right alongside your conversation instead of covering it. +- Your **channels live in the rail** and switch **in place**, so jumping between + channels never loses your spot or your scroll position. +- Map layer toggles, Disconnect, and Settings all found sensible new homes in the + panel, and the back button finally behaves the way it should: it backgrounds + the app instead of killing it, and never knocks you off a connected radio. + +### Your data, on solid ground + +Under the surface, Offband now keeps your messages, contacts, and channels in a +real SQLite database instead of a pile of loose preference files. + +- Your **existing data migrates automatically** the first time you open this + version. Nothing is thrown away, and your **full message history is preserved**. +- Storage problems that used to fail silently now surface, so nothing goes wrong + in the dark. +- On desktop, your data no longer gets swept into OneDrive/Documents sync. + +### GIFs everyone can see + +GIFs are no longer an Offband-only party trick. + +- Send a GIF and it goes out as a **plain Giphy link**, so people on **stock + MeshCore and other clients can tap it and watch it** too, not just Offband + users. +- Paste a **Giphy or Tenor** link and it renders inline, behind a trusted-host + safety allowlist. + +### Take command of your radio's front end + +Power users, this one's for you. On supported hardware, Offband now gives you +direct control of your radio's **FEM / LNA** (the front-end module and low-noise +amplifier that shape receive sensitivity and transmit path). + +- Toggle **FEM LNA** right from Radio Settings. +- Device Info now reads out your radio's Offband capabilities, so you can see at a + glance what your hardware supports. + +Both are gated on firmware capability, so they appear only where the radio +actually supports them. + +### Deeper diagnostics + +More signal, less guesswork when you need to see what your mesh is doing. + +- Message **arrival time is now logged and persisted** separately from the claimed + send time, so you can tell when a message really reached you versus when the + sender says it left (#285). +- Path diagnostics report **honest, width-aware units** instead of mislabeled hop + counts, and path lengths decode exactly per the firmware contract. + +### Faster across the board + +The new storage engine and a round of under-the-hood work add up to a noticeably +snappier app. + +- Connecting no longer rewrites your entire preferences file for every channel, so + connect time is quicker and lighter on storage. +- Bulk reads and writes go through the database now, which scales far better as + your message and contact history grows. + +### And a few more + +- Desktop remembers your window size and position between launches. +- Reach a contact's settings straight from its long-press menu. +- You can no longer accidentally block your own node. + +--- + +See the full, itemized changelog in the repository. + +### Support Offband + +Offband is free and open source, built by a very small team. If it is useful to +you, please consider chipping in at +[offband.org/donate](https://offband.org/donate). Donations go directly toward +keeping the project going and toward getting Offband onto the iOS App Store, which +carries its own ongoing Apple costs. Thank you for being part of it. diff --git a/release-notes/README.md b/release-notes/README.md new file mode 100644 index 0000000..1d70330 --- /dev/null +++ b/release-notes/README.md @@ -0,0 +1,22 @@ +# GitHub release notes + +One file per release: `release-notes/.md`, where `` is the marketing +version from `pubspec.yaml` (the part before `+`), e.g. `release-notes/1.1.2-rc.4.md`. + +CI sets the GitHub release body from this file verbatim. It is **published directly** — +the notes are reviewed in the release-cut PR, so there is no draft step. What is in this +file at tag time is what the world sees. + +## Style + +Narrative prose, not a bulleted changelog. Written for someone deciding whether to install +or update: what's new, what got fixed, anything they should know. Full Markdown is fine. + +This is distinct from: +- **`CHANGELOG.md`** — cumulative, terse, issue-referenced, for developers. +- **`play/.txt`** — plain, ≤500 chars, for the store. +- **`discord/.md`** — casual community announcement. + +Same underlying changes, four different voices. The release gate +(`scripts/release-gate.sh`) fails the build if this file is missing or empty for the +version being tagged. diff --git a/test/connector/build_update_contact_path_frame_test.dart b/test/connector/build_update_contact_path_frame_test.dart index da88f9a..dd110d3 100644 --- a/test/connector/build_update_contact_path_frame_test.dart +++ b/test/connector/build_update_contact_path_frame_test.dart @@ -16,6 +16,59 @@ void main() { final pubKey = Uint8List.fromList(List.generate(32, (i) => i)); final path = Uint8List.fromList([0xAA, 0xBB]); + // Byte offset of path_len in the frame: 1 cmd + 32 pubKey + 1 type + 1 flags. + const int pathLenOffset = 35; + + group('buildUpdateContactPathFrame path_len encoding (#309)', () { + test('packs the hash width into the high 2 bits', () { + // One 2-byte hop must go out as 0x41, not a bare 1. Writing the count + // raw leaves mode bits 00, which tells the radio "1-byte hashes" while + // handing it 2-byte hash data, so it routes to nodes that were never on + // the route. That is the send-side half of #240's misrouting. + final frame = buildUpdateContactPathFrame( + pubKey, + Uint8List.fromList([0xC6, 0x5C]), + 1, + hashWidth: 2, + ); + expect(frame[pathLenOffset], 0x41); + expect(pathHopCount(frame[pathLenOffset]), 1); + expect(pathHashSizeBytes(frame[pathLenOffset]), 2); + }); + + test('two 2-byte hops encode as 0x42', () { + final frame = buildUpdateContactPathFrame( + pubKey, + Uint8List.fromList([0xC6, 0x5C, 0xA1, 0xB2]), + 2, + hashWidth: 2, + ); + expect(frame[pathLenOffset], 0x42); + expect(pathByteLength(frame[pathLenOffset]), 4); + }); + + test('1-byte width is byte-identical to the pre-fix encoding', () { + // Legacy 1-byte nets must see no change on the wire. + final frame = buildUpdateContactPathFrame( + pubKey, + Uint8List.fromList([0xAA, 0xBB, 0xCC]), + 3, + hashWidth: 1, + ); + expect(frame[pathLenOffset], 3); + }); + + test('negative hop count emits the 0xFF flood sentinel', () { + final frame = buildUpdateContactPathFrame( + pubKey, + Uint8List(0), + -1, + hashWidth: 2, + ); + expect(frame[pathLenOffset], 0xFF); + }); + }); + group('buildUpdateContactPathFrame', () { test('omits lat/lon and timestamp tail when neither is provided', () { final frame = buildUpdateContactPathFrame( diff --git a/test/connector/offband_fem_lna_test.dart b/test/connector/offband_fem_lna_test.dart new file mode 100644 index 0000000..df56d6a --- /dev/null +++ b/test/connector/offband_fem_lna_test.dart @@ -0,0 +1,134 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; + +void main() { + group('FEM LNA capability gate (#304)', () { + test('requires the explicit bit, never model or version', () { + expect(firmwareSupportsOffbandFemLna(offbandCapFemLna), isTrue); + expect( + firmwareSupportsOffbandFemLna(offbandCapFemLna | offbandCapBlock), + isTrue, + ); + }); + + test('false when the bit is clear, even on an Offband radio', () { + // Offband firmware that supports block but not FEM LNA control. + expect(firmwareSupportsOffbandFemLna(offbandCapBlock), isFalse); + expect(firmwareSupportsOffbandFemLna(0x00), isFalse); + }); + + test('false on stock firmware (no caps byte at all)', () { + expect(firmwareSupportsOffbandFemLna(null), isFalse); + }); + + test('does not collide with the block capability bit', () { + expect(offbandCapFemLna & offbandCapBlock, equals(0)); + }); + }); + + group('FEM LNA device-info state byte, offset 83 (#304)', () { + Uint8List deviceInfo({required int length, int femByte = 0}) { + final frame = Uint8List(length); + frame[0] = respCodeDeviceInfo; + if (length >= 84) frame[83] = femByte; + return frame; + } + + test('reads the byte immediately after caps on v16+', () { + expect( + MeshCoreConnector.parseFemLnaState(deviceInfo(length: 84, femByte: 1)), + isTrue, + ); + expect( + MeshCoreConnector.parseFemLnaState(deviceInfo(length: 84, femByte: 0)), + isFalse, + ); + }); + + test('null on pre-v16 firmware that stops at the caps byte', () { + expect( + MeshCoreConnector.parseFemLnaState(deviceInfo(length: 83)), + isNull, + ); + expect(MeshCoreConnector.parseFemLnaState(Uint8List(0)), isNull); + }); + + test('does not disturb the caps byte at offset 82', () { + final frame = deviceInfo(length: 84, femByte: 1); + frame[82] = offbandCapBlock | offbandCapFemLna; + expect(MeshCoreConnector.parseOffbandCaps(frame), equals(0x06)); + expect(MeshCoreConnector.parseFemLnaState(frame), isTrue); + }); + + test('byte is present on non-capable boards and reads as bypassed', () { + // Firmware appends it unconditionally, so presence indicates version, + // not capability — the cap bit is what gates the UI. + final frame = deviceInfo(length: 84, femByte: 0); + frame[82] = 0x00; + expect(MeshCoreConnector.parseFemLnaState(frame), isFalse); + expect( + firmwareSupportsOffbandFemLna( + MeshCoreConnector.parseOffbandCaps(frame), + ), + isFalse, + ); + }); + }); + + group('FEM LNA frames (#304)', () { + test('SET carries the enable value', () { + expect( + buildOffbandFemLnaSetFrame(true), + equals(Uint8List.fromList([0xC3, 0x01, 0x01])), + ); + expect( + buildOffbandFemLnaSetFrame(false), + equals(Uint8List.fromList([0xC3, 0x01, 0x00])), + ); + }); + + test('GET is a bare 2-byte request', () { + expect( + buildOffbandFemLnaGetFrame(), + equals(Uint8List.fromList([0xC3, 0x02])), + ); + }); + + test('reply parses sub-type and value', () { + final reply = parseOffbandFemLnaReply( + Uint8List.fromList([0xC3, 0x02, 0x01]), + ); + expect(reply, isNotNull); + expect(reply!.subType, equals(offbandFemLnaGet)); + expect(reply.enabled, isTrue); + + final bypassed = parseOffbandFemLnaReply( + Uint8List.fromList([0xC3, 0x02, 0x00]), + ); + expect(bypassed!.enabled, isFalse); + }); + + test('rejects a short frame instead of throwing', () { + expect(parseOffbandFemLnaReply(Uint8List.fromList([0xC3, 0x02])), isNull); + expect(parseOffbandFemLnaReply(Uint8List.fromList([])), isNull); + }); + + test('ignores frames belonging to another command', () { + // The generic error reply is [0x01][0x06] and is NOT 0xC3-prefixed, so it + // must never be mistaken for a FEM LNA reply. + expect( + parseOffbandFemLnaReply( + Uint8List.fromList([respCodeErr, errCodeIllegalArg]), + ), + isNull, + ); + expect( + parseOffbandFemLnaReply(Uint8List.fromList([0xC2, 0x01, 0x01])), + isNull, + ); + }); + }); +} diff --git a/test/connector/path_hash_test.dart b/test/connector/path_hash_test.dart index fbe3224..fe7c3e5 100644 --- a/test/connector/path_hash_test.dart +++ b/test/connector/path_hash_test.dart @@ -1,32 +1,91 @@ -// Path-hash width / hop-count decode (#112). The firmware path-length byte is a -// BYTE count of the hop-hash array, not a hop count, so at a 2-byte hash width a -// single 2-byte hop reports 2. realHopCount divides by the device width to get -// true hops; the Packet Path screen slices and matches at the device width so a -// 2-byte hash like 6A3D resolves to one repeater instead of two 1-byte hops. +// Path-len byte encode/decode (#309, supersedes #112/#222). +// +// The firmware path-len byte is PACKED and self-describing: +// high 2 bits = hash size - 1 (1..3 bytes per hop hash) +// low 6 bits = hash COUNT, i.e. the number of HOPS +// byte length = count * size +// +// Verified against firmware src/Packet.h:79-84: +// getPathHashSize() == (path_len >> 6) + 1 +// getPathHashCount() == path_len & 63 +// getPathByteLen() == getPathHashCount() * getPathHashSize() +// setPathHashSizeAndCount(sz, n) { path_len = ((sz - 1) << 6) | (n & 63); } +// +// This file previously asserted the opposite (low 6 bits = a BYTE count, and a +// realHopCount() that divided by the device width). Those assertions encoded the +// bug: the app read `count` bytes where firmware meant `count` hops, keeping +// half of every path at 2-byte width, and halved every displayed hop count. import 'package:flutter_test/flutter_test.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; void main() { - group('realHopCount', () { - test('divides path byte-length by hash width', () { - // The bug: one 2-byte hop (6A3D) reported byteLen 2 -> shown as "2 hops". - expect(realHopCount(2, 2), 1); - expect(realHopCount(4, 2), 2); - expect(realHopCount(6, 3), 2); + group('path-len decode', () { + test('low 6 bits are a HOP count, not a byte count', () { + // 0x42 = 2-byte hashes, 2 hops. The old model called this "2 bytes". + expect(pathHopCount(0x42), 2); + expect(pathHashSizeBytes(0x42), 2); + expect(pathByteLength(0x42), 4); // 2 hops * 2 bytes }); - test('is a no-op at 1-byte width (no regression for legacy nets)', () { - expect(realHopCount(3, 1), 3); + test('one 2-byte hop is 1 hop occupying 2 bytes', () { + // Bandit's C65C case: ONE hop through a 2-byte-prefixed repeater. + expect(pathHopCount(0x41), 1); + expect(pathHashSizeBytes(0x41), 2); + expect(pathByteLength(0x41), 2); }); - test('treats width < 1 as 1', () { - expect(realHopCount(3, 0), 3); + test('1-byte width is unchanged (legacy nets)', () { + expect(pathHopCount(0x03), 3); + expect(pathHashSizeBytes(0x03), 1); + expect(pathByteLength(0x03), 3); }); - test('passes null (unknown) and negative (flood) sentinels through', () { - expect(realHopCount(null, 2), isNull); - expect(realHopCount(-1, 2), -1); + test('3-byte width', () { + // 0x82 = size 3, 2 hops -> 6 bytes. + expect(pathHashSizeBytes(0x82), 3); + expect(pathHopCount(0x82), 2); + expect(pathByteLength(0x82), 6); + }); + + test('direct (zero-hop) at 2-byte mode carries no path bytes', () { + // 0x40 = size 2, 0 hops. Reading the raw byte as a length would have + // pulled 64 junk bytes into the path. + expect(pathHopCount(0x40), 0); + expect(pathByteLength(0x40), 0); + }); + }); + + group('encodePathLen', () { + test('packs width and hop count the way firmware does', () { + expect(encodePathLen(2, 2), 0x42); + expect(encodePathLen(1, 2), 0x41); + expect(encodePathLen(3, 1), 0x03); + expect(encodePathLen(2, 3), 0x82); + expect(encodePathLen(0, 2), 0x40); + }); + + test('round-trips through the decoders', () { + for (final width in [1, 2, 3]) { + for (final hops in [0, 1, 5, 63]) { + final encoded = encodePathLen(hops, width); + expect(pathHopCount(encoded), hops, reason: 'hops w=$width'); + expect(pathHashSizeBytes(encoded), width, reason: 'width w=$width'); + } + } + }); + + test('clamps width to the 1..3 firmware range (mode 3 is reserved)', () { + expect(pathHashSizeBytes(encodePathLen(1, 0)), 1); + expect(pathHashSizeBytes(encodePathLen(1, 9)), 3); + }); + + test('a bare hop count would mislabel the width as 1 byte', () { + // The send-side bug: writing the count raw leaves mode bits 00, telling + // the radio "1-byte hashes" while handing it 2-byte hash data. + const rawCount = 2; + expect(pathHashSizeBytes(rawCount), 1); // what the radio would have read + expect(pathHashSizeBytes(encodePathLen(2, 2)), 2); // what it must read }); }); } diff --git a/test/helpers/gif_media_url_test.dart b/test/helpers/gif_media_url_test.dart new file mode 100644 index 0000000..7ec3f5b --- /dev/null +++ b/test/helpers/gif_media_url_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/helpers/gif_helper.dart'; + +// #283: render pasted GIF/media URLs inline, but ONLY from an allowlist of +// curated hosts (Giphy + Tenor). Anything off-list must stay a plain link and +// must never be auto-fetched: auto-loading a stranger's URL leaks the viewer's +// IP and enables tracking-pixel / read-receipt abuse, and inline-rendering +// arbitrary hosts is a malware and inappropriate-content vector. +void main() { + const gifId = 'zaMiq1BvCdAVIiu3Mb'; + const giphyRender = 'https://media.giphy.com/media/$gifId/giphy.gif'; + + group('Giphy forms all resolve to a renderable URL', () { + test('the app payload (giphy page URL)', () { + expect( + GifHelper.resolveGifUrl('https://giphy.com/gifs/$gifId'), + giphyRender, + ); + }); + + test('legacy g:', () { + expect(GifHelper.resolveGifUrl('g:$gifId'), giphyRender); + }); + + test('media.giphy.com direct asset', () { + expect(GifHelper.resolveGifUrl(giphyRender), giphyRender); + }); + + test('i.giphy.com webp, the form users actually paste', () { + expect( + GifHelper.resolveGifUrl('https://i.giphy.com/$gifId.webp'), + giphyRender, + ); + }); + + test('i.giphy.com gif', () { + expect( + GifHelper.resolveGifUrl('https://i.giphy.com/$gifId.gif'), + giphyRender, + ); + }); + + test('a reply carrying a pasted Giphy URL', () { + expect( + GifHelper.resolveGifUrl('@[Some Node] https://i.giphy.com/$gifId.webp'), + giphyRender, + ); + }); + }); + + group('Tenor direct media resolves to itself', () { + test('media.tenor.com gif', () { + const url = 'https://media.tenor.com/abc123XYZ/happy-dance.gif'; + expect(GifHelper.resolveGifUrl(url), url); + }); + + test('c.tenor.com gif', () { + const url = 'https://c.tenor.com/abc123XYZ/tenor.gif'; + expect(GifHelper.resolveGifUrl(url), url); + }); + + test('media.tenor.com webp', () { + const url = 'https://media.tenor.com/abc123XYZ/thing.webp'; + expect(GifHelper.resolveGifUrl(url), url); + }); + }); + + group('off-allowlist URLs must NOT render (never auto-fetch)', () { + test('an arbitrary https image', () { + expect( + GifHelper.resolveGifUrl('https://evil.example.com/tracker.gif'), + isNull, + ); + }); + + test('a lookalike host is not trusted', () { + expect( + GifHelper.resolveGifUrl('https://giphy.com.evil.example/$gifId.gif'), + isNull, + ); + }); + + test('a general image host (imgur) is off-list', () { + expect(GifHelper.resolveGifUrl('https://i.imgur.com/abc123.gif'), isNull); + }); + + test('plain text is not a GIF', () { + expect(GifHelper.resolveGifUrl('hey are you there'), isNull); + }); + + test('a non-media URL is not a GIF', () { + expect(GifHelper.resolveGifUrl('https://example.com/page'), isNull); + }); + + test('a tenor PAGE url does not render (needs API resolution)', () { + // Modern Tenor CDN paths use an opaque hash not derivable from the page + // id, so a page URL cannot be resolved without the Tenor API. Stays a + // plain link rather than silently rendering the wrong thing. + expect( + GifHelper.resolveGifUrl('https://tenor.com/view/happy-dance-12345'), + isNull, + ); + }); + }); +} diff --git a/test/models/contact_pathlen_test.dart b/test/models/contact_pathlen_test.dart index c89af0c..a6323c6 100644 --- a/test/models/contact_pathlen_test.dart +++ b/test/models/contact_pathlen_test.dart @@ -46,18 +46,37 @@ void main() { expect(c.path, [0xAA, 0xBB, 0xCC]); }); - test('2-byte-mode multi-hop (0x44) decodes to 4 bytes, not flood', () { - // High bits = mode-1 hint, low bits = 4 bytes = a 2-hop path at 2-byte - // width. Pre-#222 the raw 68 (> maxPathSize) was mis-flagged as flood. + test('2-byte-mode 2-hop (0x42) keeps all FOUR bytes (#309)', () { + // 0x42 = size 2, count 2 -> 2 hops occupying 2*2 = 4 bytes. + // + // Pre-#309 the low 6 bits were read as a byte count, so this kept only + // the first 2 bytes and silently discarded the second hop. That is the + // truncation behind #240's failed repeater logins. final c = Contact.fromFrame( - _frame(pathLen: 0x44, pathBytes: [1, 2, 3, 4]), + _frame(pathLen: 0x42, pathBytes: [0xC6, 0x5C, 0xA1, 0xB2]), )!; - expect(c.pathLength, 4); - expect(c.path, [1, 2, 3, 4]); - expect( - realHopCount(c.pathLength, 2), - 2, - ); // 4 bytes / 2-byte width = 2 hops + expect(c.pathLength, 2); // HOPS, not bytes + expect(c.pathHashWidth, 2); + expect(c.path, [0xC6, 0x5C, 0xA1, 0xB2]); // all 4 bytes retained + }); + + test('2-byte-mode single hop (0x41) is 1 hop of 2 bytes (#309)', () { + // Bandit's C65C case: ONE hop, previously surfaced as "2 hops". + final c = Contact.fromFrame( + _frame(pathLen: 0x41, pathBytes: [0xC6, 0x5C]), + )!; + expect(c.pathLength, 1); + expect(c.pathHashWidth, 2); + expect(c.path, [0xC6, 0x5C]); + }); + + test('3-byte-mode 2-hop (0x82) keeps six bytes (#309)', () { + final c = Contact.fromFrame( + _frame(pathLen: 0x82, pathBytes: [1, 2, 3, 4, 5, 6]), + )!; + expect(c.pathLength, 2); + expect(c.pathHashWidth, 3); + expect(c.path, [1, 2, 3, 4, 5, 6]); }); test('flood sentinel (0xFF) stays -1, empty path', () { diff --git a/test/services/notification_text_test.dart b/test/services/notification_text_test.dart new file mode 100644 index 0000000..3da21ad --- /dev/null +++ b/test/services/notification_text_test.dart @@ -0,0 +1,96 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/helpers/gif_helper.dart'; +import 'package:meshcore_open/services/notification_service.dart'; + +// #284: notifications summarised a GIF as "Sent a GIF" using their own +// hand-rolled `^g:$` regex. #282 changed the payload to a Giphy URL, which +// that regex does not match, so notifications regressed to dumping the raw URL. +// Detection must go through GifHelper.parseGif so every payload form the app +// can actually send is summarised, and so the regex cannot drift again. +void main() { + const gifId = 'zaMiq1BvCdAVIiu3Mb'; + + group('GIF payloads are summarised, not dumped raw', () { + test('the current URL payload summarises', () { + expect( + NotificationService.formatNotificationText(GifHelper.encodeGif(gifId)), + 'Sent a GIF', + ); + }); + + test('a reply carrying a GIF summarises', () { + expect( + NotificationService.formatNotificationText( + '@[Some Node] ${GifHelper.encodeGif(gifId)}', + ), + 'Sent a GIF', + ); + }); + + test('a legacy g: payload still summarises', () { + expect( + NotificationService.formatNotificationText('g:$gifId'), + 'Sent a GIF', + ); + }); + + test('a legacy g: reply still summarises', () { + expect( + NotificationService.formatNotificationText('@[Some Node] g:$gifId'), + 'Sent a GIF', + ); + }); + }); + + group('everything else is untouched', () { + test('a reaction still summarises', () { + expect( + NotificationService.formatNotificationText('r:abcd:00'), + startsWith('Reacted '), + ); + }); + + test('ordinary text passes through unchanged', () { + expect( + NotificationService.formatNotificationText('hey are you there'), + 'hey are you there', + ); + }); + + test('a non-GIF URL is not mistaken for a GIF', () { + expect( + NotificationService.formatNotificationText('https://example.com/page'), + 'https://example.com/page', + ); + }); + }); + + group('allowlisted media URLs summarise too (#283 consistency)', () { + test('a pasted i.giphy.com link summarises', () { + expect( + NotificationService.formatNotificationText( + 'https://i.giphy.com/$gifId.webp', + ), + 'Sent a GIF', + ); + }); + + test('a pasted Tenor CDN link summarises', () { + expect( + NotificationService.formatNotificationText( + 'https://media.tenor.com/abc123XYZ/happy-dance.gif', + ), + 'Sent a GIF', + ); + }); + + test('an off-allowlist image URL does NOT summarise', () { + expect( + NotificationService.formatNotificationText( + 'https://evil.example.com/tracker.gif', + ), + 'https://evil.example.com/tracker.gif', + ); + }); + }); +} diff --git a/test/storage/blob_store_migration_test.dart b/test/storage/blob_store_migration_test.dart new file mode 100644 index 0000000..d869314 --- /dev/null +++ b/test/storage/blob_store_migration_test.dart @@ -0,0 +1,178 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/storage/drift/blob_store.dart'; +import 'package:meshcore_open/storage/drift/offband_database.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Migration safety for #335. +/// +/// The bar here is set by #333: a storage path chose a key silently and 566 +/// real messages read as empty. A migration that gets this wrong loses data +/// permanently rather than cosmetically, so the failure paths are tested, not +/// just the happy one. +void main() { + late OffbandDatabase db; + late BlobStore store; + + setUp(() async { + db = OffbandDatabase(NativeDatabase.memory()); + store = BlobStore(db); + PrefsManager.reset(); + }); + + tearDown(() => db.close()); + + Future seedPrefs(Map values) async { + SharedPreferences.setMockInitialValues(values); + await PrefsManager.initialize(); + } + + test('moves bulk keys and removes them from prefs', () async { + await seedPrefs({ + 'channel_messages_devpsk_abc': '[{"m":1}]', + 'messages_devcontact': '[{"m":2}]', + 'contacts_dev': '[{"c":1}]', + 'discovered_contacts': '[{"d":1}]', + }); + + final report = await store.migrateFromPrefs(); + + expect(report.migrated, 4); + expect(report.failed, 0); + expect(await store.read('channel_messages_devpsk_abc'), '[{"m":1}]'); + expect(await store.read('discovered_contacts'), '[{"d":1}]'); + + final prefs = PrefsManager.instance; + expect(prefs.getString('channel_messages_devpsk_abc'), isNull); + expect(prefs.getString('discovered_contacts'), isNull); + }); + + test('leaves settings alone', () async { + await seedPrefs({ + 'ui_channels_sort_option': 'manual', + 'app_settings': '{"theme":"dark"}', + 'contacts_dev': '[{"c":1}]', + }); + + final report = await store.migrateFromPrefs(); + + expect(report.migrated, 1, reason: 'only the bulk key should move'); + final prefs = PrefsManager.instance; + expect(prefs.getString('ui_channels_sort_option'), 'manual'); + expect(prefs.getString('app_settings'), '{"theme":"dark"}'); + }); + + test('is idempotent: a second run moves nothing and loses nothing', () async { + await seedPrefs({'contacts_dev': '[{"c":1}]'}); + + final first = await store.migrateFromPrefs(); + expect(first.migrated, 1); + + final second = await store.migrateFromPrefs(); + expect(second.migrated, 0); + expect(second.failed, 0); + expect(await store.read('contacts_dev'), '[{"c":1}]'); + }); + + test( + '#355: prefs messages absent from drift are merged, not discarded', + () async { + // The bug: an in-between build (non-drift, or any build that writes prefs) + // accumulates NEW messages in prefs. On the next drift run the key is + // "already present", so the old code discarded the prefs copy and the new + // messages were gapped out of history. They must be UNIONED in instead. + await store.write( + 'channel_messages_devpsk_abc', + '[{"messageId":"a"},{"messageId":"b"}]', + ); + await seedPrefs({ + 'channel_messages_devpsk_abc': + '[{"messageId":"a"},{"messageId":"c"},{"messageId":"d"}]', + }); + + final report = await store.migrateFromPrefs(); + + expect(report.merged, 1); + expect(report.failed, 0); + final ids = (await store.read('channel_messages_devpsk_abc'))!; + // Drift's a,b kept; prefs-only c,d appended; shared a not duplicated. + expect( + ids, + '[{"messageId":"a"},{"messageId":"b"},' + '{"messageId":"c"},{"messageId":"d"}]', + ); + expect( + PrefsManager.instance.getString('channel_messages_devpsk_abc'), + isNull, + ); + }, + ); + + test( + 'merge keeps the drift copy of a shared entity, adds prefs-only ones', + () async { + // Contacts collide by publicKey: the live drift copy wins for a shared key, + // and a contact seen only on the in-between build is still added. + await store.write('contacts_dev', '[{"publicKey":"A","name":"drift"}]'); + await seedPrefs({ + 'contacts_dev': + '[{"publicKey":"A","name":"stale"},{"publicKey":"B","name":"new"}]', + }); + + final report = await store.migrateFromPrefs(); + + expect(report.merged, 1); + expect( + await store.read('contacts_dev'), + '[{"publicKey":"A","name":"drift"},{"publicKey":"B","name":"new"}]', + ); + expect(PrefsManager.instance.getString('contacts_dev'), isNull); + }, + ); + + test( + 'already-present with nothing new to add reports alreadyPresent', + () async { + // Prefs is a subset of drift: union changes nothing, and the stale prefs + // copy is dropped without a needless rewrite. + await store.write('contacts_dev', '[{"publicKey":"A"}]'); + await seedPrefs({'contacts_dev': '[{"publicKey":"A"}]'}); + + final report = await store.migrateFromPrefs(); + + expect(report.alreadyPresent, 1); + expect(report.merged, 0); + expect(await store.read('contacts_dev'), '[{"publicKey":"A"}]'); + expect(PrefsManager.instance.getString('contacts_dev'), isNull); + }, + ); + + test('preserves a payload larger than the 5 MiB localStorage cap', () async { + // 4 chars per element, so >1.4M elements clears 5 MiB. + final big = '[${'"x",' * 1400000}"end"]'; + expect(big.length, greaterThan(5 * 1024 * 1024)); + await seedPrefs({'channel_messages_devpsk_big': big}); + + final report = await store.migrateFromPrefs(); + + expect(report.failed, 0); + expect( + (await store.read('channel_messages_devpsk_big'))!.length, + big.length, + ); + }); + + test('non-string values are skipped, not failed', () async { + // getString THROWS on a non-string, so this must be type-checked, not + // caught as a failure. Verified against the real store: 'contacts' only + // ever matches bulk blobs there, but the store must not mis-report if a + // non-string ever lands under a bulk prefix. + await seedPrefs({'contacts_probe': 42}); + + final report = await store.migrateFromPrefs(); + + expect(report.failed, 0); + expect(report.skipped, 1); + }); +} diff --git a/test/storage/channel_message_merge_test.dart b/test/storage/channel_message_merge_test.dart new file mode 100644 index 0000000..3cba6e2 --- /dev/null +++ b/test/storage/channel_message_merge_test.dart @@ -0,0 +1,122 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/models/channel_message.dart'; +import 'package:meshcore_open/storage/channel_message_store.dart'; +import 'package:meshcore_open/storage/drift/blob_store.dart'; +import 'package:meshcore_open/storage/drift/offband_database.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// #343: saving a windowed in-memory list must not truncate the persisted +/// history. The store keeps full history; deletion is explicit. +void main() { + late OffbandDatabase db; + late ChannelMessageStore store; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + PrefsManager.reset(); + await PrefsManager.initialize(); + db = OffbandDatabase(NativeDatabase.memory()); + BlobStore.overrideForTest(BlobStore(db)); + store = ChannelMessageStore(); + store.setPublicKeyHex = 'a' * 20; + // No PSK resolver -> uses the slot-index key, which is fine for the test. + }); + + tearDown(() async { + await db.close(); + BlobStore.clearTestOverride(); + }); + + ChannelMessage msg(int i) => ChannelMessage( + senderName: 's', + text: 'm$i', + timestamp: DateTime.fromMillisecondsSinceEpoch(1000 + i), + isOutgoing: false, + channelIndex: 0, + messageId: 'id$i', + ); + + test('saving a 200-window does not truncate a 250-message history', () async { + // Seed 250 messages, as if a full history were persisted. + await store.saveChannelMessages(0, [for (var i = 0; i < 250; i++) msg(i)]); + expect((await store.loadChannelMessages(0)).length, 250); + + // The app now saves only the most-recent 200 (its in-memory window). + final window = [for (var i = 50; i < 250; i++) msg(i)]; + await store.saveChannelMessages(0, window); + + // Full history must survive: the older 50 are still there. + final all = await store.loadChannelMessages(0); + expect(all.length, 250, reason: 'the older 50 must not be dropped'); + expect(all.first.text, 'm0'); + expect(all.last.text, 'm249'); + }); + + test('a new message appends without dropping old history', () async { + await store.saveChannelMessages(0, [for (var i = 0; i < 10; i++) msg(i)]); + await store.saveChannelMessages(0, [msg(10)]); + final all = await store.loadChannelMessages(0); + expect(all.length, 11); + expect(all.last.text, 'm10'); + }); + + test('an edit to a message is captured, not duplicated', () async { + await store.saveChannelMessages(0, [msg(1)]); + final edited = ChannelMessage( + senderName: 's', + text: 'm1', + timestamp: DateTime.fromMillisecondsSinceEpoch(1001), + isOutgoing: false, + channelIndex: 0, + messageId: 'id1', + reactions: {'thumbsup': 2}, + ); + await store.saveChannelMessages(0, [edited]); + final all = await store.loadChannelMessages(0); + expect(all, hasLength(1)); + expect(all.single.reactions['thumbsup'], 2); + }); + + test( + 'concurrent saves to one channel do not lose messages (Gemini)', + () async { + await store.saveChannelMessages(0, [msg(0)]); + // Fire two saves without awaiting between them: they race on the same key. + // Serialisation must make the result the union, not last-writer-wins. + final a = store.saveChannelMessages(0, [msg(1)]); + final b = store.saveChannelMessages(0, [msg(2)]); + await Future.wait([a, b]); + final all = await store.loadChannelMessages(0); + expect( + all.map((m) => m.text), + containsAll(['m0', 'm1', 'm2']), + reason: 'neither concurrent save may clobber the other', + ); + expect(all, hasLength(3)); + }, + ); + + test('deleting a message does NOT resurrect it on the next save', () async { + await store.saveChannelMessages(0, [for (var i = 0; i < 5; i++) msg(i)]); + + // Delete via the explicit path. + await store.removeChannelMessage(0, msg(2)); + expect( + (await store.loadChannelMessages(0)).map((m) => m.text), + isNot(contains('m2')), + ); + + // A later save of the remaining in-memory list must not bring it back. + final remaining = [ + for (var i = 0; i < 5; i++) + if (i != 2) msg(i), + ]; + await store.saveChannelMessages(0, remaining); + + final all = await store.loadChannelMessages(0); + expect(all.map((m) => m.text), isNot(contains('m2'))); + expect(all, hasLength(4)); + }); +} diff --git a/test/storage/drift_asset_version_test.dart b/test/storage/drift_asset_version_test.dart new file mode 100644 index 0000000..e917fe1 --- /dev/null +++ b/test/storage/drift_asset_version_test.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// Guards the one way the web build can break silently (#335). +/// +/// drift on the web needs two binaries shipped in `web/`, and they must match +/// the resolved `drift` package. If someone bumps drift without re-downloading +/// them, nothing fails at build time: the app compiles, deploys, and then +/// fails in the browser. SAFELANE 6 says that must not be silent, so this +/// turns it into a red test with instructions. +/// +/// Update `web/drift_assets.version` whenever the assets are re-downloaded. +void main() { + test('shipped drift web assets match the pinned drift version', () { + final root = Directory.current.path; + + final lock = File('$root/pubspec.lock'); + expect( + lock.existsSync(), + isTrue, + reason: + 'pubspec.lock must be committed so dependency resolution is ' + 'reproducible; the drift web assets are matched against it.', + ); + + // Normalise line endings first: pubspec.lock is CRLF on Windows and LF in + // CI, and a newline-sensitive pattern would pass on one and fail on the + // other. A guard that is itself platform-fragile is worse than none. + final lockText = lock.readAsStringSync().replaceAll('\r\n', '\n'); + final resolved = RegExp( + r'^ drift:\n(?:.*\n)*? version: "([^"]+)"', + multiLine: true, + ).firstMatch(lockText)?.group(1); + expect(resolved, isNotNull, reason: 'drift not found in pubspec.lock'); + + final stamp = File('$root/web/drift_assets.version'); + expect( + stamp.existsSync(), + isTrue, + reason: + 'web/drift_assets.version is missing. It records which drift release ' + 'web/sqlite3.wasm and web/drift_worker.js came from.', + ); + + expect( + stamp.readAsStringSync().trim(), + resolved, + reason: + '\nDrift web assets are STALE.\n' + 'pubspec.lock resolves drift $resolved, but the shipped assets are ' + 'from ${stamp.readAsStringSync().trim()}.\n' + 'The web build would compile and then fail in the browser.\n\n' + 'Fix:\n' + ' gh release download drift-$resolved --repo simolus3/drift \\\n' + ' --pattern drift_worker.js --pattern sqlite3.wasm --clobber\n' + ' (run inside web/, then write $resolved into web/drift_assets.version)\n', + ); + + for (final name in ['sqlite3.wasm', 'drift_worker.js']) { + expect( + File('$root/web/$name').existsSync(), + isTrue, + reason: 'web/$name is missing; the web build would fail at runtime.', + ); + } + + // Cheap integrity check: the wasm must actually be WebAssembly. + final magic = File('$root/web/sqlite3.wasm').openSync().readSync(4); + expect( + magic, + orderedEquals([0x00, 0x61, 0x73, 0x6d]), + reason: + 'web/sqlite3.wasm does not start with the WebAssembly magic ' + 'bytes (\\0asm) — the download is corrupt or is not a wasm file.', + ); + }); +} diff --git a/test/storage/offband_database_test.dart b/test/storage/offband_database_test.dart new file mode 100644 index 0000000..4e750bc --- /dev/null +++ b/test/storage/offband_database_test.dart @@ -0,0 +1,56 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/storage/drift/offband_database.dart'; + +/// Step-1 gate for #335: the database must open, write and read back before +/// any user data is migrated into it. +/// +/// This runs against an in-memory SQLite instance so it exercises the drift +/// layer and generated code on any host. It does NOT prove the per-platform +/// backends (native libs on desktop/mobile, WASM on web) — those are proven by +/// building each target and by `verifyReadWrite()` at runtime. +void main() { + late OffbandDatabase db; + + setUp(() => db = OffbandDatabase(NativeDatabase.memory())); + tearDown(() => db.close()); + + test('opens, writes and reads back (the #335 gate)', () async { + expect(await db.verifyReadWrite(), isTrue); + }); + + test('round-trips a payload larger than the 5 MiB localStorage cap', () async { + // The web build currently stores through localStorage, capped at 5 MiB per + // origin, while this install already holds ~7 MB. Proving a >5 MiB value + // survives is the whole point of the migration. + final big = 'x' * (6 * 1024 * 1024); + await db + .into(db.storedBlobs) + .insertOnConflictUpdate( + StoredBlobsCompanion.insert(key: 'big', value: big), + ); + + final row = await (db.select( + db.storedBlobs, + )..where((t) => t.key.equals('big'))).getSingle(); + + expect(row.value.length, big.length); + }); + + test('upsert replaces rather than duplicating a key', () async { + for (final v in ['first', 'second']) { + await db + .into(db.storedBlobs) + .insertOnConflictUpdate( + StoredBlobsCompanion.insert(key: 'k', value: v), + ); + } + + final rows = await (db.select( + db.storedBlobs, + )..where((t) => t.key.equals('k'))).get(); + + expect(rows, hasLength(1)); + expect(rows.single.value, 'second'); + }); +} diff --git a/test/storage/real_store_rehearsal_test.dart b/test/storage/real_store_rehearsal_test.dart new file mode 100644 index 0000000..678aab8 --- /dev/null +++ b/test/storage/real_store_rehearsal_test.dart @@ -0,0 +1,91 @@ +@Tags(['rehearsal']) +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/storage/drift/blob_store.dart'; +import 'package:meshcore_open/storage/drift/offband_database.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Rehearses the #335 migration against a COPY of a real 7 MB store. +/// +/// Required by the plan before the migration may touch live data. Reads a +/// snapshot from disk; it never opens the user's actual store. Skipped when +/// the snapshot is absent, so CI does not depend on one machine's data. +void main() { + final path = Platform.environment['OFFBAND_REAL_STORE']; + + test( + 'migrates a real 7 MB store with zero loss', + () async { + if (path == null || !File(path).existsSync()) { + markTestSkipped( + 'set OFFBAND_REAL_STORE to a shared_preferences.json copy', + ); + return; + } + + final raw = + jsonDecode(File(path).readAsStringSync()) as Map; + // Strip the `flutter.` prefix the plugin adds on disk. + final seed = { + for (final e in raw.entries) + if (e.value is String || e.value is int || e.value is bool) + e.key.replaceFirst('flutter.', ''): e.value as Object, + }; + + final expected = { + for (final e in seed.entries) + if (e.value is String && BlobStore.isBulkKey(e.key)) + e.key: (e.value as String).length, + }; + + SharedPreferences.setMockInitialValues(seed); + PrefsManager.reset(); + await PrefsManager.initialize(); + + final db = OffbandDatabase(NativeDatabase.memory()); + final store = BlobStore(db); + final report = await store.migrateFromPrefs(); + + // ignore: avoid_print + print( + 'REHEARSAL: ${report.migrated} migrated, ${report.failed} failed, ' + '${(report.bytes / 1024 / 1024).toStringAsFixed(2)} MB, ' + '${expected.length} bulk keys expected', + ); + + expect(report.failed, 0, reason: 'no key may fail on real data'); + expect(report.migrated, expected.length); + + // Every byte accounted for, per key. + for (final e in expected.entries) { + final got = await store.read(e.key); + expect(got, isNotNull, reason: '${e.key} missing after migration'); + expect(got!.length, e.value, reason: '${e.key} changed length'); + expect( + PrefsManager.instance.get(e.key), + isNull, + reason: '${e.key} should be gone from prefs', + ); + } + + // Settings must survive untouched. + final settings = seed.keys.where((k) => !BlobStore.isBulkKey(k)); + for (final k in settings) { + expect( + PrefsManager.instance.get(k), + isNotNull, + reason: 'setting $k was wrongly removed', + ); + } + + await db.close(); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); +} diff --git a/untranslated.json b/untranslated.json index 56be62e..c6bcee5 100644 --- a/untranslated.json +++ b/untranslated.json @@ -14,11 +14,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -38,6 +42,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -72,11 +77,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -96,6 +105,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -130,11 +140,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -154,6 +168,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -188,11 +203,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -212,6 +231,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -246,11 +266,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -270,6 +294,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -304,11 +329,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -328,6 +357,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -362,11 +392,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -386,6 +420,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -420,11 +455,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -444,6 +483,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -478,11 +518,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -502,6 +546,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -536,11 +581,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -560,6 +609,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -594,11 +644,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -618,6 +672,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -652,11 +707,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -676,6 +735,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -710,11 +770,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -734,6 +798,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -768,11 +833,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -792,6 +861,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -826,11 +896,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -850,6 +924,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -884,11 +959,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -908,6 +987,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", @@ -942,11 +1022,15 @@ "settings_gpsStatusNoResponse", "settings_infoFirmware", "settings_infoModel", + "settings_infoOffbandCaps", "settings_publicKeyCopied", + "settings_femLna", + "settings_femLnaSubtitle", "appSettings_clock", "appSettings_clockSystem", "appSettings_clock12h", "appSettings_clock24h", + "contacts_searchSensors", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -966,6 +1050,7 @@ "channels_qrAddConfirm", "channels_qrAdded", "channels_qrExists", + "listFilter_sensors", "contacts_repeaterPathTraceVia", "snrIndicator_noNeighbors", "block_block", diff --git a/web/_headers b/web/_headers new file mode 100644 index 0000000..e4f84b9 --- /dev/null +++ b/web/_headers @@ -0,0 +1,28 @@ +# Cloudflare Pages response headers (#335, web storage via drift/SQLite-WASM). +# +# Syntax: a URL pattern, then indented headers. Cloudflare Pages reads this +# file from the build output and can add, override or remove response headers. +# +# Two things drift needs on the web: +# +# 1. sqlite3.wasm MUST be served as Content-Type: application/wasm, or +# WebAssembly streaming compilation refuses it. +# +# 2. COOP + COEP (cross-origin isolation) unlock drift's best storage backend, +# OPFS (`opfsLocks`). Verified locally: with these headers drift logs +# "Using WasmStorageImplementation.opfsLocks". WITHOUT them it still works, +# falling back to IndexedDB, which is slower but still unbounded compared to +# localStorage's 5 MiB cap. So these are a performance tier, not a +# correctness requirement. +# +# Verify after the first deploy: +# curl -sI https:///sqlite3.wasm | grep -i 'content-type' +# -> expect: content-type: application/wasm +# and check the browser console for the opfsLocks line above. + +/sqlite3.wasm + Content-Type: application/wasm + +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp diff --git a/web/drift_assets.version b/web/drift_assets.version new file mode 100644 index 0000000..b0c0861 --- /dev/null +++ b/web/drift_assets.version @@ -0,0 +1 @@ +2.34.2 diff --git a/web/drift_worker.js b/web/drift_worker.js new file mode 100644 index 0000000..a1978e2 --- /dev/null +++ b/web/drift_worker.js @@ -0,0 +1,13377 @@ +(function dartProgram(){function copyProperties(a,b){var s=Object.keys(a) +for(var r=0;r=0)return true +if(typeof version=="function"&&version.length==0){var q=version() +if(/^\d+\.\d+\.\d+\.\d+$/.test(q))return true}}catch(p){}return false}() +function inherit(a,b){a.prototype.constructor=a +a.prototype["$i"+a.name]=a +if(b!=null){if(z){Object.setPrototypeOf(a.prototype,b.prototype) +return}var s=Object.create(b.prototype) +copyProperties(a.prototype,s) +a.prototype=s}}function inheritMany(a,b){for(var s=0;s4294967295)throw A.b(A.W(a,0,4294967295,"length",null)) +return J.ue(new Array(a),b)}, +pR(a,b){if(a<0)throw A.b(A.J("Length must be a non-negative integer: "+a,null)) +return A.f(new Array(a),b.h("u<0>"))}, +ue(a,b){var s=A.f(a,b.h("u<0>")) +s.$flags=1 +return s}, +uf(a,b){return J.tE(a,b)}, +pS(a){if(a<256)switch(a){case 9:case 10:case 11:case 12:case 13:case 32:case 133:case 160:return!0 +default:return!1}switch(a){case 5760:case 8192:case 8193:case 8194:case 8195:case 8196:case 8197:case 8198:case 8199:case 8200:case 8201:case 8202:case 8232:case 8233:case 8239:case 8287:case 12288:case 65279:return!0 +default:return!1}}, +ug(a,b){var s,r +for(s=a.length;b0;b=s){s=b-1 +r=a.charCodeAt(s) +if(r!==32&&r!==13&&!J.pS(r))break}return b}, +cX(a){if(typeof a=="number"){if(Math.floor(a)==a)return J.es.prototype +return J.hl.prototype}if(typeof a=="string")return J.bX.prototype +if(a==null)return J.et.prototype +if(typeof a=="boolean")return J.hk.prototype +if(Array.isArray(a))return J.u.prototype +if(typeof a!="object"){if(typeof a=="function")return J.bz.prototype +if(typeof a=="symbol")return J.d9.prototype +if(typeof a=="bigint")return J.aN.prototype +return a}if(a instanceof A.e)return a +return J.nQ(a)}, +a3(a){if(typeof a=="string")return J.bX.prototype +if(a==null)return a +if(Array.isArray(a))return J.u.prototype +if(typeof a!="object"){if(typeof a=="function")return J.bz.prototype +if(typeof a=="symbol")return J.d9.prototype +if(typeof a=="bigint")return J.aN.prototype +return a}if(a instanceof A.e)return a +return J.nQ(a)}, +aT(a){if(a==null)return a +if(Array.isArray(a))return J.u.prototype +if(typeof a!="object"){if(typeof a=="function")return J.bz.prototype +if(typeof a=="symbol")return J.d9.prototype +if(typeof a=="bigint")return J.aN.prototype +return a}if(a instanceof A.e)return a +return J.nQ(a)}, +xg(a){if(typeof a=="number")return J.d8.prototype +if(typeof a=="string")return J.bX.prototype +if(a==null)return a +if(!(a instanceof A.e))return J.cF.prototype +return a}, +nP(a){if(typeof a=="string")return J.bX.prototype +if(a==null)return a +if(!(a instanceof A.e))return J.cF.prototype +return a}, +rB(a){if(a==null)return a +if(typeof a!="object"){if(typeof a=="function")return J.bz.prototype +if(typeof a=="symbol")return J.d9.prototype +if(typeof a=="bigint")return J.aN.prototype +return a}if(a instanceof A.e)return a +return J.nQ(a)}, +am(a,b){if(a==null)return b==null +if(typeof a!="object")return b!=null&&a===b +return J.cX(a).T(a,b)}, +aM(a,b){if(typeof b==="number")if(Array.isArray(a)||typeof a=="string"||A.rF(a,a[v.dispatchPropertyName]))if(b>>>0===b&&b>>0===b&&b").G(c).h("f1<1,2>")) +return new A.cp(a,b.h("@<0>").G(c).h("cp<1,2>"))}, +pT(a){return new A.da("Field '"+a+"' has been assigned during initialization.")}, +pU(a){return new A.da("Field '"+a+"' has not been initialized.")}, +ui(a){return new A.da("Field '"+a+"' has already been initialized.")}, +nR(a){var s,r=a^48 +if(r<=9)return r +s=a|32 +if(97<=s&&s<=102)return s-87 +return-1}, +c9(a,b){a=a+b&536870911 +a=a+((a&524287)<<10)&536870911 +return a^a>>>6}, +oy(a){a=a+((a&67108863)<<3)&536870911 +a^=a>>>11 +return a+((a&16383)<<15)&536870911}, +cV(a,b,c){return a}, +p9(a){var s,r +for(s=$.cU.length,r=0;rc)A.E(A.W(b,0,c,"start",null))}return new A.cD(a,b,c,d.h("cD<0>"))}, +ht(a,b,c,d){if(t.Q.b(a))return new A.cu(a,b,c.h("@<0>").G(d).h("cu<1,2>")) +return new A.aG(a,b,c.h("@<0>").G(d).h("aG<1,2>"))}, +oz(a,b,c){var s="takeCount" +A.bT(b,s) +A.ab(b,s) +if(t.Q.b(a))return new A.ek(a,b,c.h("ek<0>")) +return new A.cE(a,b,c.h("cE<0>"))}, +qe(a,b,c){var s="count" +if(t.Q.b(a)){A.bT(b,s) +A.ab(b,s) +return new A.d5(a,b,c.h("d5<0>"))}A.bT(b,s) +A.ab(b,s) +return new A.bJ(a,b,c.h("bJ<0>"))}, +uc(a,b,c){return new A.ct(a,b,c.h("ct<0>"))}, +aw(){return new A.aI("No element")}, +pP(){return new A.aI("Too few elements")}, +ce:function ce(){}, +fU:function fU(a,b){this.a=a +this.$ti=b}, +cp:function cp(a,b){this.a=a +this.$ti=b}, +f1:function f1(a,b){this.a=a +this.$ti=b}, +eW:function eW(){}, +ai:function ai(a,b){this.a=a +this.$ti=b}, +da:function da(a){this.a=a}, +fV:function fV(a){this.a=a}, +nY:function nY(){}, +kN:function kN(){}, +q:function q(){}, +Q:function Q(){}, +cD:function cD(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.$ti=d}, +b4:function b4(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +aG:function aG(a,b,c){this.a=a +this.b=b +this.$ti=c}, +cu:function cu(a,b,c){this.a=a +this.b=b +this.$ti=c}, +dc:function dc(a,b,c){var _=this +_.a=null +_.b=a +_.c=b +_.$ti=c}, +D:function D(a,b,c){this.a=a +this.b=b +this.$ti=c}, +aK:function aK(a,b,c){this.a=a +this.b=b +this.$ti=c}, +cG:function cG(a,b){this.a=a +this.b=b}, +em:function em(a,b,c){this.a=a +this.b=b +this.$ti=c}, +ha:function ha(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=null +_.$ti=d}, +cE:function cE(a,b,c){this.a=a +this.b=b +this.$ti=c}, +ek:function ek(a,b,c){this.a=a +this.b=b +this.$ti=c}, +hR:function hR(a,b,c){this.a=a +this.b=b +this.$ti=c}, +bJ:function bJ(a,b,c){this.a=a +this.b=b +this.$ti=c}, +d5:function d5(a,b,c){this.a=a +this.b=b +this.$ti=c}, +hM:function hM(a,b){this.a=a +this.b=b}, +eI:function eI(a,b,c){this.a=a +this.b=b +this.$ti=c}, +hN:function hN(a,b){this.a=a +this.b=b +this.c=!1}, +cv:function cv(a){this.$ti=a}, +h7:function h7(){}, +eR:function eR(a,b){this.a=a +this.$ti=b}, +i8:function i8(a,b){this.a=a +this.$ti=b}, +by:function by(a,b,c){this.a=a +this.b=b +this.$ti=c}, +ct:function ct(a,b,c){this.a=a +this.b=b +this.$ti=c}, +eq:function eq(a,b){this.a=a +this.b=b +this.c=-1}, +en:function en(){}, +hV:function hV(){}, +du:function du(){}, +eG:function eG(a,b){this.a=a +this.$ti=b}, +hQ:function hQ(a){this.a=a}, +fA:function fA(){}, +rO(a){var s=v.mangledGlobalNames[a] +if(s!=null)return s +return"minified:"+a}, +rF(a,b){var s +if(b!=null){s=b.x +if(s!=null)return s}return t.aU.b(a)}, +t(a){var s +if(typeof a=="string")return a +if(typeof a=="number"){if(a!==0)return""+a}else if(!0===a)return"true" +else if(!1===a)return"false" +else if(a==null)return"null" +s=J.b1(a) +return s}, +eE(a){var s,r=$.pZ +if(r==null)r=$.pZ=Symbol("identityHashCode") +s=a[r] +if(s==null){s=Math.random()*0x3fffffff|0 +a[r]=s}return s}, +q5(a,b){var s,r,q,p,o,n=null,m=/^\s*[+-]?((0x[a-f0-9]+)|(\d+)|([a-z0-9]+))\s*$/i.exec(a) +if(m==null)return n +s=m[3] +if(b==null){if(s!=null)return parseInt(a,10) +if(m[2]!=null)return parseInt(a,16) +return n}if(b<2||b>36)throw A.b(A.W(b,2,36,"radix",n)) +if(b===10&&s!=null)return parseInt(a,10) +if(b<10||s==null){r=b<=10?47+b:86+b +q=m[1] +for(p=q.length,o=0;or)return n}return parseInt(a,b)}, +hH(a){var s,r,q,p +if(a instanceof A.e)return A.aZ(A.aU(a),null) +s=J.cX(a) +if(s===B.as||s===B.av||t.ak.b(a)){r=B.H(a) +if(r!=="Object"&&r!=="")return r +q=a.constructor +if(typeof q=="function"){p=q.name +if(typeof p=="string"&&p!=="Object"&&p!=="")return p}}return A.aZ(A.aU(a),null)}, +q6(a){var s,r,q +if(a==null||typeof a=="number"||A.bQ(a))return J.b1(a) +if(typeof a=="string")return JSON.stringify(a) +if(a instanceof A.cq)return a.i(0) +if(a instanceof A.fi)return a.fP(!0) +s=$.tr() +for(r=0;r<1;++r){q=s[r].ll(a) +if(q!=null)return q}return"Instance of '"+A.hH(a)+"'"}, +us(){if(!!self.location)return self.location.href +return null}, +pY(a){var s,r,q,p,o=a.length +if(o<=500)return String.fromCharCode.apply(null,a) +for(s="",r=0;r65535)return A.uw(a)}return A.pY(a)}, +ux(a,b,c){var s,r,q,p +if(c<=500&&b===0&&c===a.length)return String.fromCharCode.apply(null,a) +for(s=b,r="";s>>0,s&1023|56320)}}throw A.b(A.W(a,0,1114111,null,null))}, +aH(a){if(a.date===void 0)a.date=new Date(a.a) +return a.date}, +q4(a){return a.c?A.aH(a).getUTCFullYear()+0:A.aH(a).getFullYear()+0}, +q2(a){return a.c?A.aH(a).getUTCMonth()+1:A.aH(a).getMonth()+1}, +q_(a){return a.c?A.aH(a).getUTCDate()+0:A.aH(a).getDate()+0}, +q0(a){return a.c?A.aH(a).getUTCHours()+0:A.aH(a).getHours()+0}, +q1(a){return a.c?A.aH(a).getUTCMinutes()+0:A.aH(a).getMinutes()+0}, +q3(a){return a.c?A.aH(a).getUTCSeconds()+0:A.aH(a).getSeconds()+0}, +uu(a){return a.c?A.aH(a).getUTCMilliseconds()+0:A.aH(a).getMilliseconds()+0}, +uv(a){return B.b.ab((a.c?A.aH(a).getUTCDay()+0:A.aH(a).getDay()+0)+6,7)+1}, +ut(a){var s=a.$thrownJsError +if(s==null)return null +return A.a5(s)}, +eF(a,b){var s +if(a.$thrownJsError==null){s=new Error() +A.aa(a,s) +a.$thrownJsError=s +s.stack=b.i(0)}}, +iX(a,b){var s,r="index" +if(!A.bv(b))return new A.bc(!0,b,r,null) +s=J.aC(a) +if(b<0||b>=s)return A.hf(b,s,a,null,r) +return A.kJ(b,r)}, +xa(a,b,c){if(a>c)return A.W(a,0,c,"start",null) +if(b!=null)if(bc)return A.W(b,a,c,"end",null) +return new A.bc(!0,b,"end",null)}, +e2(a){return new A.bc(!0,a,null,null)}, +b(a){return A.aa(a,new Error())}, +aa(a,b){var s +if(a==null)a=new A.bL() +b.dartException=a +s=A.xO +if("defineProperty" in Object){Object.defineProperty(b,"message",{get:s}) +b.name=""}else b.toString=s +return b}, +xO(){return J.b1(this.dartException)}, +E(a,b){throw A.aa(a,b==null?new Error():b)}, +y(a,b,c){var s +if(b==null)b=0 +if(c==null)c=0 +s=Error() +A.E(A.vY(a,b,c),s)}, +vY(a,b,c){var s,r,q,p,o,n,m,l,k +if(typeof b=="string")s=b +else{r="[]=;add;removeWhere;retainWhere;removeRange;setRange;setInt8;setInt16;setInt32;setUint8;setUint16;setUint32;setFloat32;setFloat64".split(";") +q=r.length +p=b +if(p>q){c=p/q|0 +p%=q}s=r[p]}o=typeof c=="string"?c:"modify;remove from;add to".split(";")[c] +n=t.j.b(a)?"list":"ByteData" +m=a.$flags|0 +l="a " +if((m&4)!==0)k="constant " +else if((m&2)!==0){k="unmodifiable " +l="an "}else k=(m&1)!==0?"fixed-length ":"" +return new A.eP("'"+s+"': Cannot "+o+" "+l+k+n)}, +P(a){throw A.b(A.an(a))}, +bM(a){var s,r,q,p,o,n +a=A.rM(a.replace(String({}),"$receiver$")) +s=a.match(/\\\$[a-zA-Z]+\\\$/g) +if(s==null)s=A.f([],t.s) +r=s.indexOf("\\$arguments\\$") +q=s.indexOf("\\$argumentsExpr\\$") +p=s.indexOf("\\$expr\\$") +o=s.indexOf("\\$method\\$") +n=s.indexOf("\\$receiver\\$") +return new A.lt(a.replace(new RegExp("\\\\\\$arguments\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$","g"),"((?:x|[^x])*)"),r,q,p,o,n)}, +lu(a){return function($expr$){var $argumentsExpr$="$arguments$" +try{$expr$.$method$($argumentsExpr$)}catch(s){return s.message}}(a)}, +qn(a){return function($expr$){try{$expr$.$method$}catch(s){return s.message}}(a)}, +oo(a,b){var s=b==null,r=s?null:b.method +return new A.hn(a,r,s?null:b.receiver)}, +H(a){if(a==null)return new A.hD(a) +if(a instanceof A.el)return A.cl(a,a.a) +if(typeof a!=="object")return a +if("dartException" in a)return A.cl(a,a.dartException) +return A.wH(a)}, +cl(a,b){if(t.C.b(b))if(b.$thrownJsError==null)b.$thrownJsError=a +return b}, +wH(a){var s,r,q,p,o,n,m,l,k,j,i,h,g +if(!("message" in a))return a +s=a.message +if("number" in a&&typeof a.number=="number"){r=a.number +q=r&65535 +if((B.b.M(r,16)&8191)===10)switch(q){case 438:return A.cl(a,A.oo(A.t(s)+" (Error "+q+")",null)) +case 445:case 5007:A.t(s) +return A.cl(a,new A.eA())}}if(a instanceof TypeError){p=$.rX() +o=$.rY() +n=$.rZ() +m=$.t_() +l=$.t2() +k=$.t3() +j=$.t1() +$.t0() +i=$.t5() +h=$.t4() +g=p.aw(s) +if(g!=null)return A.cl(a,A.oo(s,g)) +else{g=o.aw(s) +if(g!=null){g.method="call" +return A.cl(a,A.oo(s,g))}else if(n.aw(s)!=null||m.aw(s)!=null||l.aw(s)!=null||k.aw(s)!=null||j.aw(s)!=null||m.aw(s)!=null||i.aw(s)!=null||h.aw(s)!=null)return A.cl(a,new A.eA())}return A.cl(a,new A.hU(typeof s=="string"?s:""))}if(a instanceof RangeError){if(typeof s=="string"&&s.indexOf("call stack")!==-1)return new A.eK() +s=function(b){try{return String(b)}catch(f){}return null}(a) +return A.cl(a,new A.bc(!1,null,null,typeof s=="string"?s.replace(/^RangeError:\s*/,""):s))}if(typeof InternalError=="function"&&a instanceof InternalError)if(typeof s=="string"&&s==="too much recursion")return new A.eK() +return a}, +a5(a){var s +if(a instanceof A.el)return a.b +if(a==null)return new A.fm(a) +s=a.$cachedTrace +if(s!=null)return s +s=new A.fm(a) +if(typeof a==="object")a.$cachedTrace=s +return s}, +pb(a){if(a==null)return J.aE(a) +if(typeof a=="object")return A.eE(a) +return J.aE(a)}, +xc(a,b){var s,r,q,p=a.length +for(s=0;s=0 +else if(b instanceof A.cx){s=B.a.L(a,c) +return b.b.test(s)}else return!J.o9(b,B.a.L(a,c)).gB(0)}, +p6(a){if(a.indexOf("$",0)>=0)return a.replace(/\$/g,"$$$$") +return a}, +xK(a,b,c,d){var s=b.ff(a,d) +if(s==null)return a +return A.ph(a,s.b.index,s.gbz(),c)}, +rM(a){if(/[[\]{}()*+?.\\^$|]/.test(a))return a.replace(/[[\]{}()*+?.\\^$|]/g,"\\$&") +return a}, +bk(a,b,c){var s +if(typeof b=="string")return A.xJ(a,b,c) +if(b instanceof A.cx){s=b.gfq() +s.lastIndex=0 +return a.replace(s,A.p6(c))}return A.xI(a,b,c)}, +xI(a,b,c){var s,r,q,p +for(s=J.o9(b,a),s=s.gq(s),r=0,q="";s.k();){p=s.gm() +q=q+a.substring(r,p.gcv())+c +r=p.gbz()}s=q+a.substring(r) +return s.charCodeAt(0)==0?s:s}, +xJ(a,b,c){var s,r,q +if(b===""){if(a==="")return c +s=a.length +for(r=c,q=0;q=0)return a.split(b).join(c) +return a.replace(new RegExp(A.rM(b),"g"),A.p6(c))}, +xL(a,b,c,d){var s,r,q,p +if(typeof b=="string"){s=a.indexOf(b,d) +if(s<0)return a +return A.ph(a,s,s+b.length,c)}if(b instanceof A.cx)return d===0?a.replace(b.b,A.p6(c)):A.xK(a,b,c,d) +r=J.tC(b,a,d) +q=r.gq(r) +if(!q.k())return a +p=q.gm() +return B.a.aO(a,p.gcv(),p.gbz(),c)}, +ph(a,b,c,d){return a.substring(0,b)+d+a.substring(c)}, +ag:function ag(a,b){this.a=a +this.b=b}, +cR:function cR(a,b){this.a=a +this.b=b}, +iE:function iE(a,b){this.a=a +this.b=b}, +ef:function ef(){}, +eg:function eg(a,b,c){this.a=a +this.b=b +this.$ti=c}, +cP:function cP(a,b){this.a=a +this.$ti=b}, +ix:function ix(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +kl:function kl(){}, +er:function er(a,b){this.a=a +this.$ti=b}, +eH:function eH(){}, +lt:function lt(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +eA:function eA(){}, +hn:function hn(a,b,c){this.a=a +this.b=b +this.c=c}, +hU:function hU(a){this.a=a}, +hD:function hD(a){this.a=a}, +el:function el(a,b){this.a=a +this.b=b}, +fm:function fm(a){this.a=a +this.b=null}, +cq:function cq(){}, +jg:function jg(){}, +jh:function jh(){}, +lj:function lj(){}, +l9:function l9(){}, +eb:function eb(a,b){this.a=a +this.b=b}, +hK:function hK(a){this.a=a}, +bA:function bA(a){var _=this +_.a=0 +_.f=_.e=_.d=_.c=_.b=null +_.r=0 +_.$ti=a}, +ks:function ks(a){this.a=a}, +kv:function kv(a,b){var _=this +_.a=a +_.b=b +_.d=_.c=null}, +bB:function bB(a,b){this.a=a +this.$ti=b}, +hr:function hr(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +ew:function ew(a,b){this.a=a +this.$ti=b}, +db:function db(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +ev:function ev(a,b){this.a=a +this.$ti=b}, +hq:function hq(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=null +_.$ti=d}, +nS:function nS(a){this.a=a}, +nT:function nT(a){this.a=a}, +nU:function nU(a){this.a=a}, +fi:function fi(){}, +iD:function iD(){}, +cx:function cx(a,b){var _=this +_.a=a +_.b=b +_.e=_.d=_.c=null}, +dJ:function dJ(a){this.b=a}, +i9:function i9(a,b,c){this.a=a +this.b=b +this.c=c}, +m5:function m5(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +ds:function ds(a,b){this.a=a +this.c=b}, +iM:function iM(a,b,c){this.a=a +this.b=b +this.c=c}, +ne:function ne(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +xN(a){throw A.aa(A.pT(a),new Error())}, +x(){throw A.aa(A.pU(""),new Error())}, +iZ(){throw A.aa(A.ui(""),new Error())}, +pj(){throw A.aa(A.pT(""),new Error())}, +mm(a){var s=new A.ml(a) +return s.b=s}, +ml:function ml(a){this.a=a +this.b=null}, +vW(a){return a}, +fB(a,b,c){}, +fC(a){var s,r,q +if(t.aP.b(a))return a +s=J.a3(a) +r=A.b5(s.gl(a),null,!1,t.z) +for(q=0;q>>0!==a||a>=c)throw A.b(A.iX(b,a))}, +ci(a,b,c){var s +if(!(a>>>0!==a))s=b>>>0!==b||a>b||b>c +else s=!0 +if(s)throw A.b(A.xa(a,b,c)) +return b}, +de:function de(){}, +dd:function dd(){}, +ey:function ey(){}, +iS:function iS(a){this.a=a}, +cz:function cz(){}, +dg:function dg(){}, +c_:function c_(){}, +aX:function aX(){}, +hu:function hu(){}, +hv:function hv(){}, +hw:function hw(){}, +df:function df(){}, +hx:function hx(){}, +hy:function hy(){}, +hz:function hz(){}, +ez:function ez(){}, +c0:function c0(){}, +fd:function fd(){}, +fe:function fe(){}, +ff:function ff(){}, +fg:function fg(){}, +ou(a,b){var s=b.c +return s==null?b.c=A.fs(a,"C",[b.x]):s}, +qc(a){var s=a.w +if(s===6||s===7)return A.qc(a.x) +return s===11||s===12}, +uB(a){return a.as}, +aB(a){return A.nl(v.typeUniverse,a,!1)}, +xn(a,b){var s,r,q,p,o +if(a==null)return null +s=b.y +r=a.Q +if(r==null)r=a.Q=new Map() +q=b.as +p=r.get(q) +if(p!=null)return p +o=A.cj(v.typeUniverse,a.x,s,0) +r.set(q,o) +return o}, +cj(a1,a2,a3,a4){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0=a2.w +switch(a0){case 5:case 1:case 2:case 3:case 4:return a2 +case 6:s=a2.x +r=A.cj(a1,s,a3,a4) +if(r===s)return a2 +return A.qP(a1,r,!0) +case 7:s=a2.x +r=A.cj(a1,s,a3,a4) +if(r===s)return a2 +return A.qO(a1,r,!0) +case 8:q=a2.y +p=A.e_(a1,q,a3,a4) +if(p===q)return a2 +return A.fs(a1,a2.x,p) +case 9:o=a2.x +n=A.cj(a1,o,a3,a4) +m=a2.y +l=A.e_(a1,m,a3,a4) +if(n===o&&l===m)return a2 +return A.oO(a1,n,l) +case 10:k=a2.x +j=a2.y +i=A.e_(a1,j,a3,a4) +if(i===j)return a2 +return A.qQ(a1,k,i) +case 11:h=a2.x +g=A.cj(a1,h,a3,a4) +f=a2.y +e=A.wE(a1,f,a3,a4) +if(g===h&&e===f)return a2 +return A.qN(a1,g,e) +case 12:d=a2.y +a4+=d.length +c=A.e_(a1,d,a3,a4) +o=a2.x +n=A.cj(a1,o,a3,a4) +if(c===d&&n===o)return a2 +return A.oP(a1,n,c,!0) +case 13:b=a2.x +if(b") +for(r=1;r=0)p+=" "+r[q];++q}return p+"})"}, +re(a1,a2,a3){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a=", ",a0=null +if(a3!=null){s=a3.length +if(a2==null)a2=A.f([],t.s) +else a0=a2.length +r=a2.length +for(q=s;q>0;--q)a2.push("T"+(r+q)) +for(p=t.X,o="<",n="",q=0;q0){c+=b+"[" +for(b="",q=0;q0){c+=b+"{" +for(b="",q=0;q "+d}, +aZ(a,b){var s,r,q,p,o,n,m=a.w +if(m===5)return"erased" +if(m===2)return"dynamic" +if(m===3)return"void" +if(m===1)return"Never" +if(m===4)return"any" +if(m===6){s=a.x +r=A.aZ(s,b) +q=s.w +return(q===11||q===12?"("+r+")":r)+"?"}if(m===7)return"FutureOr<"+A.aZ(a.x,b)+">" +if(m===8){p=A.wG(a.x) +o=a.y +return o.length>0?p+("<"+A.rp(o,b)+">"):p}if(m===10)return A.wq(a,b) +if(m===11)return A.re(a,b,null) +if(m===12)return A.re(a.x,b,a.y) +if(m===13){n=a.x +return b[b.length-1-n]}return"?"}, +wG(a){var s=v.mangledGlobalNames[a] +if(s!=null)return s +return"minified:"+a}, +vr(a,b){var s=a.tR[b] +while(typeof s=="string")s=a.tR[s] +return s}, +vq(a,b){var s,r,q,p,o,n=a.eT,m=n[b] +if(m==null)return A.nl(a,b,!1) +else if(typeof m=="number"){s=m +r=A.ft(a,5,"#") +q=A.nt(s) +for(p=0;p0)p+="<"+A.fr(c)+">" +s=a.eC.get(p) +if(s!=null)return s +r=new A.be(null,null) +r.w=8 +r.x=b +r.y=c +if(c.length>0)r.c=c[0] +r.as=p +q=A.ch(a,r) +a.eC.set(p,q) +return q}, +oO(a,b,c){var s,r,q,p,o,n +if(b.w===9){s=b.x +r=b.y.concat(c)}else{r=c +s=b}q=s.as+(";<"+A.fr(r)+">") +p=a.eC.get(q) +if(p!=null)return p +o=new A.be(null,null) +o.w=9 +o.x=s +o.y=r +o.as=q +n=A.ch(a,o) +a.eC.set(q,n) +return n}, +qQ(a,b,c){var s,r,q="+"+(b+"("+A.fr(c)+")"),p=a.eC.get(q) +if(p!=null)return p +s=new A.be(null,null) +s.w=10 +s.x=b +s.y=c +s.as=q +r=A.ch(a,s) +a.eC.set(q,r) +return r}, +qN(a,b,c){var s,r,q,p,o,n=b.as,m=c.a,l=m.length,k=c.b,j=k.length,i=c.c,h=i.length,g="("+A.fr(m) +if(j>0){s=l>0?",":"" +g+=s+"["+A.fr(k)+"]"}if(h>0){s=l>0?",":"" +g+=s+"{"+A.vj(i)+"}"}r=n+(g+")") +q=a.eC.get(r) +if(q!=null)return q +p=new A.be(null,null) +p.w=11 +p.x=b +p.y=c +p.as=r +o=A.ch(a,p) +a.eC.set(r,o) +return o}, +oP(a,b,c,d){var s,r=b.as+("<"+A.fr(c)+">"),q=a.eC.get(r) +if(q!=null)return q +s=A.vl(a,b,c,r,d) +a.eC.set(r,s) +return s}, +vl(a,b,c,d,e){var s,r,q,p,o,n,m,l +if(e){s=c.length +r=A.nt(s) +for(q=0,p=0;p0){n=A.cj(a,b,r,0) +m=A.e_(a,c,r,0) +return A.oP(a,n,m,c!==m)}}l=new A.be(null,null) +l.w=12 +l.x=b +l.y=c +l.as=d +return A.ch(a,l)}, +qH(a,b,c,d){return{u:a,e:b,r:c,s:[],p:0,n:d}}, +qJ(a){var s,r,q,p,o,n,m,l=a.r,k=a.s +for(s=l.length,r=0;r=48&&q<=57)r=A.vb(r+1,q,l,k) +else if((((q|32)>>>0)-97&65535)<26||q===95||q===36||q===124)r=A.qI(a,r,l,k,!1) +else if(q===46)r=A.qI(a,r,l,k,!0) +else{++r +switch(q){case 44:break +case 58:k.push(!1) +break +case 33:k.push(!0) +break +case 59:k.push(A.cQ(a.u,a.e,k.pop())) +break +case 94:k.push(A.vn(a.u,k.pop())) +break +case 35:k.push(A.ft(a.u,5,"#")) +break +case 64:k.push(A.ft(a.u,2,"@")) +break +case 126:k.push(A.ft(a.u,3,"~")) +break +case 60:k.push(a.p) +a.p=k.length +break +case 62:A.vd(a,k) +break +case 38:A.vc(a,k) +break +case 63:p=a.u +k.push(A.qP(p,A.cQ(p,a.e,k.pop()),a.n)) +break +case 47:p=a.u +k.push(A.qO(p,A.cQ(p,a.e,k.pop()),a.n)) +break +case 40:k.push(-3) +k.push(a.p) +a.p=k.length +break +case 41:A.va(a,k) +break +case 91:k.push(a.p) +a.p=k.length +break +case 93:o=k.splice(a.p) +A.qK(a.u,a.e,o) +a.p=k.pop() +k.push(o) +k.push(-1) +break +case 123:k.push(a.p) +a.p=k.length +break +case 125:o=k.splice(a.p) +A.vf(a.u,a.e,o) +a.p=k.pop() +k.push(o) +k.push(-2) +break +case 43:n=l.indexOf("(",r) +k.push(l.substring(r,n)) +k.push(-4) +k.push(a.p) +a.p=k.length +r=n+1 +break +default:throw"Bad character "+q}}}m=k.pop() +return A.cQ(a.u,a.e,m)}, +vb(a,b,c,d){var s,r,q=b-48 +for(s=c.length;a=48&&r<=57))break +q=q*10+(r-48)}d.push(q) +return a}, +qI(a,b,c,d,e){var s,r,q,p,o,n,m=b+1 +for(s=c.length;m>>0)-97&65535)<26||r===95||r===36||r===124))q=r>=48&&r<=57 +else q=!0 +if(!q)break}}p=c.substring(b,m) +if(e){s=a.u +o=a.e +if(o.w===9)o=o.x +n=A.vr(s,o.x)[p] +if(n==null)A.E('No "'+p+'" in "'+A.uB(o)+'"') +d.push(A.fu(s,o,n))}else d.push(p) +return m}, +vd(a,b){var s,r=a.u,q=A.qG(a,b),p=b.pop() +if(typeof p=="string")b.push(A.fs(r,p,q)) +else{s=A.cQ(r,a.e,p) +switch(s.w){case 11:b.push(A.oP(r,s,q,a.n)) +break +default:b.push(A.oO(r,s,q)) +break}}}, +va(a,b){var s,r,q,p=a.u,o=b.pop(),n=null,m=null +if(typeof o=="number")switch(o){case-1:n=b.pop() +break +case-2:m=b.pop() +break +default:b.push(o) +break}else b.push(o) +s=A.qG(a,b) +o=b.pop() +switch(o){case-3:o=b.pop() +if(n==null)n=p.sEA +if(m==null)m=p.sEA +r=A.cQ(p,a.e,o) +q=new A.ir() +q.a=s +q.b=n +q.c=m +b.push(A.qN(p,r,q)) +return +case-4:b.push(A.qQ(p,b.pop(),s)) +return +default:throw A.b(A.e8("Unexpected state under `()`: "+A.t(o)))}}, +vc(a,b){var s=b.pop() +if(0===s){b.push(A.ft(a.u,1,"0&")) +return}if(1===s){b.push(A.ft(a.u,4,"1&")) +return}throw A.b(A.e8("Unexpected extended operation "+A.t(s)))}, +qG(a,b){var s=b.splice(a.p) +A.qK(a.u,a.e,s) +a.p=b.pop() +return s}, +cQ(a,b,c){if(typeof c=="string")return A.fs(a,c,a.sEA) +else if(typeof c=="number"){b.toString +return A.ve(a,b,c)}else return c}, +qK(a,b,c){var s,r=c.length +for(s=0;sn)return!1 +m=n-o +l=s.b +k=r.b +j=l.length +i=k.length +if(o+j=d)return!1 +a1=f[b] +b+=3 +if(a00?new Array(q):v.typeUniverse.sEA +for(o=0;o0?new Array(a):v.typeUniverse.sEA}, +be:function be(a,b){var _=this +_.a=a +_.b=b +_.r=_.f=_.d=_.c=null +_.w=0 +_.as=_.Q=_.z=_.y=_.x=null}, +ir:function ir(){this.c=this.b=this.a=null}, +nk:function nk(a){this.a=a}, +im:function im(){}, +fq:function fq(a){this.a=a}, +uW(){var s,r,q +if(self.scheduleImmediate!=null)return A.wK() +if(self.MutationObserver!=null&&self.document!=null){s={} +r=self.document.createElement("div") +q=self.document.createElement("span") +s.a=null +new self.MutationObserver(A.ck(new A.m7(s),1)).observe(r,{childList:true}) +return new A.m6(s,r,q)}else if(self.setImmediate!=null)return A.wL() +return A.wM()}, +uX(a){self.scheduleImmediate(A.ck(new A.m8(a),0))}, +uY(a){self.setImmediate(A.ck(new A.m9(a),0))}, +uZ(a){A.oA(B.J,a)}, +oA(a,b){var s=B.b.I(a.a,1000) +return A.vh(s<0?0:s,b)}, +vh(a,b){var s=new A.iP() +s.i2(a,b) +return s}, +vi(a,b){var s=new A.iP() +s.i3(a,b) +return s}, +k(a){return new A.ia(new A.n($.m,a.h("n<0>")),a.h("ia<0>"))}, +j(a,b){a.$2(0,null) +b.b=!0 +return b.a}, +c(a,b){A.vM(a,b)}, +i(a,b){b.O(a)}, +h(a,b){b.by(A.H(a),A.a5(a))}, +vM(a,b){var s,r,q=new A.nu(b),p=new A.nv(b) +if(a instanceof A.n)a.fN(q,p,t.z) +else{s=t.z +if(a instanceof A.n)a.bf(q,p,s) +else{r=new A.n($.m,t.eI) +r.a=8 +r.c=a +r.fN(q,p,s)}}}, +l(a){var s=function(b,c){return function(d,e){while(true){try{b(d,e) +break}catch(r){e=r +d=c}}}}(a,1) +return $.m.dc(new A.nJ(s),t.H,t.S,t.z)}, +qM(a,b,c){return 0}, +fP(a){var s +if(t.C.b(a)){s=a.gaP() +if(s!=null)return s}return B.t}, +oi(a,b){var s,r,q,p,o,n,m,l=null +try{l=a.$0()}catch(q){s=A.H(q) +r=A.a5(q) +p=new A.n($.m,b.h("n<0>")) +o=s +n=r +m=A.dY(o,n) +if(m==null)o=new A.U(o,n==null?A.fP(o):n) +else o=m +p.aR(o) +return p}return b.h("C<0>").b(l)?l:A.cL(l,b)}, +b3(a,b){var s=a==null?b.a(a):a,r=new A.n($.m,b.h("n<0>")) +r.b3(s) +return r}, +pK(a,b){var s +if(!b.b(null))throw A.b(A.ad(null,"computation","The type parameter is not nullable")) +s=new A.n($.m,b.h("n<0>")) +A.uH(a,new A.kc(null,s,b)) +return s}, +pL(a,b){var s,r,q,p,o,n,m,l,k,j,i={},h=null,g=!1,f=new A.n($.m,b.h("n>")) +i.a=null +i.b=0 +i.c=i.d=null +s=new A.ke(i,h,g,f) +try{for(n=J.Z(a),m=t.P;n.k();){r=n.gm() +q=i.b +r.bf(new A.kd(i,q,f,b,h,g),s,m);++i.b}n=i.b +if(n===0){n=f +n.bL(A.f([],b.h("u<0>"))) +return n}i.a=A.b5(n,null,!1,b.h("0?"))}catch(l){p=A.H(l) +o=A.a5(l) +if(i.b===0||g){n=f +m=p +k=o +j=A.dY(m,k) +if(j==null)m=new A.U(m,k==null?A.fP(m):k) +else m=j +n.aR(m) +return n}else{i.d=p +i.c=o}}return f}, +ua(a,b){var s,r,q,p=A.f([],b.h("u>")) +for(s=a.length,r=b.h("f7<0>"),q=0;q")),b.h("o<0>")) +s=new A.n($.m,b.h("n>")) +A.v8(p,new A.kb(new A.a1(s,b.h("a1>")),p,b)) +return s}, +wk(a){return a!=null}, +v8(a,b){var s,r={},q=r.a=r.b=0,p=new A.mC(r,a,b) +for(s=a.length;q")) +s.a=8 +s.c=a +return s}, +cL(a,b){var s=new A.n($.m,b.h("n<0>")) +s.a=8 +s.c=a +return s}, +mI(a,b,c){var s,r,q,p={},o=p.a=a +while(s=o.a,(s&4)!==0){o=o.c +p.a=o}if(o===b){s=A.l8() +b.aR(new A.U(new A.bc(!0,o,null,"Cannot complete a future with itself"),s)) +return}r=b.a&1 +s=o.a=s|r +if((s&24)===0){q=b.c +b.a=b.a&1|4 +b.c=o +o.ft(q) +return}if(!c)if(b.c==null)o=(s&16)===0||r!==0 +else o=!1 +else o=!0 +if(o){q=b.bS() +b.cC(p.a) +A.cM(b,q) +return}b.a^=2 +b.b.b1(new A.mJ(p,b))}, +cM(a,b){var s,r,q,p,o,n,m,l,k,j,i,h,g={},f=g.a=a +for(;;){s={} +r=f.a +q=(r&16)===0 +p=!q +if(b==null){if(p&&(r&1)===0){r=f.c +f.b.c7(r.a,r.b)}return}s.a=b +o=b.a +for(f=b;o!=null;f=o,o=n){f.a=null +A.cM(g.a,f) +s.a=o +n=o.a}r=g.a +m=r.c +s.b=p +s.c=m +if(q){l=f.c +l=(l&1)!==0||(l&15)===8}else l=!0 +if(l){k=f.b.b +if(p){f=r.b +f=!(f===k||f.gaK()===k.gaK())}else f=!1 +if(f){f=g.a +r=f.c +f.b.c7(r.a,r.b) +return}j=$.m +if(j!==k)$.m=k +else j=null +f=s.a.c +if((f&15)===8)new A.mN(s,g,p).$0() +else if(q){if((f&1)!==0)new A.mM(s,m).$0()}else if((f&2)!==0)new A.mL(g,s).$0() +if(j!=null)$.m=j +f=s.c +if(f instanceof A.n){r=s.a.$ti +r=r.h("C<2>").b(f)||!r.y[1].b(f)}else r=!1 +if(r){i=s.a.b +if((f.a&24)!==0){h=i.c +i.c=null +b=i.cJ(h) +i.a=f.a&30|i.a&1 +i.c=f.c +g.a=f +continue}else A.mI(f,i,!0) +return}}i=s.a.b +h=i.c +i.c=null +b=i.cJ(h) +f=s.b +r=s.c +if(!f){i.a=8 +i.c=r}else{i.a=i.a&1|16 +i.c=r}g.a=i +f=i}}, +ws(a,b){if(t._.b(a))return b.dc(a,t.z,t.K,t.l) +if(t.bI.b(a))return b.bb(a,t.z,t.K) +throw A.b(A.ad(a,"onError",u.c))}, +wj(){var s,r +for(s=$.dZ;s!=null;s=$.dZ){$.fE=null +r=s.b +$.dZ=r +if(r==null)$.fD=null +s.a.$0()}}, +wD(){$.oZ=!0 +try{A.wj()}finally{$.fE=null +$.oZ=!1 +if($.dZ!=null)$.pm().$1(A.rx())}}, +rr(a){var s=new A.ib(a),r=$.fD +if(r==null){$.dZ=$.fD=s +if(!$.oZ)$.pm().$1(A.rx())}else $.fD=r.b=s}, +wA(a){var s,r,q,p=$.dZ +if(p==null){A.rr(a) +$.fE=$.fD +return}s=new A.ib(a) +r=$.fE +if(r==null){s.b=p +$.dZ=$.fE=s}else{q=r.b +s.b=q +$.fE=r.b=s +if(q==null)$.fD=s}}, +pe(a){var s,r=null,q=$.m +if(B.d===q){A.nG(r,r,B.d,a) +return}if(B.d===q.ge3().a)s=B.d.gaK()===q.gaK() +else s=!1 +if(s){A.nG(r,r,q,q.az(a,t.H)) +return}s=$.m +s.b1(s.c2(a))}, +y5(a){return new A.dO(A.cV(a,"stream",t.K))}, +eN(a,b,c,d){var s=null +return c?new A.dS(b,s,s,a,d.h("dS<0>")):new A.dA(b,s,s,a,d.h("dA<0>"))}, +iV(a){var s,r,q +if(a==null)return +try{a.$0()}catch(q){s=A.H(q) +r=A.a5(q) +$.m.c7(s,r)}}, +v6(a,b,c,d,e,f){var s=$.m,r=e?1:0,q=c!=null?32:0,p=A.ih(s,b,f),o=A.ii(s,c),n=d==null?A.rw():d +return new A.cf(a,p,o,s.az(n,t.H),s,r|q,f.h("cf<0>"))}, +ih(a,b,c){var s=b==null?A.wO():b +return a.bb(s,t.H,c)}, +ii(a,b){if(b==null)b=A.wP() +if(t.da.b(b))return a.dc(b,t.z,t.K,t.l) +if(t.d5.b(b))return a.bb(b,t.z,t.K) +throw A.b(A.J("handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace.",null))}, +wl(a){}, +wn(a,b){$.m.c7(a,b)}, +wm(){}, +wy(a,b,c){var s,r,q,p +try{b.$1(a.$0())}catch(p){s=A.H(p) +r=A.a5(p) +q=A.dY(s,r) +if(q!=null)c.$2(q.a,q.b) +else c.$2(s,r)}}, +vT(a,b,c){var s=a.J() +if(s!==$.cm())s.ai(new A.nx(b,c)) +else b.V(c)}, +vU(a,b){return new A.nw(a,b)}, +r7(a,b,c){var s=a.J() +if(s!==$.cm())s.ai(new A.ny(b,c)) +else b.b4(c)}, +vg(a,b,c){return new A.dM(new A.nd(null,null,a,c,b),b.h("@<0>").G(c).h("dM<1,2>"))}, +uH(a,b){var s=$.m +if(s===B.d)return s.ei(a,b) +return s.ei(a,s.c2(b))}, +rN(a,b,c,d){return A.wz(a,c,b,d)}, +wz(a,b,c,d){return $.m.h8(c,b).bd(a,d)}, +ww(a,b,c,d,e){A.fF(d,e)}, +fF(a,b){A.wA(new A.nC(a,b))}, +nD(a,b,c,d){var s,r=$.m +if(r===c)return d.$0() +$.m=c +s=r +try{r=d.$0() +return r}finally{$.m=s}}, +nF(a,b,c,d,e){var s,r=$.m +if(r===c)return d.$1(e) +$.m=c +s=r +try{r=d.$1(e) +return r}finally{$.m=s}}, +nE(a,b,c,d,e,f){var s,r=$.m +if(r===c)return d.$2(e,f) +$.m=c +s=r +try{r=d.$2(e,f) +return r}finally{$.m=s}}, +rn(a,b,c,d){return d}, +ro(a,b,c,d){return d}, +rm(a,b,c,d){return d}, +wv(a,b,c,d,e){return null}, +nG(a,b,c,d){var s,r +if(B.d!==c){s=B.d.gaK() +r=c.gaK() +d=s!==r?c.c2(d):c.ef(d,t.H)}A.rr(d)}, +wu(a,b,c,d,e){return A.oA(d,B.d!==c?c.ef(e,t.H):e)}, +wt(a,b,c,d,e){var s +if(B.d!==c)e=c.fX(e,t.H,t.aF) +s=B.b.I(d.a,1000) +return A.vi(s<0?0:s,e)}, +wx(a,b,c,d){A.pd(d)}, +wp(a){$.m.hj(a)}, +rl(a,b,c,d,e){var s,r,q,p +$.rk=A.wQ() +if(d==null)d=B.bu +if(e==null)s=c.gfn() +else{r=t.X +s=A.ub(e,r,r)}r=new A.ij(c.gfF(),c.gfH(),c.gfG(),c.gfB(),c.gfC(),c.gfA(),c.gfe(),c.ge3(),c.gf9(),c.gf8(),c.gfu(),c.gfh(),c.gdW(),c,s) +q=d.x +if(q!=null)r.w=new A.av(r,q) +p=d.a +if(p!=null)r.as=new A.av(r,p) +return r}, +m7:function m7(a){this.a=a}, +m6:function m6(a,b,c){this.a=a +this.b=b +this.c=c}, +m8:function m8(a){this.a=a}, +m9:function m9(a){this.a=a}, +iP:function iP(){this.c=0}, +nj:function nj(a,b){this.a=a +this.b=b}, +ni:function ni(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +ia:function ia(a,b){this.a=a +this.b=!1 +this.$ti=b}, +nu:function nu(a){this.a=a}, +nv:function nv(a){this.a=a}, +nJ:function nJ(a){this.a=a}, +iN:function iN(a){var _=this +_.a=a +_.e=_.d=_.c=_.b=null}, +dR:function dR(a,b){this.a=a +this.$ti=b}, +U:function U(a,b){this.a=a +this.b=b}, +eV:function eV(a,b){this.a=a +this.$ti=b}, +cJ:function cJ(a,b,c,d,e,f,g){var _=this +_.ay=0 +_.CW=_.ch=null +_.w=a +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.r=_.f=null +_.$ti=g}, +cI:function cI(){}, +fp:function fp(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.r=_.f=_.e=_.d=null +_.$ti=c}, +nf:function nf(a,b){this.a=a +this.b=b}, +nh:function nh(a,b,c){this.a=a +this.b=b +this.c=c}, +ng:function ng(a){this.a=a}, +kc:function kc(a,b,c){this.a=a +this.b=b +this.c=c}, +ke:function ke(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +kd:function kd(a,b,c,d,e,f){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f}, +kb:function kb(a,b,c){this.a=a +this.b=b +this.c=c}, +eD:function eD(a,b){this.c=a +this.d=b}, +f7:function f7(a,b){var _=this +_.a=a +_.c=_.b=null +_.$ti=b}, +mD:function mD(a,b){this.a=a +this.b=b}, +mE:function mE(a,b){this.a=a +this.b=b}, +mC:function mC(a,b,c){this.a=a +this.b=b +this.c=c}, +dB:function dB(){}, +a7:function a7(a,b){this.a=a +this.$ti=b}, +a1:function a1(a,b){this.a=a +this.$ti=b}, +cg:function cg(a,b,c,d,e){var _=this +_.a=null +_.b=a +_.c=b +_.d=c +_.e=d +_.$ti=e}, +n:function n(a,b){var _=this +_.a=0 +_.b=a +_.c=null +_.$ti=b}, +mF:function mF(a,b){this.a=a +this.b=b}, +mK:function mK(a,b){this.a=a +this.b=b}, +mJ:function mJ(a,b){this.a=a +this.b=b}, +mH:function mH(a,b){this.a=a +this.b=b}, +mG:function mG(a,b){this.a=a +this.b=b}, +mN:function mN(a,b,c){this.a=a +this.b=b +this.c=c}, +mO:function mO(a,b){this.a=a +this.b=b}, +mP:function mP(a){this.a=a}, +mM:function mM(a,b){this.a=a +this.b=b}, +mL:function mL(a,b){this.a=a +this.b=b}, +ib:function ib(a){this.a=a +this.b=null}, +X:function X(){}, +lg:function lg(a,b){this.a=a +this.b=b}, +lh:function lh(a,b){this.a=a +this.b=b}, +le:function le(a){this.a=a}, +lf:function lf(a,b,c){this.a=a +this.b=b +this.c=c}, +lc:function lc(a,b){this.a=a +this.b=b}, +ld:function ld(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +la:function la(a,b){this.a=a +this.b=b}, +lb:function lb(a,b,c){this.a=a +this.b=b +this.c=c}, +hP:function hP(){}, +cS:function cS(){}, +nc:function nc(a){this.a=a}, +nb:function nb(a){this.a=a}, +iO:function iO(){}, +ic:function ic(){}, +dA:function dA(a,b,c,d,e){var _=this +_.a=null +_.b=0 +_.c=null +_.d=a +_.e=b +_.f=c +_.r=d +_.$ti=e}, +dS:function dS(a,b,c,d,e){var _=this +_.a=null +_.b=0 +_.c=null +_.d=a +_.e=b +_.f=c +_.r=d +_.$ti=e}, +at:function at(a,b){this.a=a +this.$ti=b}, +cf:function cf(a,b,c,d,e,f,g){var _=this +_.w=a +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.r=_.f=null +_.$ti=g}, +dP:function dP(a){this.a=a}, +af:function af(){}, +mk:function mk(a,b,c){this.a=a +this.b=b +this.c=c}, +mj:function mj(a){this.a=a}, +dN:function dN(){}, +il:function il(){}, +dD:function dD(a){this.b=a +this.a=null}, +eZ:function eZ(a,b){this.b=a +this.c=b +this.a=null}, +mu:function mu(){}, +fh:function fh(){this.a=0 +this.c=this.b=null}, +n1:function n1(a,b){this.a=a +this.b=b}, +f0:function f0(a){this.a=1 +this.b=a +this.c=null}, +dO:function dO(a){this.a=null +this.b=a +this.c=!1}, +nx:function nx(a,b){this.a=a +this.b=b}, +nw:function nw(a,b){this.a=a +this.b=b}, +ny:function ny(a,b){this.a=a +this.b=b}, +f5:function f5(){}, +dE:function dE(a,b,c,d,e,f,g){var _=this +_.w=a +_.x=null +_.a=b +_.b=c +_.c=d +_.d=e +_.e=f +_.r=_.f=null +_.$ti=g}, +fc:function fc(a,b,c){this.b=a +this.a=b +this.$ti=c}, +f2:function f2(a){this.a=a}, +dL:function dL(a,b,c,d,e,f){var _=this +_.w=$ +_.x=null +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.r=_.f=null +_.$ti=f}, +fo:function fo(){}, +eU:function eU(a,b,c){this.a=a +this.b=b +this.$ti=c}, +dF:function dF(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.$ti=e}, +dM:function dM(a,b){this.a=a +this.$ti=b}, +nd:function nd(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e}, +av:function av(a,b){this.a=a +this.b=b}, +iU:function iU(){}, +ij:function ij(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=h +_.x=i +_.y=j +_.z=k +_.Q=l +_.as=m +_.at=null +_.ax=n +_.ay=o}, +mr:function mr(a,b,c){this.a=a +this.b=b +this.c=c}, +mt:function mt(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +mq:function mq(a,b){this.a=a +this.b=b}, +ms:function ms(a,b,c){this.a=a +this.b=b +this.c=c}, +iI:function iI(){}, +n6:function n6(a,b,c){this.a=a +this.b=b +this.c=c}, +n8:function n8(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +n5:function n5(a,b){this.a=a +this.b=b}, +n7:function n7(a,b,c){this.a=a +this.b=b +this.c=c}, +dV:function dV(a){this.a=a}, +nC:function nC(a,b){this.a=a +this.b=b}, +fz:function fz(a,b,c,d,e,f,g,h,i,j,k,l,m){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=h +_.x=i +_.y=j +_.z=k +_.Q=l +_.as=m}, +pN(a,b){return new A.cN(a.h("@<0>").G(b).h("cN<1,2>"))}, +qF(a,b){var s=a[b] +return s===a?null:s}, +oM(a,b,c){if(c==null)a[b]=a +else a[b]=c}, +oL(){var s=Object.create(null) +A.oM(s,"",s) +delete s[""] +return s}, +uj(a,b){return new A.bA(a.h("@<0>").G(b).h("bA<1,2>"))}, +uk(a,b,c){return A.xc(a,new A.bA(b.h("@<0>").G(c).h("bA<1,2>")))}, +ao(a,b){return new A.bA(a.h("@<0>").G(b).h("bA<1,2>"))}, +op(a){return new A.fa(a.h("fa<0>"))}, +oN(){var s=Object.create(null) +s[""]=s +delete s[""] +return s}, +iy(a,b,c){var s=new A.dI(a,b,c.h("dI<0>")) +s.c=a.e +return s}, +ub(a,b,c){var s=A.pN(b,c) +a.ar(0,new A.kh(s,b,c)) +return s}, +oq(a){var s,r +if(A.p9(a))return"{...}" +s=new A.aD("") +try{r={} +$.cU.push(a) +s.a+="{" +r.a=!0 +a.ar(0,new A.kA(r,s)) +s.a+="}"}finally{$.cU.pop()}r=s.a +return r.charCodeAt(0)==0?r:r}, +cN:function cN(a){var _=this +_.a=0 +_.e=_.d=_.c=_.b=null +_.$ti=a}, +mQ:function mQ(a){this.a=a}, +dG:function dG(a){var _=this +_.a=0 +_.e=_.d=_.c=_.b=null +_.$ti=a}, +cO:function cO(a,b){this.a=a +this.$ti=b}, +is:function is(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +fa:function fa(a){var _=this +_.a=0 +_.f=_.e=_.d=_.c=_.b=null +_.r=0 +_.$ti=a}, +n_:function n_(a){this.a=a +this.c=this.b=null}, +dI:function dI(a,b,c){var _=this +_.a=a +_.b=b +_.d=_.c=null +_.$ti=c}, +kh:function kh(a,b,c){this.a=a +this.b=b +this.c=c}, +cy:function cy(a){var _=this +_.b=_.a=0 +_.c=null +_.$ti=a}, +iz:function iz(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=null +_.d=c +_.e=!1 +_.$ti=d}, +ay:function ay(){}, +w:function w(){}, +S:function S(){}, +kz:function kz(a){this.a=a}, +kA:function kA(a,b){this.a=a +this.b=b}, +fb:function fb(a,b){this.a=a +this.$ti=b}, +iA:function iA(a,b,c){var _=this +_.a=a +_.b=b +_.c=null +_.$ti=c}, +dp:function dp(){}, +fk:function fk(){}, +vE(a,b,c){var s,r,q,p,o=c-b +if(o<=4096)s=$.tg() +else s=new Uint8Array(o) +for(r=J.a3(a),q=0;q=16)return null +r=r*16+o}n=h-1 +i[h]=r +for(;s=16)return null +r=r*16+o}m=n-1 +i[n]=r}if(j===1&&i[0]===0)return $.bb() +l=A.aS(j,i) +return new A.a8(l===0?!1:c,i,l)}, +v5(a,b){var s,r,q,p,o +if(a==="")return null +s=$.t9().a8(a) +if(s==null)return null +r=s.b +q=r[1]==="-" +p=r[4] +o=r[3] +if(p!=null)return A.v2(p,q) +if(o!=null)return A.v3(o,2,q) +return null}, +aS(a,b){for(;;){if(!(a>0&&b[a-1]===0))break;--a}return a}, +oI(a,b,c,d){var s,r=new Uint16Array(d),q=c-b +for(s=0;s>>0)+(o>>>4)-1075 +m=new Uint16Array(4) +m[0]=(r[1]<<8>>>0)+r[0] +m[1]=(r[3]<<8>>>0)+r[2] +m[2]=(r[5]<<8>>>0)+r[4] +m[3]=o&15|16 +l=new A.a8(!1,m,4) +if(n<0)k=l.bl(0,-n) +else k=n>0?l.aE(0,n):l +if(s)return k.aj(0) +return k}, +oJ(a,b,c,d){var s,r,q +if(b===0)return 0 +if(c===0&&d===a)return b +for(s=b-1,r=d.$flags|0;s>=0;--s){q=a[s] +r&2&&A.y(d) +d[s+c]=q}for(s=c-1;s>=0;--s){r&2&&A.y(d) +d[s]=0}return b+c}, +qC(a,b,c,d){var s,r,q,p,o,n=B.b.I(c,16),m=B.b.ab(c,16),l=16-m,k=B.b.aE(1,l)-1 +for(s=b-1,r=d.$flags|0,q=0;s>=0;--s){p=a[s] +o=B.b.bl(p,l) +r&2&&A.y(d) +d[s+n+1]=(o|q)>>>0 +q=B.b.aE((p&k)>>>0,m)}r&2&&A.y(d) +d[n]=q}, +qx(a,b,c,d){var s,r,q,p,o=B.b.I(c,16) +if(B.b.ab(c,16)===0)return A.oJ(a,b,o,d) +s=b+o+1 +A.qC(a,b,c,d) +for(r=d.$flags|0,q=o;--q,q>=0;){r&2&&A.y(d) +d[q]=0}p=s-1 +return d[p]===0?p:s}, +v4(a,b,c,d){var s,r,q,p,o=B.b.I(c,16),n=B.b.ab(c,16),m=16-n,l=B.b.aE(1,n)-1,k=B.b.bl(a[o],n),j=b-o-1 +for(s=d.$flags|0,r=0;r>>0,m) +s&2&&A.y(d) +d[r]=(p|k)>>>0 +k=B.b.bl(q,n)}s&2&&A.y(d) +d[j]=k}, +mg(a,b,c,d){var s,r=b-d +if(r===0)for(s=b-1;s>=0;--s){r=a[s]-c[s] +if(r!==0)return r}return r}, +v0(a,b,c,d,e){var s,r,q +for(s=e.$flags|0,r=0,q=0;q=0;e=o,c=q){q=c+1 +p=a*b[c]+d[e]+r +o=e+1 +s&2&&A.y(d) +d[e]=p&65535 +r=B.b.I(p,65536)}for(;r!==0;e=o){n=d[e]+r +o=e+1 +s&2&&A.y(d) +d[e]=n&65535 +r=B.b.I(n,65536)}}, +v1(a,b,c){var s,r=b[c] +if(r===a)return 65535 +s=B.b.eY((r<<16|b[c-1])>>>0,a) +if(s>65535)return 65535 +return s}, +u1(a){throw A.b(A.ad(a,"object","Expandos are not allowed on strings, numbers, bools, records or null"))}, +mB(a,b){var s=$.tb() +s=s==null?null:new s(A.ck(A.xR(a,b),1)) +return new A.iq(s,b.h("iq<0>"))}, +bj(a,b){var s=A.q5(a,b) +if(s!=null)return s +throw A.b(A.aj(a,null,null))}, +u0(a,b){a=A.aa(a,new Error()) +a.stack=b.i(0) +throw a}, +b5(a,b,c,d){var s,r=c?J.pR(a,d):J.pQ(a,d) +if(a!==0&&b!=null)for(s=0;s")) +for(s=J.Z(a);s.k();)r.push(s.gm()) +r.$flags=1 +return r}, +ak(a,b){var s,r +if(Array.isArray(a))return A.f(a.slice(0),b.h("u<0>")) +s=A.f([],b.h("u<0>")) +for(r=J.Z(a);r.k();)s.push(r.gm()) +return s}, +aO(a,b){var s=A.um(a,!1,b) +s.$flags=3 +return s}, +qh(a,b,c){var s,r,q,p,o +A.ab(b,"start") +s=c==null +r=!s +if(r){q=c-b +if(q<0)throw A.b(A.W(c,b,null,"end",null)) +if(q===0)return""}if(Array.isArray(a)){p=a +o=p.length +if(s)c=o +return A.q7(b>0||c0)a=J.e7(a,b) +s=A.ak(a,t.S) +return A.q7(s)}, +qg(a){return A.aR(a)}, +uF(a,b,c){var s=a.length +if(b>=s)return"" +return A.ux(a,b,c==null||c>s?s:c)}, +G(a,b,c,d,e){return new A.cx(a,A.om(a,d,b,e,c,""))}, +ox(a,b,c){var s=J.Z(b) +if(!s.k())return a +if(c.length===0){do a+=A.t(s.gm()) +while(s.k())}else{a+=A.t(s.gm()) +while(s.k())a=a+c+A.t(s.gm())}return a}, +i_(){var s,r,q=A.us() +if(q==null)throw A.b(A.a4("'Uri.base' is not supported")) +s=$.qs +if(s!=null&&q===$.qr)return s +r=A.bu(q) +$.qs=r +$.qr=q +return r}, +vC(a,b,c,d){var s,r,q,p,o,n="0123456789ABCDEF" +if(c===B.j){s=$.td() +s=s.b.test(b)}else s=!1 +if(s)return b +r=B.i.a4(b) +for(s=r.length,q=0,p="";q>>4&15]+n[o&15]}return p.charCodeAt(0)==0?p:p}, +l8(){return A.a5(new Error())}, +pD(a,b,c){var s="microsecond" +if(b>999)throw A.b(A.W(b,0,999,s,null)) +if(a<-864e13||a>864e13)throw A.b(A.W(a,-864e13,864e13,"millisecondsSinceEpoch",null)) +if(a===864e13&&b!==0)throw A.b(A.ad(b,s,"Time including microseconds is outside valid range")) +A.cV(c,"isUtc",t.y) +return a}, +tX(a){var s=Math.abs(a),r=a<0?"-":"" +if(s>=1000)return""+a +if(s>=100)return r+"0"+s +if(s>=10)return r+"00"+s +return r+"000"+s}, +pC(a){if(a>=100)return""+a +if(a>=10)return"0"+a +return"00"+a}, +h0(a){if(a>=10)return""+a +return"0"+a}, +pE(a,b){return new A.bx(a+1000*b)}, +oe(a,b){var s,r +for(s=0;s<5;++s){r=a[s] +if(r.b===b)return r}throw A.b(A.ad(b,"name","No enum value with that name"))}, +u_(a,b){var s,r,q=A.ao(t.N,b) +for(s=0;s<2;++s){r=a[s] +q.t(0,r.b,r)}return q}, +h9(a){if(typeof a=="number"||A.bQ(a)||a==null)return J.b1(a) +if(typeof a=="string")return JSON.stringify(a) +return A.q6(a)}, +pH(a,b){A.cV(a,"error",t.K) +A.cV(b,"stackTrace",t.l) +A.u0(a,b)}, +e8(a){return new A.fO(a)}, +J(a,b){return new A.bc(!1,null,b,a)}, +ad(a,b,c){return new A.bc(!0,a,b,c)}, +bT(a,b){return a}, +kJ(a,b){return new A.dk(null,null,!0,a,b,"Value not in range")}, +W(a,b,c,d,e){return new A.dk(b,c,!0,a,d,"Invalid value")}, +qa(a,b,c,d){if(ac)throw A.b(A.W(a,b,c,d,null)) +return a}, +uz(a,b,c,d){if(0>a||a>=d)A.E(A.hf(a,d,b,null,c)) +return a}, +bd(a,b,c){if(0>a||a>c)throw A.b(A.W(a,0,c,"start",null)) +if(b!=null){if(a>b||b>c)throw A.b(A.W(b,a,c,"end",null)) +return b}return c}, +ab(a,b){if(a<0)throw A.b(A.W(a,0,null,b,null)) +return a}, +pO(a,b){var s=b.b +return new A.ep(s,!0,a,null,"Index out of range")}, +hf(a,b,c,d,e){return new A.ep(b,!0,a,e,"Index out of range")}, +a4(a){return new A.eP(a)}, +qo(a){return new A.hT(a)}, +A(a){return new A.aI(a)}, +an(a){return new A.fW(a)}, +k2(a){return new A.ip(a)}, +aj(a,b,c){return new A.aF(a,b,c)}, +ud(a,b,c){var s,r +if(A.p9(a)){if(b==="("&&c===")")return"(...)" +return b+"..."+c}s=A.f([],t.s) +$.cU.push(a) +try{A.wi(a,s)}finally{$.cU.pop()}r=A.ox(b,s,", ")+c +return r.charCodeAt(0)==0?r:r}, +ol(a,b,c){var s,r +if(A.p9(a))return b+"..."+c +s=new A.aD(b) +$.cU.push(a) +try{r=s +r.a=A.ox(r.a,a,", ")}finally{$.cU.pop()}s.a+=c +r=s.a +return r.charCodeAt(0)==0?r:r}, +wi(a,b){var s,r,q,p,o,n,m,l=a.gq(a),k=0,j=0 +for(;;){if(!(k<80||j<3))break +if(!l.k())return +s=A.t(l.gm()) +b.push(s) +k+=s.length+2;++j}if(!l.k()){if(j<=5)return +r=b.pop() +q=b.pop()}else{p=l.gm();++j +if(!l.k()){if(j<=4){b.push(A.t(p)) +return}r=A.t(p) +q=b.pop() +k+=r.length+2}else{o=l.gm();++j +for(;l.k();p=o,o=n){n=l.gm();++j +if(j>100){for(;;){if(!(k>75&&j>3))break +k-=b.pop().length+2;--j}b.push("...") +return}}q=A.t(p) +r=A.t(o) +k+=r.length+q.length+4}}if(j>b.length+2){k+=5 +m="..."}else m=null +for(;;){if(!(k>80&&b.length>3))break +k-=b.pop().length+2 +if(m==null){k+=5 +m="..."}}if(m!=null)b.push(m) +b.push(q) +b.push(r)}, +eB(a,b,c,d){var s +if(B.f===c){s=J.aE(a) +b=J.aE(b) +return A.oy(A.c9(A.c9($.o7(),s),b))}if(B.f===d){s=J.aE(a) +b=J.aE(b) +c=J.aE(c) +return A.oy(A.c9(A.c9(A.c9($.o7(),s),b),c))}s=J.aE(a) +b=J.aE(b) +c=J.aE(c) +d=J.aE(d) +d=A.oy(A.c9(A.c9(A.c9(A.c9($.o7(),s),b),c),d)) +return d}, +xC(a){var s=A.t(a),r=$.rk +if(r==null)A.pd(s) +else r.$1(s)}, +qq(a){var s,r=null,q=new A.aD(""),p=A.f([-1],t.t) +A.uP(r,r,r,q,p) +p.push(q.a.length) +q.a+="," +A.uO(256,B.ad.kB(a),q) +s=q.a +return new A.hY(s.charCodeAt(0)==0?s:s,p,r).geN()}, +bu(a5){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3=null,a4=a5.length +if(a4>=5){s=((a5.charCodeAt(4)^58)*3|a5.charCodeAt(0)^100|a5.charCodeAt(1)^97|a5.charCodeAt(2)^116|a5.charCodeAt(3)^97)>>>0 +if(s===0)return A.qp(a4=14)r[7]=a4 +q=r[1] +if(q>=0)if(A.rq(a5,0,q,20,r)===20)r[7]=q +p=r[2]+1 +o=r[3] +n=r[4] +m=r[5] +l=r[6] +if(lq+3)){i=o>0 +if(!(i&&o+1===n)){if(!B.a.C(a5,"\\",n))if(p>0)h=B.a.C(a5,"\\",p-1)||B.a.C(a5,"\\",p-2) +else h=!1 +else h=!0 +if(!h){if(!(mn+2&&B.a.C(a5,"/..",m-3) +else h=!0 +if(!h)if(q===4){if(B.a.C(a5,"file",0)){if(p<=0){if(!B.a.C(a5,"/",n)){g="file:///" +s=3}else{g="file://" +s=2}a5=g+B.a.p(a5,n,a4) +m+=s +l+=s +a4=a5.length +p=7 +o=7 +n=7}else if(n===m){++l +f=m+1 +a5=B.a.aO(a5,n,m,"/");++a4 +m=f}j="file"}else if(B.a.C(a5,"http",0)){if(i&&o+3===n&&B.a.C(a5,"80",o+1)){l-=3 +e=n-3 +m-=3 +a5=B.a.aO(a5,o,n,"") +a4-=3 +n=e}j="http"}}else if(q===5&&B.a.C(a5,"https",0)){if(i&&o+4===n&&B.a.C(a5,"443",o+1)){l-=4 +e=n-4 +m-=4 +a5=B.a.aO(a5,o,n,"") +a4-=3 +n=e}j="https"}k=!h}}}}if(k)return new A.b7(a40)j=A.np(a5,0,q) +else{if(q===0)A.dT(a5,0,"Invalid empty scheme") +j=""}d=a3 +if(p>0){c=q+3 +b=c=c?0:a.charCodeAt(q) +m=n^48 +if(m<=9){if(o!==0||q===r){o=o*10+m +if(o<=255){++q +continue}A.hZ("each part must be in the range 0..255",a,r)}A.hZ("parts must not have leading zeros",a,r)}if(q===r){if(q===c)break +A.hZ(k,a,q)}l=p+1 +s&2&&A.y(d) +d[e+p]=o +if(n===46){if(l<4){++q +p=l +r=q +o=0 +continue}break}if(q===c){if(l===4)return +break}A.hZ(k,a,q) +p=l}A.hZ("IPv4 address should contain exactly 4 parts",a,q)}, +uR(a,b,c){var s +if(b===c)throw A.b(A.aj("Empty IP address",a,b)) +if(a.charCodeAt(b)===118){s=A.uS(a,b,c) +if(s!=null)throw A.b(s) +return!1}A.qt(a,b,c) +return!0}, +uS(a,b,c){var s,r,q,p,o="Missing hex-digit in IPvFuture address";++b +for(s=b;;s=r){if(s=97&&p<=102)continue +if(q===46){if(r-1===b)return new A.aF(o,a,r) +s=r +break}return new A.aF("Unexpected character",a,r-1)}if(s-1===b)return new A.aF(o,a,s) +return new A.aF("Missing '.' in IPvFuture address",a,s)}if(s===c)return new A.aF("Missing address in IPvFuture address, host, cursor",null,null) +for(;;){if((u.v.charCodeAt(a.charCodeAt(s))&16)!==0){++s +if(s=a3?0:a1.charCodeAt(p) +A:{k=l^48 +j=!1 +if(k<=9)i=k +else{h=l|32 +if(h>=97&&h<=102)i=h-87 +else break A +m=j}if(po){if(l===46){if(m){if(q<=6){A.uQ(a1,o,a3,s,q*2) +q+=2 +p=a3 +break}a0.$2(a,o)}break}g=q*2 +s[g]=B.b.M(n,8) +s[g+1]=n&255;++q +if(l===58){if(q<8){++p +o=p +n=0 +m=!0 +continue}a0.$2(a,p)}break}if(l===58){if(r<0){f=q+1;++p +r=q +q=f +o=p +continue}a0.$2("only one wildcard `::` is allowed",p)}if(r!==q-1)a0.$2("missing part",p) +break}if(p0){c=e*2 +b=16-d*2 +B.e.N(s,b,16,s,c) +B.e.em(s,c,b,0)}}return s}, +fw(a,b,c,d,e,f,g){return new A.fv(a,b,c,d,e,f,g)}, +al(a,b,c,d){var s,r,q,p,o,n,m,l,k=null +d=d==null?"":A.np(d,0,d.length) +s=A.r_(k,0,0) +a=A.qX(a,0,a==null?0:a.length,!1) +r=A.qZ(k,0,0,k) +q=A.qW(k,0,0) +p=A.no(k,d) +o=d==="file" +if(a==null)n=s.length!==0||p!=null||o +else n=!1 +if(n)a="" +n=a==null +m=!n +b=A.qY(b,0,b==null?0:b.length,c,d,m) +l=d.length===0 +if(l&&n&&!B.a.u(b,"/"))b=A.oS(b,!l||m) +else b=A.cT(b) +return A.fw(d,s,n&&B.a.u(b,"//")?"":a,p,b,r,q)}, +qT(a){if(a==="http")return 80 +if(a==="https")return 443 +return 0}, +dT(a,b,c){throw A.b(A.aj(c,a,b))}, +qS(a,b){return b?A.vy(a,!1):A.vx(a,!1)}, +vt(a,b){var s,r,q +for(s=a.length,r=0;r")),r=r.h("Q.E");s.k();){q=s.d +if(q==null)q=r.a(q) +if(B.a.H(q,A.G('["*/:<>?\\\\|]',!0,!1,!1,!1)))if(b)throw A.b(A.J("Illegal character in path",null)) +else throw A.b(A.a4("Illegal character in path: "+q))}}, +vu(a,b){var s,r="Illegal drive letter " +if(!(65<=a&&a<=90))s=97<=a&&a<=122 +else s=!0 +if(s)return +if(b)throw A.b(A.J(r+A.qg(a),null)) +else throw A.b(A.a4(r+A.qg(a)))}, +vx(a,b){var s=null,r=A.f(a.split("/"),t.s) +if(B.a.u(a,"/"))return A.al(s,s,r,"file") +else return A.al(s,s,r,s)}, +vy(a,b){var s,r,q,p,o="\\",n=null,m="file" +if(B.a.u(a,"\\\\?\\"))if(B.a.C(a,"UNC\\",4))a=B.a.aO(a,0,7,o) +else{a=B.a.L(a,4) +if(a.length<3||a.charCodeAt(1)!==58||a.charCodeAt(2)!==92)throw A.b(A.ad(a,"path","Windows paths with \\\\?\\ prefix must be absolute"))}else a=A.bk(a,"/",o) +s=a.length +if(s>1&&a.charCodeAt(1)===58){A.vu(a.charCodeAt(0),!0) +if(s===2||a.charCodeAt(2)!==92)throw A.b(A.ad(a,"path","Windows paths with drive letter must be absolute")) +r=A.f(a.split(o),t.s) +A.nm(r,!0,1) +return A.al(n,n,r,m)}if(B.a.u(a,o))if(B.a.C(a,o,1)){q=B.a.aY(a,o,2) +s=q<0 +p=s?B.a.L(a,2):B.a.p(a,2,q) +r=A.f((s?"":B.a.L(a,q+1)).split(o),t.s) +A.nm(r,!0,0) +return A.al(p,n,r,m)}else{r=A.f(a.split(o),t.s) +A.nm(r,!0,0) +return A.al(n,n,r,m)}else{r=A.f(a.split(o),t.s) +A.nm(r,!0,0) +return A.al(n,n,r,n)}}, +no(a,b){if(a!=null&&a===A.qT(b))return null +return a}, +qX(a,b,c,d){var s,r,q,p,o,n,m,l +if(a==null)return null +if(b===c)return"" +if(a.charCodeAt(b)===91){s=c-1 +if(a.charCodeAt(s)!==93)A.dT(a,b,"Missing end `]` to match `[` in host") +r=b+1 +q="" +if(a.charCodeAt(r)!==118){p=A.vv(a,r,s) +if(p=b&&s=b&&s=p){if(i==null)i=new A.aD("") +if(r=o){if(q==null)q=new A.aD("") +if(r")).av(0,"/")}else if(d!=null)throw A.b(A.J("Both path and pathSegments specified",null)) +else s=A.fx(a,b,c,128,!0,!0) +if(s.length===0){if(r)return"/"}else if(q&&!B.a.u(s,"/"))s="/"+s +return A.vz(s,e,f)}, +vz(a,b,c){var s=b.length===0 +if(s&&!c&&!B.a.u(a,"/")&&!B.a.u(a,"\\"))return A.oS(a,!s||c) +return A.cT(a)}, +qZ(a,b,c,d){if(a!=null)return A.fx(a,b,c,256,!0,!1) +return null}, +qW(a,b,c){if(a==null)return null +return A.fx(a,b,c,256,!0,!1)}, +oR(a,b,c){var s,r,q,p,o,n=b+2 +if(n>=a.length)return"%" +s=a.charCodeAt(b+1) +r=a.charCodeAt(n) +q=A.nR(s) +p=A.nR(r) +if(q<0||p<0)return"%" +o=q*16+p +if(o<127&&(u.v.charCodeAt(o)&1)!==0)return A.aR(c&&65<=o&&90>=o?(o|32)>>>0:o) +if(s>=97||r>=97)return B.a.p(a,b,b+3).toUpperCase() +return null}, +oQ(a){var s,r,q,p,o,n="0123456789ABCDEF" +if(a<=127){s=new Uint8Array(3) +s[0]=37 +s[1]=n.charCodeAt(a>>>4) +s[2]=n.charCodeAt(a&15)}else{if(a>2047)if(a>65535){r=240 +q=4}else{r=224 +q=3}else{r=192 +q=2}s=new Uint8Array(3*q) +for(p=0;--q,q>=0;r=128){o=B.b.jt(a,6*q)&63|r +s[p]=37 +s[p+1]=n.charCodeAt(o>>>4) +s[p+2]=n.charCodeAt(o&15) +p+=3}}return A.qh(s,0,null)}, +fx(a,b,c,d,e,f){var s=A.r1(a,b,c,d,e,f) +return s==null?B.a.p(a,b,c):s}, +r1(a,b,c,d,e,f){var s,r,q,p,o,n,m,l,k,j=null,i=u.v +for(s=!e,r=b,q=r,p=j;r=2&&A.qV(a.charCodeAt(0)))for(s=1;s127||(u.v.charCodeAt(r)&8)===0)break}return a}, +vB(a,b){if(a.kL("package")&&a.c==null)return A.rs(b,0,b.length) +return-1}, +vw(a,b){var s,r,q +for(s=0,r=0;r<2;++r){q=a.charCodeAt(b+r) +if(48<=q&&q<=57)s=s*16+q-48 +else{q|=32 +if(97<=q&&q<=102)s=s*16+q-87 +else throw A.b(A.J("Invalid URL encoding",null))}}return s}, +oT(a,b,c,d,e){var s,r,q,p,o=b +for(;;){if(!(o127)throw A.b(A.J("Illegal percent encoding in URI",null)) +if(r===37){if(o+3>q)throw A.b(A.J("Truncated URI",null)) +p.push(A.vw(a,o+1)) +o+=2}else p.push(r)}}return d.cX(p)}, +qV(a){var s=a|32 +return 97<=s&&s<=122}, +uP(a,b,c,d,e){d.a=d.a}, +qp(a,b,c){var s,r,q,p,o,n,m,l,k="Invalid MIME type",j=A.f([b-1],t.t) +for(s=a.length,r=b,q=-1,p=null;rb)throw A.b(A.aj(k,a,r)) +while(p!==44){j.push(r);++r +for(o=-1;r=0)j.push(o) +else{n=B.c.gD(j) +if(p!==44||r!==n+7||!B.a.C(a,"base64",n+1))throw A.b(A.aj("Expecting '='",a,r)) +break}}j.push(r) +m=r+1 +if((j.length&1)===1)a=B.ae.kV(a,m,s) +else{l=A.r1(a,m,s,256,!0,!1) +if(l!=null)a=B.a.aO(a,m,s,l)}return new A.hY(a,j,c)}, +uO(a,b,c){var s,r,q,p,o,n="0123456789ABCDEF" +for(s=b.length,r=0,q=0;q>>4)) +c.a+=o +o=A.aR(n.charCodeAt(p&15)) +c.a+=o}}if((r&4294967040)!==0)for(q=0;q255)throw A.b(A.ad(p,"non-byte value",null))}}, +rq(a,b,c,d,e){var s,r,q +for(s=b;s95)r=31 +q='\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe3\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x0e\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xea\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\n\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xeb\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\xeb\xeb\xeb\x8b\xeb\xeb\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\xeb\x83\xeb\xeb\x8b\xeb\x8b\xeb\xcd\x8b\xeb\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x92\x83\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\x8b\xeb\x8b\xeb\x8b\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xebD\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x12D\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\xe5\xe5\xe5\x05\xe5D\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe8\x8a\xe5\xe5\x05\xe5\x05\xe5\xcd\x05\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x8a\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05f\x05\xe5\x05\xe5\xac\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\xe5\xe5\xe5\x05\xe5D\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\xe5\x8a\xe5\xe5\x05\xe5\x05\xe5\xcd\x05\xe5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x8a\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05f\x05\xe5\x05\xe5\xac\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7D\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\xe7\xe7\xe7\xe7\xe7\xe7\xcd\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\xe7\x07\x07\x07\x07\x07\x07\x07\x07\x07\xe7\xe7\xe7\xe7\xe7\xac\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7D\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\xe7\xe7\xe7\xe7\xe7\xe7\xcd\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\xe7\x8a\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\xe7\xe7\xe7\xe7\xe7\xac\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\x05\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x10\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x12\n\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\v\n\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xec\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\xec\xec\xec\f\xec\xec\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\f\xec\xec\xec\xec\f\xec\f\xec\xcd\f\xec\f\f\f\f\f\f\f\f\f\xec\f\f\f\f\f\f\f\f\f\f\xec\f\xec\f\xec\f\xed\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\xed\xed\xed\r\xed\xed\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\xed\xed\xed\xed\r\xed\r\xed\xed\r\xed\r\r\r\r\r\r\r\r\r\xed\r\r\r\r\r\r\r\r\r\r\xed\r\xed\r\xed\r\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xea\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x0f\xea\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe1\xe1\x01\xe1\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xe1\xe9\xe1\xe1\x01\xe1\x01\xe1\xcd\x01\xe1\x01\x01\x01\x01\x01\x01\x01\x01\x01\t\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"\x01\xe1\x01\xe1\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x11\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xe9\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\v\t\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\x13\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xeb\xeb\v\xeb\xeb\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\v\xeb\xea\xeb\xeb\v\xeb\v\xeb\xcd\v\xeb\v\v\v\v\v\v\v\v\v\xea\v\v\v\v\v\v\v\v\v\v\xeb\v\xeb\v\xeb\xac\xf5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\xf5\x15\xf5\x15\x15\xf5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xf5\xf5\xf5\xf5\xf5\xf5'.charCodeAt(d*96+r) +d=q&31 +e[q>>>5]=s}return d}, +qL(a){if(a.b===7&&B.a.u(a.a,"package")&&a.c<=0)return A.rs(a.a,a.e,a.f) +return-1}, +rs(a,b,c){var s,r,q +for(s=b,r=0;s=1)return a.$1(b) +return a.$0()}, +vP(a,b,c,d){if(d>=2)return a.$2(b,c) +if(d===1)return a.$1(b) +return a.$0()}, +vQ(a,b,c,d,e){if(e>=3)return a.$3(b,c,d) +if(e===2)return a.$2(b,c) +if(e===1)return a.$1(b) +return a.$0()}, +vR(a,b,c,d,e,f){if(f>=4)return a.$4(b,c,d,e) +if(f===3)return a.$3(b,c,d) +if(f===2)return a.$2(b,c) +if(f===1)return a.$1(b) +return a.$0()}, +vS(a,b,c,d,e,f,g){if(g>=5)return a.$5(b,c,d,e,f) +if(g===4)return a.$4(b,c,d,e) +if(g===3)return a.$3(b,c,d) +if(g===2)return a.$2(b,c) +if(g===1)return a.$1(b) +return a.$0()}, +rj(a){return a==null||A.bQ(a)||typeof a=="number"||typeof a=="string"||t.gj.b(a)||t.E.b(a)||t.go.b(a)||t.dQ.b(a)||t.h7.b(a)||t.an.b(a)||t.bv.b(a)||t.h4.b(a)||t.gN.b(a)||t.w.b(a)||t.fd.b(a)}, +xp(a){if(A.rj(a))return a +return new A.nW(new A.dG(t.hg)).$1(a)}, +p1(a,b,c){return a[b].apply(a,c)}, +e3(a,b){var s,r +if(b==null)return new a() +if(b instanceof Array)switch(b.length){case 0:return new a() +case 1:return new a(b[0]) +case 2:return new a(b[0],b[1]) +case 3:return new a(b[0],b[1],b[2]) +case 4:return new a(b[0],b[1],b[2],b[3])}s=[null] +B.c.aI(s,b) +r=a.bind.apply(a,s) +String(r) +return new r()}, +V(a,b){var s=new A.n($.m,b.h("n<0>")),r=new A.a7(s,b.h("a7<0>")) +a.then(A.ck(new A.o0(r),1),A.ck(new A.o1(r),1)) +return s}, +ri(a){return a==null||typeof a==="boolean"||typeof a==="number"||typeof a==="string"||a instanceof Int8Array||a instanceof Uint8Array||a instanceof Uint8ClampedArray||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array||a instanceof ArrayBuffer||a instanceof DataView}, +ry(a){if(A.ri(a))return a +return new A.nM(new A.dG(t.hg)).$1(a)}, +nW:function nW(a){this.a=a}, +o0:function o0(a){this.a=a}, +o1:function o1(a){this.a=a}, +nM:function nM(a){this.a=a}, +rG(a,b){return Math.max(a,b)}, +xG(a){return Math.sqrt(a)}, +xF(a){return Math.sin(a)}, +x7(a){return Math.cos(a)}, +xM(a){return Math.tan(a)}, +wI(a){return Math.acos(a)}, +wJ(a){return Math.asin(a)}, +x3(a){return Math.atan(a)}, +mY:function mY(a){this.a=a}, +d4:function d4(){}, +h1:function h1(){}, +hs:function hs(){}, +hB:function hB(){}, +hW:function hW(){}, +tY(a,b){var s=new A.ej(a,b,A.ao(t.S,t.aR),A.eN(null,null,!0,t.al),new A.a7(new A.n($.m,t.D),t.h)) +s.hW(a,!1,b) +return s}, +ej:function ej(a,b,c,d,e){var _=this +_.a=a +_.c=b +_.d=0 +_.e=c +_.f=d +_.r=!1 +_.w=e}, +jS:function jS(a){this.a=a}, +jT:function jT(a,b){this.a=a +this.b=b}, +iC:function iC(a,b){this.a=a +this.b=b}, +fX:function fX(){}, +h5:function h5(a){this.a=a}, +h4:function h4(){}, +jU:function jU(a){this.a=a}, +jV:function jV(a){this.a=a}, +bZ:function bZ(){}, +ar:function ar(a,b){this.a=a +this.b=b}, +bf:function bf(a,b){this.a=a +this.b=b}, +aQ:function aQ(a){this.a=a}, +bo:function bo(a,b,c){this.a=a +this.b=b +this.c=c}, +bw:function bw(a){this.a=a}, +dh:function dh(a,b){this.a=a +this.b=b}, +cC:function cC(a,b){this.a=a +this.b=b}, +bW:function bW(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +c2:function c2(a){this.a=a}, +bp:function bp(a,b){this.a=a +this.b=b}, +c1:function c1(a,b){this.a=a +this.b=b}, +c4:function c4(a,b){this.a=a +this.b=b}, +bV:function bV(a,b){this.a=a +this.b=b}, +c5:function c5(a){this.a=a}, +c3:function c3(a,b){this.a=a +this.b=b}, +bF:function bF(a){this.a=a}, +bI:function bI(a){this.a=a}, +uC(a,b,c){var s=null,r=t.S,q=A.f([],t.t) +r=new A.kO(a,!1,!0,A.ao(r,t.x),A.ao(r,t.g1),q,new A.fp(s,s,t.dn),A.op(t.gw),new A.a7(new A.n($.m,t.D),t.h),A.eN(s,s,!1,t.bw)) +r.hY(a,!1,!0) +return r}, +kO:function kO(a,b,c,d,e,f,g,h,i,j){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.f=_.e=0 +_.r=e +_.w=f +_.x=g +_.y=!1 +_.z=h +_.Q=i +_.as=j}, +kT:function kT(a){this.a=a}, +kU:function kU(a,b){this.a=a +this.b=b}, +kV:function kV(a,b){this.a=a +this.b=b}, +kP:function kP(a,b){this.a=a +this.b=b}, +kQ:function kQ(a,b){this.a=a +this.b=b}, +kS:function kS(a,b){this.a=a +this.b=b}, +kR:function kR(a){this.a=a}, +fj:function fj(a,b,c){this.a=a +this.b=b +this.c=c}, +i7:function i7(a){this.a=a}, +m1:function m1(a,b){this.a=a +this.b=b}, +m2:function m2(a,b){this.a=a +this.b=b}, +m_:function m_(){}, +lW:function lW(a,b){this.a=a +this.b=b}, +lX:function lX(){}, +lY:function lY(){}, +lV:function lV(){}, +m0:function m0(){}, +lZ:function lZ(){}, +dv:function dv(a,b){this.a=a +this.b=b}, +bK:function bK(a,b){this.a=a +this.b=b}, +xD(a,b){var s,r,q={} +q.a=s +q.a=null +s=new A.bU(new A.a1(new A.n($.m,b.h("n<0>")),b.h("a1<0>")),A.f([],t.bT),b.h("bU<0>")) +q.a=s +r=t.X +A.rN(new A.o2(q,a,b),null,A.uk([B.U,s],r,r),t.H) +return q.a}, +p2(){var s=$.m.j(0,B.U) +if(s instanceof A.bU&&s.c)throw A.b(B.E)}, +o2:function o2(a,b,c){this.a=a +this.b=b +this.c=c}, +bU:function bU(a,b,c){var _=this +_.a=a +_.b=b +_.c=!1 +_.$ti=c}, +ec:function ec(){}, +aq:function aq(){}, +ea:function ea(a,b){this.a=a +this.b=b}, +d2:function d2(a,b){this.a=a +this.b=b}, +rb(a){return"SAVEPOINT s"+a}, +r9(a){return"RELEASE s"+a}, +ra(a){return"ROLLBACK TO s"+a}, +jJ:function jJ(){}, +kG:function kG(){}, +ls:function ls(){}, +kB:function kB(){}, +jM:function jM(){}, +hA:function hA(){}, +k0:function k0(){}, +id:function id(){}, +ma:function ma(a,b,c){this.a=a +this.b=b +this.c=c}, +mf:function mf(a,b,c){this.a=a +this.b=b +this.c=c}, +md:function md(a,b,c){this.a=a +this.b=b +this.c=c}, +me:function me(a,b,c){this.a=a +this.b=b +this.c=c}, +mc:function mc(a,b,c){this.a=a +this.b=b +this.c=c}, +mb:function mb(a,b){this.a=a +this.b=b}, +iQ:function iQ(){}, +fn:function fn(a,b,c,d,e,f,g,h,i){var _=this +_.y=a +_.z=null +_.Q=b +_.as=c +_.at=d +_.ax=e +_.ay=f +_.ch=g +_.e=h +_.a=i +_.b=0 +_.d=_.c=!1}, +n9:function n9(a){this.a=a}, +na:function na(a){this.a=a}, +h2:function h2(){}, +jR:function jR(a,b){this.a=a +this.b=b}, +jQ:function jQ(a){this.a=a}, +ie:function ie(a,b){var _=this +_.e=a +_.a=b +_.b=0 +_.d=_.c=!1}, +f4:function f4(a,b,c){var _=this +_.e=a +_.f=null +_.r=b +_.a=c +_.b=0 +_.d=_.c=!1}, +my:function my(a,b){this.a=a +this.b=b}, +q9(a,b){var s,r,q,p=A.ao(t.N,t.S) +for(s=a.length,r=0;r3)throw A.b("Expected two or three arguments to regexp") +s=a.j(0,0) +q=a.j(0,1) +if(s==null||q==null)return null +if(typeof s!="string"||typeof q!="string")throw A.b("Expected two strings as parameters to regexp") +if(g===3){p=a.j(0,2) +if(A.bv(p)){k=(p&1)===1 +j=(p&2)!==2 +i=(p&4)===4 +h=(p&8)===8}}r=null +try{o=k +n=j +m=i +r=A.G(s,n,h,o,m)}catch(l){if(A.H(l) instanceof A.aF)throw A.b("Invalid regex") +else throw l}o=r.b +return o.test(q)}, +vX(a){var s,r,q=a.a.b +if(q<2||q>3)throw A.b("Expected 2 or 3 arguments to moor_contains") +s=a.j(0,0) +r=a.j(0,1) +if(s==null||r==null)return null +if(typeof s!="string"||typeof r!="string")throw A.b("First two args to contains must be strings") +return q===3&&a.j(0,2)===1?B.a.H(s,r):B.a.H(s.toLowerCase(),r.toLowerCase())}, +k1:function k1(){}, +nH:function nH(a){this.a=a}, +ho:function ho(a){var _=this +_.a=$ +_.b=!1 +_.d=null +_.e=a}, +kt:function kt(a,b){this.a=a +this.b=b}, +ku:function ku(a,b){this.a=a +this.b=b}, +bq:function bq(){this.a=null}, +kw:function kw(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e}, +kx:function kx(a,b,c){this.a=a +this.b=b +this.c=c}, +ky:function ky(a,b){this.a=a +this.b=b}, +uV(a,b,c,d){var s,r=null,q=new A.hO(t.a7),p=t.X,o=A.eN(r,r,!1,p),n=A.eN(r,r,!1,p),m=A.pM(new A.at(n,A.r(n).h("at<1>")),new A.dP(o),!0,p) +q.a=m +p=A.pM(new A.at(o,A.r(o).h("at<1>")),new A.dP(n),!0,p) +q.b=p +s=new A.i7(A.or(c)) +a.onmessage=A.bi(new A.lS(b,q,d,s)) +m=m.b +m===$&&A.x() +new A.at(m,A.r(m).h("at<1>")).eB(new A.lT(d,s,a),new A.lU(b,a)) +return p}, +lS:function lS(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +lT:function lT(a,b,c){this.a=a +this.b=b +this.c=c}, +lU:function lU(a,b){this.a=a +this.b=b}, +jN:function jN(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=null}, +jP:function jP(a){this.a=a}, +jO:function jO(a,b){this.a=a +this.b=b}, +or(a){var s +A:{if(a<=0){s=B.p +break A}if(1===a){s=B.aJ +break A}if(2===a){s=B.aK +break A}if(3===a){s=B.aL +break A}if(a>3){s=B.q +break A}s=A.E(A.e8(null))}return s}, +q8(a){if("v" in a)return A.or(A.B(A.Y(a.v))) +else return B.p}, +oB(a){var s,r,q,p,o,n,m,l,k,j=A.a2(a.type),i=a.payload +A:{if("Error"===j){s=new A.dz(A.a2(A.a9(i))) +break A}if("ServeDriftDatabase"===j){A.a9(i) +r=A.q8(i) +s=A.bu(A.a2(i.sqlite)) +q=A.a9(i.port) +p=A.oe(B.ax,A.a2(i.storage)) +o=A.a2(i.database) +n=A.oU(i.initPort) +m=r.c +l=m<2||A.bh(i.migrations) +s=new A.dn(s,q,p,o,n,r,l,m<3||A.bh(i.new_serialization)) +break A}if("StartFileSystemServer"===j){s=new A.eL(A.a9(i)) +break A}if("RequestCompatibilityCheck"===j){s=new A.dl(A.a2(i)) +break A}if("DedicatedWorkerCompatibilityResult"===j){A.a9(i) +k=A.f([],t.L) +if("existing" in i)B.c.aI(k,A.pG(t.c.a(i.existing))) +s=A.bh(i.supportsNestedWorkers) +q=A.bh(i.canAccessOpfs) +p=A.bh(i.supportsSharedArrayBuffers) +o=A.bh(i.supportsIndexedDb) +n=A.bh(i.indexedDbExists) +m=A.bh(i.opfsExists) +m=new A.ei(s,q,p,o,k,A.q8(i),n,m) +s=m +break A}if("SharedWorkerCompatibilityResult"===j){s=A.uD(t.c.a(i)) +break A}if("DeleteDatabase"===j){s=i==null?A.oV(i):i +t.c.a(s) +q=$.pl().j(0,A.a2(s[0])) +q.toString +s=new A.h3(new A.ag(q,A.a2(s[1]))) +break A}s=A.E(A.J("Unknown type "+j,null))}return s}, +uD(a){var s,r,q=new A.l1(a) +if(a.length>5){s=A.pG(t.c.a(a[5])) +r=a.length>6?A.or(A.B(A.Y(a[6]))):B.p}else{s=B.y +r=B.p}return new A.c6(q.$1(0),q.$1(1),q.$1(2),s,r,q.$1(3),q.$1(4))}, +pG(a){var s,r,q=A.f([],t.L),p=B.c.bx(a,t.m),o=p.$ti +p=new A.b4(p,p.gl(0),o.h("b4")) +o=o.h("w.E") +while(p.k()){s=p.d +if(s==null)s=o.a(s) +r=$.pl().j(0,A.a2(s.l)) +r.toString +q.push(new A.ag(r,A.a2(s.n)))}return q}, +pF(a){var s,r,q,p,o=A.f([],t.W) +for(s=a.length,r=0;r"))) +while(i.k()){l=i.gm() +if(J.am(l.name,a)){q=!0 +s=1 +break A}}q=!1 +s=1 +break +case 8:k=n.open(a,1) +k.onupgradeneeded=A.bi(new A.nK(g,k)) +s=10 +return A.c(A.jj(k,t.m),$async$e4) +case 10:j=c +if(g.a==null)g.a=!0 +j.close() +s=g.a===!1?11:12 +break +case 11:s=13 +return A.c(A.jj(n.deleteDatabase(a),t.X),$async$e4) +case 13:case 12:p=2 +s=6 +break +case 4:p=3 +f=o.pop() +s=6 +break +case 3:s=2 +break +case 6:i=g.a +q=i===!0 +s=1 +break +case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$e4,r)}, +nN(a){var s=0,r=A.k(t.H),q +var $async$nN=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:q=v.G +s="indexedDB" in q?2:3 +break +case 2:s=4 +return A.c(A.jj(A.a9(q.indexedDB).deleteDatabase(a),t.X),$async$nN) +case 4:case 3:return A.i(null,r)}}) +return A.j($async$nN,r)}, +iY(){var s=null +return A.xB()}, +xB(){var s=0,r=A.k(t.A),q,p=2,o=[],n,m,l,k,j,i,h +var $async$iY=A.l(function(a,b){if(a===1){o.push(b) +s=p}for(;;)switch(s){case 0:j=null +i=A.pg() +if(i==null){q=null +s=1 +break}m=t.m +s=3 +return A.c(A.V(i.getDirectory(),m),$async$iY) +case 3:n=b +p=5 +l=j +if(l==null)l={} +s=8 +return A.c(A.V(n.getDirectoryHandle("drift_db",l),m),$async$iY) +case 8:m=b +q=m +s=1 +break +p=2 +s=7 +break +case 5:p=4 +h=o.pop() +q=null +s=1 +break +s=7 +break +case 4:s=2 +break +case 7:case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$iY,r)}, +e6(){var s=0,r=A.k(t.q),q,p=2,o=[],n=[],m,l,k,j,i,h,g,f +var $async$e6=A.l(function(a,b){if(a===1){o.push(b) +s=p}for(;;)switch(s){case 0:s=3 +return A.c(A.iY(),$async$e6) +case 3:g=b +if(g==null){q=B.x +s=1 +break}j=t.cO +if(!(v.G.Symbol.asyncIterator in g))A.E(A.J("Target object does not implement the async iterable interface",null)) +m=new A.fc(new A.nZ(),new A.e9(g,j),j.h("fc")) +l=A.f([],t.s) +j=new A.dO(A.cV(m,"stream",t.K)) +p=4 +i=t.m +case 7:s=9 +return A.c(j.k(),$async$e6) +case 9:if(!b){s=8 +break}k=j.gm() +s=J.am(k.kind,"directory")?10:11 +break +case 10:p=13 +s=16 +return A.c(A.V(k.getFileHandle("database"),i),$async$e6) +case 16:J.o8(l,k.name) +p=4 +s=15 +break +case 13:p=12 +f=o.pop() +s=15 +break +case 12:s=4 +break +case 15:case 11:s=7 +break +case 8:n.push(6) +s=5 +break +case 4:n=[2] +case 5:p=2 +s=17 +return A.c(j.J(),$async$e6) +case 17:s=n.pop() +break +case 6:q=l +s=1 +break +case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$e6,r)}, +fG(a){return A.x9(a)}, +x9(a){var s=0,r=A.k(t.H),q,p=2,o=[],n,m,l,k,j +var $async$fG=A.l(function(b,c){if(b===1){o.push(c) +s=p}for(;;)switch(s){case 0:k=A.pg() +if(k==null){s=1 +break}m=t.m +s=3 +return A.c(A.V(k.getDirectory(),m),$async$fG) +case 3:n=c +p=5 +s=8 +return A.c(A.V(n.getDirectoryHandle("drift_db"),m),$async$fG) +case 8:n=c +s=9 +return A.c(A.V(n.removeEntry(a,{recursive:!0}),t.X),$async$fG) +case 9:p=2 +s=7 +break +case 5:p=4 +j=o.pop() +s=7 +break +case 4:s=2 +break +case 7:case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$fG,r)}, +jj(a,b){var s=new A.n($.m,b.h("n<0>")),r=new A.a1(s,b.h("a1<0>")) +A.aL(a,"success",new A.jm(r,a,b),!1) +A.aL(a,"error",new A.jn(r,a),!1) +A.aL(a,"blocked",new A.jo(r,a),!1) +return s}, +nK:function nK(a,b){this.a=a +this.b=b}, +nZ:function nZ(){}, +h6:function h6(a,b){this.a=a +this.b=b}, +k_:function k_(a,b){this.a=a +this.b=b}, +jX:function jX(a){this.a=a}, +jW:function jW(a){this.a=a}, +jY:function jY(a,b,c){this.a=a +this.b=b +this.c=c}, +jZ:function jZ(a,b,c){this.a=a +this.b=b +this.c=c}, +mn:function mn(a,b){this.a=a +this.b=b}, +dm:function dm(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=c}, +kM:function kM(a){this.a=a}, +lF:function lF(a,b){this.a=a +this.b=b}, +jm:function jm(a,b,c){this.a=a +this.b=b +this.c=c}, +jn:function jn(a,b){this.a=a +this.b=b}, +jo:function jo(a,b){this.a=a +this.b=b}, +kW:function kW(a,b){this.a=a +this.b=null +this.c=b}, +l0:function l0(a){this.a=a}, +kX:function kX(a,b){this.a=a +this.b=b}, +l_:function l_(a,b,c){this.a=a +this.b=b +this.c=c}, +kY:function kY(a){this.a=a}, +kZ:function kZ(a,b,c){this.a=a +this.b=b +this.c=c}, +cc:function cc(a,b){this.a=a +this.b=b}, +bO:function bO(a,b){this.a=a +this.b=b}, +i4:function i4(a,b,c,d,e){var _=this +_.e=a +_.f=null +_.r=b +_.w=c +_.x=d +_.a=e +_.b=0 +_.d=_.c=!1}, +iT:function iT(a,b,c,d,e,f,g){var _=this +_.Q=a +_.as=b +_.at=c +_.b=null +_.d=_.c=!1 +_.e=d +_.f=e +_.r=f +_.x=g +_.y=$ +_.a=!1}, +pB(a){return new A.fY(a,".")}, +p_(a){return a}, +rt(a,b){var s,r,q,p,o,n,m,l +for(s=b.length,r=1;r=1;s=q){q=s-1 +if(b[q]!=null)break}p=new A.aD("") +o=a+"(" +p.a=o +n=A.O(b) +m=n.h("cD<1>") +l=new A.cD(b,0,s,m) +l.hZ(b,0,s,n.c) +m=o+new A.D(l,new A.nI(),m.h("D")).av(0,", ") +p.a=m +p.a=m+("): part "+(r-1)+" was null, but part "+r+" was not.") +throw A.b(A.J(p.i(0),null))}}, +fY:function fY(a,b){this.a=a +this.b=b}, +js:function js(){}, +jt:function jt(){}, +nI:function nI(){}, +kp:function kp(){}, +di(a,b){var s,r,q,p,o,n=b.hF(a) +b.aZ(a) +if(n!=null)a=B.a.L(a,n.length) +s=t.s +r=A.f([],s) +q=A.f([],s) +s=a.length +if(s!==0&&b.au(a.charCodeAt(0))){q.push(a[0]) +p=1}else{q.push("") +p=0}for(o=p;o"))}, +o_:function o_(){}, +ju:function ju(){}, +hJ:function hJ(a,b,c){this.d=a +this.a=b +this.c=c}, +bs:function bs(a,b){this.a=a +this.b=b}, +n3:function n3(a){this.a=a +this.b=-1}, +iG:function iG(){}, +iH:function iH(){}, +iJ:function iJ(){}, +iK:function iK(){}, +kC:function kC(a,b){this.a=a +this.b=b}, +d3:function d3(){}, +cw:function cw(a){this.a=a}, +ca(a){return new A.aJ(a)}, +pu(a,b){var s,r,q,p +if(b==null)b=$.fI() +for(s=a.length,r=a.$flags|0,q=0;q")),r=new A.a1(s,b.h("a1<0>")) +A.aL(a,"success",new A.jk(r,a,b),!1) +A.aL(a,"error",new A.jl(r,a),!1) +return s}, +tW(a,b){var s=new A.n($.m,b.h("n<0>")),r=new A.a1(s,b.h("a1<0>")) +A.aL(a,"success",new A.jp(r,a,b),!1) +A.aL(a,"error",new A.jq(r,a),!1) +A.aL(a,"blocked",new A.jr(r),!1) +return s}, +cK:function cK(a,b){var _=this +_.c=_.b=_.a=null +_.d=a +_.$ti=b}, +mo:function mo(a,b){this.a=a +this.b=b}, +mp:function mp(a,b){this.a=a +this.b=b}, +jk:function jk(a,b,c){this.a=a +this.b=b +this.c=c}, +jl:function jl(a,b){this.a=a +this.b=b}, +jp:function jp(a,b,c){this.a=a +this.b=b +this.c=c}, +jq:function jq(a,b){this.a=a +this.b=b}, +jr:function jr(a){this.a=a}, +lL:function lL(a){this.a=a}, +lM:function lM(a){this.a=a}, +lO(a){var s=0,r=A.k(t.ab),q,p,o +var $async$lO=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:p=v.G +o=A +s=3 +return A.c(A.V(p.fetch(new p.URL(a,A.a9(p.location).href),null),t.m),$async$lO) +case 3:q=o.lN(c,null) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$lO,r)}, +lN(a,b){var s=0,r=A.k(t.ab),q,p,o,n,m +var $async$lN=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:p=new A.fZ(A.ao(t.S,t.b9)) +o=A +n=A +m=A +s=3 +return A.c(new A.lL(p).d7(a),$async$lN) +case 3:q=new o.i6(new n.lP(m.uU(d,p))) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$lN,r)}, +i6:function i6(a){this.a=a}, +dy:function dy(a,b,c,d){var _=this +_.d=a +_.e=b +_.b=c +_.a=d}, +i5:function i5(a,b){this.a=a +this.b=b +this.c=0}, +qb(a){var s=J.am(a.byteLength,8) +if(!s)throw A.b(A.J("Must be 8 in length",null)) +s=v.G.Int32Array +return new A.kL(t.ha.a(A.e3(s,[a])))}, +un(a){return B.h}, +uo(a){var s=a.b +return new A.R(s.getInt32(0,!1),s.getInt32(4,!1),s.getInt32(8,!1))}, +up(a){var s=a.b +return new A.aW(B.j.cX(new Uint8Array(A.fC(A.ow(a.a,16,s.getInt32(12,!1))))),s.getInt32(0,!1),s.getInt32(4,!1),s.getInt32(8,!1))}, +kL:function kL(a){this.b=a}, +br:function br(a,b,c){this.a=a +this.b=b +this.c=c}, +ac:function ac(a,b,c,d,e){var _=this +_.c=a +_.d=b +_.a=c +_.b=d +_.$ti=e}, +bC:function bC(){}, +b2:function b2(){}, +R:function R(a,b,c){this.a=a +this.b=b +this.c=c}, +aW:function aW(a,b,c,d){var _=this +_.d=a +_.a=b +_.b=c +_.c=d}, +i3(a){var s=0,r=A.k(t.ei),q,p,o,n,m,l,k,j +var $async$i3=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:l=t.m +s=3 +return A.c(A.V(A.pf().getDirectory(),l),$async$i3) +case 3:k=c +j=A.pc(a.root) +p=J.Z(j.a),o=new A.cG(p,j.b) +case 4:if(!o.k()){s=5 +break}s=6 +return A.c(A.V(k.getDirectoryHandle(p.gm(),{create:!0}),l),$async$i3) +case 6:k=c +s=4 +break +case 5:l=t.cT +p=A.qb(a.synchronizationBuffer) +o=a.communicationBuffer +n=A.qd(o,65536,2048) +m=v.G.Uint8Array +q=new A.eQ(p,new A.br(o,n,t.Z.a(A.e3(m,[o]))),k,A.ao(t.S,l),A.op(l)) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$i3,r)}, +iF:function iF(a,b,c){this.a=a +this.b=b +this.c=c}, +eQ:function eQ(a,b,c,d,e){var _=this +_.a=a +_.b=b +_.c=c +_.d=0 +_.e=!1 +_.f=d +_.r=e}, +dK:function dK(a,b,c,d,e,f,g){var _=this +_.a=a +_.b=b +_.c=c +_.d=d +_.e=e +_.f=f +_.r=g +_.w=!1 +_.x=null}, +v9(a){var s=new A.f9(a,new A.a1(new A.n($.m,t.D),t.F),a.objectStore("files"),a.objectStore("blocks")) +s.i0(a) +return s}, +hg(a,b){var s=0,r=A.k(t.bd),q,p,o,n,m,l +var $async$hg=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:p=t.N +o=new A.j6(a) +n=A.ok(null) +m=$.fI() +l=new A.d7(o,n,new A.cy(t.au),A.op(p),A.ao(p,t.S),m,"indexeddb") +l.r=!1 +s=3 +return A.c(o.d8(),$async$hg) +case 3:s=4 +return A.c(l.bR(),$async$hg) +case 4:q=l +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$hg,r)}, +j6:function j6(a){this.a=null +this.b=a}, +j9:function j9(a){this.a=a}, +j8:function j8(a,b,c){this.a=a +this.b=b +this.c=c}, +j7:function j7(a){this.a=a}, +f9:function f9(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=!1 +_.d=c +_.e=d}, +mT:function mT(a){this.a=a}, +mU:function mU(a){this.a=a}, +mS:function mS(a){this.a=a}, +mV:function mV(a,b,c){this.a=a +this.b=b +this.c=c}, +mX:function mX(a,b){this.a=a +this.b=b}, +mW:function mW(a,b){this.a=a +this.b=b}, +mz:function mz(a,b,c){this.a=a +this.b=b +this.c=c}, +mA:function mA(a,b){this.a=a +this.b=b}, +iB:function iB(a,b){this.a=a +this.b=b}, +d7:function d7(a,b,c,d,e,f,g){var _=this +_.d=a +_.f=_.e=!1 +_.r=!0 +_.w=b +_.x=c +_.y=d +_.z=e +_.b=f +_.a=g}, +kj:function kj(a,b,c){this.a=a +this.b=b +this.c=c}, +kk:function kk(){}, +ki:function ki(a,b){this.a=a +this.b=b}, +iu:function iu(a,b,c){this.a=a +this.b=b +this.c=c}, +mR:function mR(a,b){this.a=a +this.b=b}, +au:function au(){}, +f6:function f6(a,b){var _=this +_.w=a +_.d=b +_.c=_.b=_.a=null}, +f_:function f_(a,b,c){var _=this +_.w=a +_.x=b +_.d=c +_.c=_.b=_.a=null}, +dC:function dC(a,b,c){var _=this +_.w=a +_.x=b +_.d=c +_.c=_.b=_.a=null}, +dU:function dU(a,b,c,d,e){var _=this +_.w=a +_.x=b +_.y=c +_.z=d +_.d=e +_.c=_.b=_.a=null}, +hL(a,b){var s=0,r=A.k(t.e1),q,p,o,n,m,l,k,j +var $async$hL=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:j=A.pf() +if(j==null)throw A.b(A.ca(1)) +p=t.m +s=3 +return A.c(A.V(j.getDirectory(),p),$async$hL) +case 3:o=d +n=A.pc(a),m=J.Z(n.a),n=new A.cG(m,n.b),l=null +case 4:if(!n.k()){s=6 +break}s=7 +return A.c(A.V(o.getDirectoryHandle(m.gm(),{create:!0}),p),$async$hL) +case 7:k=d +case 5:l=o,o=k +s=4 +break +case 6:q=new A.ag(l,o) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$hL,r)}, +l5(a){var s=0,r=A.k(t.m),q +var $async$l5=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.hL(a,!0),$async$l5) +case 3:q=c.b +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$l5,r)}, +l3(a){var s=0,r=A.k(t.gW),q,p +var $async$l3=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:if(A.pf()==null)throw A.b(A.ca(1)) +p=A +s=3 +return A.c(A.l5(a),$async$l3) +case 3:q=p.l2(c,!1,"simple-opfs") +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$l3,r)}, +l2(a,b,c){var s=0,r=A.k(t.gW),q,p,o,n +var $async$l2=A.l(function(d,e){if(d===1)return A.h(e,r) +for(;;)switch(s){case 0:p=A.ok(null) +o=$.fI() +n=new A.dq(p,o,c) +s=3 +return A.c(n.bC(a,!1),$async$l2) +case 3:q=n +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$l2,r)}, +d6:function d6(a,b,c){this.c=a +this.a=b +this.b=c}, +dq:function dq(a,b,c){var _=this +_.d=null +_.e=a +_.b=b +_.a=c}, +l4:function l4(a,b){this.a=a +this.b=b}, +iL:function iL(a,b,c){var _=this +_.a=a +_.b=b +_.c=c +_.d=0}, +n0:function n0(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +uU(a,b){var s=A.a9(a.exports.memory) +b.b!==$&&A.iZ() +b.b=s +s=new A.lA(s,b,a.exports) +s.i_(a,b) +return s}, +oD(a,b){var s,r=A.bE(a.buffer,b,null) +for(s=0;r[s]!==0;)++s +return s}, +cd(a,b,c){var s=a.buffer +return B.j.cX(A.bE(s,b,c==null?A.oD(a,b):c))}, +oC(a,b,c){var s +if(b===0)return null +s=a.buffer +return B.j.cX(A.bE(s,b,c==null?A.oD(a,b):c))}, +qu(a,b,c){var s=new Uint8Array(c) +B.e.b2(s,0,A.bE(a.buffer,b,c)) +return s}, +lA:function lA(a,b,c){var _=this +_.b=a +_.c=b +_.d=c +_.w=_.r=null}, +lB:function lB(a){this.a=a}, +lC:function lC(a){this.a=a}, +lD:function lD(a){this.a=a}, +lE:function lE(a){this.a=a}, +tQ(a){var s,r,q=u.q +if(a.length===0)return new A.bm(A.aO(A.f([],t.J),t.a)) +s=$.pq() +if(B.a.H(a,s)){s=B.a.bm(a,s) +r=A.O(s) +return new A.bm(A.aO(new A.aG(new A.aK(s,new A.ja(),r.h("aK<1>")),A.xQ(),r.h("aG<1,a0>")),t.a))}if(!B.a.H(a,q))return new A.bm(A.aO(A.f([A.qm(a)],t.J),t.a)) +return new A.bm(A.aO(new A.D(A.f(a.split(q),t.s),A.xP(),t.fe),t.a))}, +bm:function bm(a){this.a=a}, +ja:function ja(){}, +jf:function jf(){}, +je:function je(){}, +jc:function jc(){}, +jd:function jd(a){this.a=a}, +jb:function jb(a){this.a=a}, +u9(a){return A.pJ(a)}, +pJ(a){return A.hc(a,new A.ka(a))}, +u8(a){return A.u5(a)}, +u5(a){return A.hc(a,new A.k8(a))}, +u2(a){return A.hc(a,new A.k5(a))}, +u6(a){return A.u3(a)}, +u3(a){return A.hc(a,new A.k6(a))}, +u7(a){return A.u4(a)}, +u4(a){return A.hc(a,new A.k7(a))}, +hd(a){if(B.a.H(a,$.rS()))return A.bu(a) +else if(B.a.H(a,$.rT()))return A.qS(a,!0) +else if(B.a.u(a,"/"))return A.qS(a,!1) +if(B.a.H(a,"\\"))return $.tB().ht(a) +return A.bu(a)}, +hc(a,b){var s,r +try{s=b.$0() +return s}catch(r){if(A.H(r) instanceof A.aF)return new A.bt(A.al(null,"unparsed",null,null),a) +else throw r}}, +M:function M(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +ka:function ka(a){this.a=a}, +k8:function k8(a){this.a=a}, +k9:function k9(a){this.a=a}, +k5:function k5(a){this.a=a}, +k6:function k6(a){this.a=a}, +k7:function k7(a){this.a=a}, +hp:function hp(a){this.a=a +this.b=$}, +ql(a){if(t.a.b(a))return a +if(a instanceof A.bm)return a.hs() +return new A.hp(new A.lo(a))}, +qm(a){var s,r,q +try{if(a.length===0){r=A.qi(A.f([],t.e),null) +return r}if(B.a.H(a,$.tw())){r=A.uK(a) +return r}if(B.a.H(a,"\tat ")){r=A.uJ(a) +return r}if(B.a.H(a,$.tl())||B.a.H(a,$.tj())){r=A.uI(a) +return r}if(B.a.H(a,u.q)){r=A.tQ(a).hs() +return r}if(B.a.H(a,$.to())){r=A.qj(a) +return r}r=A.qk(a) +return r}catch(q){r=A.H(q) +if(r instanceof A.aF){s=r +throw A.b(A.aj(s.a+"\nStack trace:\n"+a,null,null))}else throw q}}, +uM(a){return A.qk(a)}, +qk(a){var s=A.aO(A.uN(a),t.B) +return new A.a0(s)}, +uN(a){var s,r=B.a.eM(a),q=$.pq(),p=t.U,o=new A.aK(A.f(A.bk(r,q,"").split("\n"),t.s),new A.lp(),p) +if(!o.gq(0).k())return A.f([],t.e) +r=A.oz(o,o.gl(0)-1,p.h("d.E")) +r=A.ht(r,A.xf(),A.r(r).h("d.E"),t.B) +s=A.ak(r,A.r(r).h("d.E")) +if(!B.a.ek(o.gD(0),".da"))s.push(A.pJ(o.gD(0))) +return s}, +uK(a){var s=A.b6(A.f(a.split("\n"),t.s),1,null,t.N).hR(0,new A.ln()),r=t.B +r=A.aO(A.ht(s,A.rA(),s.$ti.h("d.E"),r),r) +return new A.a0(r)}, +uJ(a){var s=A.aO(new A.aG(new A.aK(A.f(a.split("\n"),t.s),new A.lm(),t.U),A.rA(),t.M),t.B) +return new A.a0(s)}, +uI(a){var s=A.aO(new A.aG(new A.aK(A.f(B.a.eM(a).split("\n"),t.s),new A.lk(),t.U),A.xd(),t.M),t.B) +return new A.a0(s)}, +uL(a){return A.qj(a)}, +qj(a){var s=a.length===0?A.f([],t.e):new A.aG(new A.aK(A.f(B.a.eM(a).split("\n"),t.s),new A.ll(),t.U),A.xe(),t.M) +s=A.aO(s,t.B) +return new A.a0(s)}, +qi(a,b){var s=A.aO(a,t.B) +return new A.a0(s)}, +a0:function a0(a){this.a=a}, +lo:function lo(a){this.a=a}, +lp:function lp(){}, +ln:function ln(){}, +lm:function lm(){}, +lk:function lk(){}, +ll:function ll(){}, +lr:function lr(){}, +lq:function lq(a){this.a=a}, +bt:function bt(a,b){this.a=a +this.w=b}, +ee:function ee(a){var _=this +_.b=_.a=$ +_.c=null +_.d=!1 +_.$ti=a}, +eY:function eY(a,b,c){this.a=a +this.b=b +this.$ti=c}, +eX:function eX(a,b){this.b=a +this.a=b}, +pM(a,b,c,d){var s,r={} +r.a=a +s=new A.eo(d.h("eo<0>")) +s.hX(b,!0,r,d) +return s}, +eo:function eo(a){var _=this +_.b=_.a=$ +_.c=null +_.d=!1 +_.$ti=a}, +kg:function kg(a,b){this.a=a +this.b=b}, +kf:function kf(a){this.a=a}, +f8:function f8(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.e=_.d=!1 +_.r=_.f=null +_.w=d}, +hO:function hO(a){this.b=this.a=$ +this.$ti=a}, +eM:function eM(){}, +dt:function dt(){}, +iv:function iv(){}, +bg:function bg(a,b){this.a=a +this.b=b}, +aL(a,b,c,d){var s +if(c==null)s=null +else{s=A.ru(new A.mw(c),t.m) +s=s==null?null:A.bi(s)}s=new A.io(a,b,s,!1) +s.e5() +return s}, +ru(a,b){var s=$.m +if(s===B.d)return a +return s.eg(a,b)}, +of:function of(a,b){this.a=a +this.$ti=b}, +f3:function f3(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.$ti=d}, +io:function io(a,b,c,d){var _=this +_.a=0 +_.b=a +_.c=b +_.d=c +_.e=d}, +mw:function mw(a){this.a=a}, +mx:function mx(a){this.a=a}, +pd(a){if(typeof dartPrint=="function"){dartPrint(a) +return}if(typeof console=="object"&&typeof console.log!="undefined"){console.log(a) +return}if(typeof print=="function"){print(a) +return}throw"Unable to print message: "+String(a)}, +hm(a,b,c,d,e,f){var s +if(c==null)return a[b]() +else if(d==null)return a[b](c) +else if(e==null)return a[b](c,d) +else{s=a[b](c,d,e) +return s}}, +p5(){var s,r,q,p,o=null +try{o=A.i_()}catch(s){if(t.g8.b(A.H(s))){r=$.nz +if(r!=null)return r +throw s}else throw s}if(J.am(o,$.r8)){r=$.nz +r.toString +return r}$.r8=o +if($.pk()===$.fJ())r=$.nz=o.hq(".").i(0) +else{q=o.eL() +p=q.length-1 +r=$.nz=p===0?q:B.a.p(q,0,p)}return r}, +rE(a){var s +if(!(a>=65&&a<=90))s=a>=97&&a<=122 +else s=!0 +return s}, +rz(a,b){var s,r,q=null,p=a.length,o=b+2 +if(p0)throw A.b(A.k2("BigInt value exceeds the range of 64 bits")) +return a}, +uA(a){var s,r=a.a,q=a.b,p=r.d,o=p.sqlite3_value_type(q) +A:{s=null +if(1===o){r=A.B(v.G.Number(p.sqlite3_value_int64(q))) +break A}if(2===o){r=p.sqlite3_value_double(q) +break A}if(3===o){o=p.sqlite3_value_bytes(q) +o=A.cd(r.b,p.sqlite3_value_text(q),o) +r=o +break A}if(4===o){o=p.sqlite3_value_bytes(q) +o=A.qu(r.b,p.sqlite3_value_blob(q),o) +r=o +break A}r=s +break A}return r}, +oj(a,b){var s,r +for(s=b,r=0;r<16;++r)s+=A.aR("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012346789".charCodeAt(a.hg(61))) +return s.charCodeAt(0)==0?s:s}, +kK(a){var s=0,r=A.k(t.w),q +var $async$kK=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.V(a.arrayBuffer(),t.u),$async$kK) +case 3:q=c +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$kK,r)}, +qd(a,b,c){var s=v.G.DataView,r=[a] +r.push(b) +r.push(c) +return t.gT.a(A.e3(s,r))}, +ow(a,b,c){var s=v.G.Uint8Array,r=[a] +r.push(b) +r.push(c) +return t.Z.a(A.e3(s,r))}, +tN(a,b){v.G.Atomics.notify(a,b,1/0)}, +pf(){var s=v.G.navigator +if("storage" in s)return s.storage +return null}, +og(a,b,c){var s=a.read(b,c) +return s}, +oh(a,b,c){var s=a.write(b,c) +return s}, +pI(a,b){return A.V(a.removeEntry(b,{recursive:!1}),t.X)}, +xr(){var s=v.G +if(A.kq(s,"DedicatedWorkerGlobalScope"))new A.jN(s,new A.bq(),new A.h6(A.ao(t.N,t.fE),null)).R() +else if(A.kq(s,"SharedWorkerGlobalScope"))new A.kW(s,new A.h6(A.ao(t.N,t.fE),null)).R()}},B={} +var w=[A,J,B] +var $={} +A.on.prototype={} +J.hi.prototype={ +T(a,b){return a===b}, +gA(a){return A.eE(a)}, +i(a){return"Instance of '"+A.hH(a)+"'"}, +gS(a){return A.bR(A.oY(this))}} +J.hk.prototype={ +i(a){return String(a)}, +gA(a){return a?519018:218159}, +gS(a){return A.bR(t.y)}, +$iK:1, +$iI:1} +J.et.prototype={ +T(a,b){return null==b}, +i(a){return"null"}, +gA(a){return 0}, +$iK:1, +$iN:1} +J.eu.prototype={$iz:1} +J.bY.prototype={ +gA(a){return 0}, +i(a){return String(a)}} +J.hG.prototype={} +J.cF.prototype={} +J.bz.prototype={ +i(a){var s=a[$.rR()] +if(s==null)s=a[$.cZ()] +if(s==null)return this.hS(a) +return"JavaScript function for "+J.b1(s)}} +J.aN.prototype={ +gA(a){return 0}, +i(a){return String(a)}} +J.d9.prototype={ +gA(a){return 0}, +i(a){return String(a)}} +J.u.prototype={ +bx(a,b){return new A.ai(a,A.O(a).h("@<1>").G(b).h("ai<1,2>"))}, +v(a,b){a.$flags&1&&A.y(a,29) +a.push(b)}, +dd(a,b){var s +a.$flags&1&&A.y(a,"removeAt",1) +s=a.length +if(b>=s)throw A.b(A.kJ(b,null)) +return a.splice(b,1)[0]}, +d2(a,b,c){var s +a.$flags&1&&A.y(a,"insert",2) +s=a.length +if(b>s)throw A.b(A.kJ(b,null)) +a.splice(b,0,c)}, +ev(a,b,c){var s,r +a.$flags&1&&A.y(a,"insertAll",2) +A.qa(b,0,a.length,"index") +if(!t.Q.b(c))c=J.j2(c) +s=J.aC(c) +a.length=a.length+s +r=b+s +this.N(a,r,a.length,a,b) +this.ac(a,b,r,c)}, +hm(a){a.$flags&1&&A.y(a,"removeLast",1) +if(a.length===0)throw A.b(A.iX(a,-1)) +return a.pop()}, +F(a,b){var s +a.$flags&1&&A.y(a,"remove",1) +for(s=0;s").G(c).h("D<1,2>"))}, +av(a,b){var s,r=A.b5(a.length,"",!1,t.N) +for(s=0;ss)throw A.b(A.W(b,0,s,"start",null)) +if(cs)throw A.b(A.W(c,b,s,"end",null)) +if(b===c)return A.f([],A.O(a)) +return A.f(a.slice(b,c),A.O(a))}, +ct(a,b,c){A.bd(b,c,a.length) +return A.b6(a,b,c,A.O(a).c)}, +gE(a){if(a.length>0)return a[0] +throw A.b(A.aw())}, +gD(a){var s=a.length +if(s>0)return a[s-1] +throw A.b(A.aw())}, +N(a,b,c,d,e){var s,r,q,p,o +a.$flags&2&&A.y(a,5) +A.bd(b,c,a.length) +s=c-b +if(s===0)return +A.ab(e,"skipCount") +if(t.j.b(d)){r=d +q=e}else{r=J.e7(d,e).aC(0,!1) +q=0}p=J.a3(r) +if(q+s>p.gl(r))throw A.b(A.pP()) +if(q=0;--o)a[b+o]=p.j(r,q+o) +else for(o=0;o0){a[0]=q +a[1]=r}return}p=0 +if(A.O(a).c.b(null))for(o=0;o0)this.jd(a,p)}, +hM(a){return this.hN(a,null)}, +jd(a,b){var s,r=a.length +for(;s=r-1,r>0;r=s)if(a[s]===null){a[s]=void 0;--b +if(b===0)break}}, +d5(a,b){var s,r=a.length,q=r-1 +if(q<0)return-1 +q=0;--s)if(J.am(a[s],b))return s +return-1}, +gB(a){return a.length===0}, +i(a){return A.ol(a,"[","]")}, +aC(a,b){var s=A.f(a.slice(0),A.O(a)) +return s}, +cn(a){return this.aC(a,!0)}, +gq(a){return new J.fL(a,a.length,A.O(a).h("fL<1>"))}, +gA(a){return A.eE(a)}, +gl(a){return a.length}, +j(a,b){if(!(b>=0&&b=0&&b=p){r.d=null +return!1}r.d=q[s] +r.c=s+1 +return!0}} +J.d8.prototype={ +af(a,b){var s +if(ab)return 1 +else if(a===b){if(a===0){s=this.gey(b) +if(this.gey(a)===s)return 0 +if(this.gey(a))return-1 +return 1}return 0}else if(isNaN(a)){if(isNaN(b))return 0 +return 1}else return-1}, +gey(a){return a===0?1/a<0:a<0}, +lj(a){var s +if(a>=-2147483648&&a<=2147483647)return a|0 +if(isFinite(a)){s=a<0?Math.ceil(a):Math.floor(a) +return s+0}throw A.b(A.a4(""+a+".toInt()"))}, +k_(a){var s,r +if(a>=0){if(a<=2147483647){s=a|0 +return a===s?s:s+1}}else if(a>=-2147483648)return a|0 +r=Math.ceil(a) +if(isFinite(r))return r +throw A.b(A.a4(""+a+".ceil()"))}, +i(a){if(a===0&&1/a<0)return"-0.0" +else return""+a}, +gA(a){var s,r,q,p,o=a|0 +if(a===o)return o&536870911 +s=Math.abs(a) +r=Math.log(s)/0.6931471805599453|0 +q=Math.pow(2,r) +p=s<1?s/q:q/s +return((p*9007199254740992|0)+(p*3542243181176521|0))*599197+r*1259&536870911}, +ab(a,b){var s=a%b +if(s===0)return 0 +if(s>0)return s +return s+b}, +eY(a,b){if((a|0)===a)if(b>=1||b<-1)return a/b|0 +return this.fL(a,b)}, +I(a,b){return(a|0)===a?a/b|0:this.fL(a,b)}, +fL(a,b){var s=a/b +if(s>=-2147483648&&s<=2147483647)return s|0 +if(s>0){if(s!==1/0)return Math.floor(s)}else if(s>-1/0)return Math.ceil(s) +throw A.b(A.a4("Result of truncating division is "+A.t(s)+": "+A.t(a)+" ~/ "+b))}, +aE(a,b){if(b<0)throw A.b(A.e2(b)) +return b>31?0:a<>>0}, +bl(a,b){var s +if(b<0)throw A.b(A.e2(b)) +if(a>0)s=this.e4(a,b) +else{s=b>31?31:b +s=a>>s>>>0}return s}, +M(a,b){var s +if(a>0)s=this.e4(a,b) +else{s=b>31?31:b +s=a>>s>>>0}return s}, +jt(a,b){if(0>b)throw A.b(A.e2(b)) +return this.e4(a,b)}, +e4(a,b){return b>31?0:a>>>b}, +gS(a){return A.bR(t.o)}, +$iF:1, +$ib0:1} +J.es.prototype={ +gfY(a){var s,r=a<0?-a-1:a,q=r +for(s=32;q>=4294967296;){q=this.I(q,4294967296) +s+=32}return s-Math.clz32(q)}, +gS(a){return A.bR(t.S)}, +$iK:1, +$ia:1} +J.hl.prototype={ +gS(a){return A.bR(t.i)}, +$iK:1} +J.bX.prototype={ +cS(a,b,c){var s=b.length +if(c>s)throw A.b(A.W(c,0,s,null,null)) +return new A.iM(b,a,c)}, +ed(a,b){return this.cS(a,b,0)}, +he(a,b,c){var s,r,q=null +if(c<0||c>b.length)throw A.b(A.W(c,0,b.length,q,q)) +s=a.length +if(c+s>b.length)return q +for(r=0;rr)return!1 +return b===this.L(a,r-s)}, +hp(a,b,c){A.qa(0,0,a.length,"startIndex") +return A.xL(a,b,c,0)}, +bm(a,b){var s +if(typeof b=="string")return A.f(a.split(b),t.s) +else{if(b instanceof A.cx){s=b.e +s=!(s==null?b.e=b.ij():s)}else s=!1 +if(s)return A.f(a.split(b.b),t.s) +else return this.ir(a,b)}}, +aO(a,b,c,d){var s=A.bd(b,c,a.length) +return A.ph(a,b,s,d)}, +ir(a,b){var s,r,q,p,o,n,m=A.f([],t.s) +for(s=J.o9(b,a),s=s.gq(s),r=0,q=1;s.k();){p=s.gm() +o=p.gcv() +n=p.gbz() +q=n-o +if(q===0&&r===o)continue +m.push(this.p(a,r,o)) +r=n}if(r0)m.push(this.L(a,r)) +return m}, +C(a,b,c){var s +if(c<0||c>a.length)throw A.b(A.W(c,0,a.length,null,null)) +if(typeof b=="string"){s=c+b.length +if(s>a.length)return!1 +return b===a.substring(c,s)}return J.tH(b,a,c)!=null}, +u(a,b){return this.C(a,b,0)}, +p(a,b,c){return a.substring(b,A.bd(b,c,a.length))}, +L(a,b){return this.p(a,b,null)}, +eM(a){var s,r,q,p=a.trim(),o=p.length +if(o===0)return p +if(p.charCodeAt(0)===133){s=J.ug(p,1) +if(s===o)return""}else s=0 +r=o-1 +q=p.charCodeAt(r)===133?J.uh(p,r):o +if(s===0&&q===o)return p +return p.substring(s,q)}, +bI(a,b){var s,r +if(0>=b)return"" +if(b===1||a.length===0)return a +if(b!==b>>>0)throw A.b(B.ap) +for(s=a,r="";;){if((b&1)===1)r=s+r +b=b>>>1 +if(b===0)break +s+=s}return r}, +l0(a,b,c){var s=b-a.length +if(s<=0)return a +return this.bI(c,s)+a}, +hh(a,b){var s=b-a.length +if(s<=0)return a +return a+this.bI(" ",s)}, +aY(a,b,c){var s +if(c<0||c>a.length)throw A.b(A.W(c,0,a.length,null,null)) +s=a.indexOf(b,c) +return s}, +kG(a,b){return this.aY(a,b,0)}, +hd(a,b,c){var s,r +if(c==null)c=a.length +else if(c<0||c>a.length)throw A.b(A.W(c,0,a.length,null,null)) +s=b.length +r=a.length +if(c+s>r)c=r-s +return a.lastIndexOf(b,c)}, +d5(a,b){return this.hd(a,b,null)}, +H(a,b){return A.xH(a,b,0)}, +af(a,b){var s +if(a===b)s=0 +else s=a>6}r=r+((r&67108863)<<3)&536870911 +r^=r>>11 +return r+((r&16383)<<15)&536870911}, +gS(a){return A.bR(t.N)}, +gl(a){return a.length}, +j(a,b){if(!(b>=0&&b"))}, +gl(a){return J.aC(this.gao())}, +gB(a){return J.oa(this.gao())}, +U(a,b){var s=A.r(this) +return A.ed(J.e7(this.gao(),b),s.c,s.y[1])}, +ah(a,b){var s=A.r(this) +return A.ed(J.j1(this.gao(),b),s.c,s.y[1])}, +K(a,b){return A.r(this).y[1].a(J.j_(this.gao(),b))}, +gE(a){return A.r(this).y[1].a(J.j0(this.gao()))}, +gD(a){return A.r(this).y[1].a(J.ob(this.gao()))}, +i(a){return J.b1(this.gao())}} +A.fU.prototype={ +k(){return this.a.k()}, +gm(){return this.$ti.y[1].a(this.a.gm())}} +A.cp.prototype={ +gao(){return this.a}} +A.f1.prototype={$iq:1} +A.eW.prototype={ +j(a,b){return this.$ti.y[1].a(J.aM(this.a,b))}, +t(a,b,c){J.pr(this.a,b,this.$ti.c.a(c))}, +ct(a,b,c){var s=this.$ti +return A.ed(J.tG(this.a,b,c),s.c,s.y[1])}, +N(a,b,c,d,e){var s=this.$ti +J.tI(this.a,b,c,A.ed(d,s.y[1],s.c),e)}, +ac(a,b,c,d){return this.N(0,b,c,d,0)}, +$iq:1, +$io:1} +A.ai.prototype={ +bx(a,b){return new A.ai(this.a,this.$ti.h("@<1>").G(b).h("ai<1,2>"))}, +gao(){return this.a}} +A.da.prototype={ +i(a){return"LateInitializationError: "+this.a}} +A.fV.prototype={ +gl(a){return this.a.length}, +j(a,b){return this.a.charCodeAt(b)}} +A.nY.prototype={ +$0(){return A.b3(null,t.H)}, +$S:10} +A.kN.prototype={} +A.q.prototype={} +A.Q.prototype={ +gq(a){var s=this +return new A.b4(s,s.gl(s),A.r(s).h("b4"))}, +gB(a){return this.gl(this)===0}, +gE(a){if(this.gl(this)===0)throw A.b(A.aw()) +return this.K(0,0)}, +gD(a){var s=this +if(s.gl(s)===0)throw A.b(A.aw()) +return s.K(0,s.gl(s)-1)}, +av(a,b){var s,r,q,p=this,o=p.gl(p) +if(b.length!==0){if(o===0)return"" +s=A.t(p.K(0,0)) +if(o!==p.gl(p))throw A.b(A.an(p)) +for(r=s,q=1;q").G(c).h("D<1,2>"))}, +kE(a,b,c){var s,r,q=this,p=q.gl(q) +for(s=b,r=0;rs)throw A.b(A.W(r,0,s,"start",null))}}, +giy(){var s=J.aC(this.a),r=this.c +if(r==null||r>s)return s +return r}, +gjy(){var s=J.aC(this.a),r=this.b +if(r>s)return s +return r}, +gl(a){var s,r=J.aC(this.a),q=this.b +if(q>=r)return 0 +s=this.c +if(s==null||s>=r)return r-q +return s-q}, +K(a,b){var s=this,r=s.gjy()+b +if(b<0||r>=s.giy())throw A.b(A.hf(b,s.gl(0),s,null,"index")) +return J.j_(s.a,r)}, +U(a,b){var s,r,q=this +A.ab(b,"count") +s=q.b+b +r=q.c +if(r!=null&&s>=r)return new A.cv(q.$ti.h("cv<1>")) +return A.b6(q.a,s,r,q.$ti.c)}, +ah(a,b){var s,r,q,p=this +A.ab(b,"count") +s=p.c +r=p.b +q=r+b +if(s==null)return A.b6(p.a,r,q,p.$ti.c) +else{if(s=o){r.d=null +return!1}r.d=p.K(q,s);++r.c +return!0}} +A.aG.prototype={ +gq(a){var s=this.a +return new A.dc(s.gq(s),this.b,A.r(this).h("dc<1,2>"))}, +gl(a){var s=this.a +return s.gl(s)}, +gB(a){var s=this.a +return s.gB(s)}, +gE(a){var s=this.a +return this.b.$1(s.gE(s))}, +gD(a){var s=this.a +return this.b.$1(s.gD(s))}, +K(a,b){var s=this.a +return this.b.$1(s.K(s,b))}} +A.cu.prototype={$iq:1} +A.dc.prototype={ +k(){var s=this,r=s.b +if(r.k()){s.a=s.c.$1(r.gm()) +return!0}s.a=null +return!1}, +gm(){var s=this.a +return s==null?this.$ti.y[1].a(s):s}} +A.D.prototype={ +gl(a){return J.aC(this.a)}, +K(a,b){return this.b.$1(J.j_(this.a,b))}} +A.aK.prototype={ +gq(a){return new A.cG(J.Z(this.a),this.b)}, +ba(a,b,c){return new A.aG(this,b,this.$ti.h("@<1>").G(c).h("aG<1,2>"))}} +A.cG.prototype={ +k(){var s,r +for(s=this.a,r=this.b;s.k();)if(r.$1(s.gm()))return!0 +return!1}, +gm(){return this.a.gm()}} +A.em.prototype={ +gq(a){return new A.ha(J.Z(this.a),this.b,B.G,this.$ti.h("ha<1,2>"))}} +A.ha.prototype={ +gm(){var s=this.d +return s==null?this.$ti.y[1].a(s):s}, +k(){var s,r,q=this,p=q.c +if(p==null)return!1 +for(s=q.a,r=q.b;!p.k();){q.d=null +if(s.k()){q.c=null +p=J.Z(r.$1(s.gm())) +q.c=p}else return!1}q.d=q.c.gm() +return!0}} +A.cE.prototype={ +gq(a){var s=this.a +return new A.hR(s.gq(s),this.b,A.r(this).h("hR<1>"))}} +A.ek.prototype={ +gl(a){var s=this.a,r=s.gl(s) +s=this.b +if(r>s)return s +return r}, +$iq:1} +A.hR.prototype={ +k(){if(--this.b>=0)return this.a.k() +this.b=-1 +return!1}, +gm(){if(this.b<0){this.$ti.c.a(null) +return null}return this.a.gm()}} +A.bJ.prototype={ +U(a,b){A.bT(b,"count") +A.ab(b,"count") +return new A.bJ(this.a,this.b+b,A.r(this).h("bJ<1>"))}, +gq(a){var s=this.a +return new A.hM(s.gq(s),this.b)}} +A.d5.prototype={ +gl(a){var s=this.a,r=s.gl(s)-this.b +if(r>=0)return r +return 0}, +U(a,b){A.bT(b,"count") +A.ab(b,"count") +return new A.d5(this.a,this.b+b,this.$ti)}, +$iq:1} +A.hM.prototype={ +k(){var s,r +for(s=this.a,r=0;r"))}, +U(a,b){A.ab(b,"count") +return this}, +ah(a,b){A.ab(b,"count") +return this}} +A.h7.prototype={ +k(){return!1}, +gm(){throw A.b(A.aw())}} +A.eR.prototype={ +gq(a){return new A.i8(J.Z(this.a),this.$ti.h("i8<1>"))}} +A.i8.prototype={ +k(){var s,r +for(s=this.a,r=this.$ti.c;s.k();)if(r.b(s.gm()))return!0 +return!1}, +gm(){return this.$ti.c.a(this.a.gm())}} +A.by.prototype={ +gl(a){return J.aC(this.a)}, +gB(a){return J.oa(this.a)}, +gE(a){return new A.ag(this.b,J.j0(this.a))}, +K(a,b){return new A.ag(b+this.b,J.j_(this.a,b))}, +ah(a,b){A.bT(b,"count") +A.ab(b,"count") +return new A.by(J.j1(this.a,b),this.b,A.r(this).h("by<1>"))}, +U(a,b){A.bT(b,"count") +A.ab(b,"count") +return new A.by(J.e7(this.a,b),b+this.b,A.r(this).h("by<1>"))}, +gq(a){return new A.eq(J.Z(this.a),this.b)}} +A.ct.prototype={ +gD(a){var s,r=this.a,q=J.a3(r),p=q.gl(r) +if(p<=0)throw A.b(A.aw()) +s=q.gD(r) +if(p!==q.gl(r))throw A.b(A.an(this)) +return new A.ag(p-1+this.b,s)}, +ah(a,b){A.bT(b,"count") +A.ab(b,"count") +return new A.ct(J.j1(this.a,b),this.b,this.$ti)}, +U(a,b){A.bT(b,"count") +A.ab(b,"count") +return new A.ct(J.e7(this.a,b),this.b+b,this.$ti)}, +$iq:1} +A.eq.prototype={ +k(){if(++this.c>=0&&this.a.k())return!0 +this.c=-2 +return!1}, +gm(){var s=this.c +return s>=0?new A.ag(this.b+s,this.a.gm()):A.E(A.aw())}} +A.en.prototype={} +A.hV.prototype={ +t(a,b,c){throw A.b(A.a4("Cannot modify an unmodifiable list"))}, +N(a,b,c,d,e){throw A.b(A.a4("Cannot modify an unmodifiable list"))}, +ac(a,b,c,d){return this.N(0,b,c,d,0)}} +A.du.prototype={} +A.eG.prototype={ +gl(a){return J.aC(this.a)}, +K(a,b){var s=this.a,r=J.a3(s) +return r.K(s,r.gl(s)-1-b)}} +A.hQ.prototype={ +gA(a){var s=this._hashCode +if(s!=null)return s +s=664597*B.a.gA(this.a)&536870911 +this._hashCode=s +return s}, +i(a){return'Symbol("'+this.a+'")'}, +T(a,b){if(b==null)return!1 +return b instanceof A.hQ&&this.a===b.a}} +A.fA.prototype={} +A.ag.prototype={$r:"+(1,2)",$s:1} +A.cR.prototype={$r:"+file,outFlags(1,2)",$s:2} +A.iE.prototype={$r:"+result,resultCode(1,2)",$s:3} +A.ef.prototype={ +i(a){return A.oq(this)}, +gcZ(){return new A.dR(this.kC(),A.r(this).h("dR>"))}, +kC(){var s=this +return function(){var r=0,q=1,p=[],o,n,m +return function $async$gcZ(a,b,c){if(b===1){p.push(c) +r=q}for(;;)switch(r){case 0:o=s.gX(),o=o.gq(o),n=A.r(s).h("aP<1,2>") +case 2:if(!o.k()){r=3 +break}m=o.gm() +r=4 +return a.b=new A.aP(m,s.j(0,m),n),1 +case 4:r=2 +break +case 3:return 0 +case 1:return a.c=p.at(-1),3}}}}, +$iap:1} +A.eg.prototype={ +gl(a){return this.b.length}, +gfm(){var s=this.$keys +if(s==null){s=Object.keys(this.a) +this.$keys=s}return s}, +a3(a){if(typeof a!="string")return!1 +if("__proto__"===a)return!1 +return this.a.hasOwnProperty(a)}, +j(a,b){if(!this.a3(b))return null +return this.b[this.a[b]]}, +ar(a,b){var s,r,q=this.gfm(),p=this.b +for(s=q.length,r=0;r"))}, +gbH(){return new A.cP(this.b,this.$ti.h("cP<2>"))}} +A.cP.prototype={ +gl(a){return this.a.length}, +gB(a){return 0===this.a.length}, +gq(a){var s=this.a +return new A.ix(s,s.length,this.$ti.h("ix<1>"))}} +A.ix.prototype={ +gm(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +k(){var s=this,r=s.c +if(r>=s.b){s.d=null +return!1}s.d=s.a[r] +s.c=r+1 +return!0}} +A.kl.prototype={ +T(a,b){if(b==null)return!1 +return b instanceof A.er&&this.a.T(0,b.a)&&A.p7(this)===A.p7(b)}, +gA(a){return A.eB(this.a,A.p7(this),B.f,B.f)}, +i(a){var s=B.c.av([A.bR(this.$ti.c)],", ") +return this.a.i(0)+" with "+("<"+s+">")}} +A.er.prototype={ +$2(a,b){return this.a.$1$2(a,b,this.$ti.y[0])}, +$4(a,b,c,d){return this.a.$1$4(a,b,c,d,this.$ti.y[0])}, +$S(){return A.xn(A.nL(this.a),this.$ti)}} +A.eH.prototype={} +A.lt.prototype={ +aw(a){var s,r,q=this,p=new RegExp(q.a).exec(a) +if(p==null)return null +s=Object.create(null) +r=q.b +if(r!==-1)s.arguments=p[r+1] +r=q.c +if(r!==-1)s.argumentsExpr=p[r+1] +r=q.d +if(r!==-1)s.expr=p[r+1] +r=q.e +if(r!==-1)s.method=p[r+1] +r=q.f +if(r!==-1)s.receiver=p[r+1] +return s}} +A.eA.prototype={ +i(a){return"Null check operator used on a null value"}} +A.hn.prototype={ +i(a){var s,r=this,q="NoSuchMethodError: method not found: '",p=r.b +if(p==null)return"NoSuchMethodError: "+r.a +s=r.c +if(s==null)return q+p+"' ("+r.a+")" +return q+p+"' on '"+s+"' ("+r.a+")"}} +A.hU.prototype={ +i(a){var s=this.a +return s.length===0?"Error":"Error: "+s}} +A.hD.prototype={ +i(a){return"Throw of null ('"+(this.a===null?"null":"undefined")+"' from JavaScript)"}, +$ia6:1} +A.el.prototype={} +A.fm.prototype={ +i(a){var s,r=this.b +if(r!=null)return r +r=this.a +s=r!==null&&typeof r==="object"?r.stack:null +return this.b=s==null?"":s}, +$ia_:1} +A.cq.prototype={ +i(a){var s=this.constructor,r=s==null?null:s.name +return"Closure '"+A.rO(r==null?"unknown":r)+"'"}, +glY(){return this}, +$C:"$1", +$R:1, +$D:null} +A.jg.prototype={$C:"$0",$R:0} +A.jh.prototype={$C:"$2",$R:2} +A.lj.prototype={} +A.l9.prototype={ +i(a){var s=this.$static_name +if(s==null)return"Closure of unknown static method" +return"Closure '"+A.rO(s)+"'"}} +A.eb.prototype={ +T(a,b){if(b==null)return!1 +if(this===b)return!0 +if(!(b instanceof A.eb))return!1 +return this.$_target===b.$_target&&this.a===b.a}, +gA(a){return(A.pb(this.a)^A.eE(this.$_target))>>>0}, +i(a){return"Closure '"+this.$_name+"' of "+("Instance of '"+A.hH(this.a)+"'")}} +A.hK.prototype={ +i(a){return"RuntimeError: "+this.a}} +A.bA.prototype={ +gl(a){return this.a}, +gB(a){return this.a===0}, +gX(){return new A.bB(this,A.r(this).h("bB<1>"))}, +gbH(){return new A.ew(this,A.r(this).h("ew<2>"))}, +gcZ(){return new A.ev(this,A.r(this).h("ev<1,2>"))}, +a3(a){var s,r +if(typeof a=="string"){s=this.b +if(s==null)return!1 +return s[a]!=null}else if(typeof a=="number"&&(a&0x3fffffff)===a){r=this.c +if(r==null)return!1 +return r[a]!=null}else return this.kH(a)}, +kH(a){var s=this.d +if(s==null)return!1 +return this.d4(s[this.d3(a)],a)>=0}, +aI(a,b){b.ar(0,new A.ks(this))}, +j(a,b){var s,r,q,p,o=null +if(typeof b=="string"){s=this.b +if(s==null)return o +r=s[b] +q=r==null?o:r.b +return q}else if(typeof b=="number"&&(b&0x3fffffff)===b){p=this.c +if(p==null)return o +r=p[b] +q=r==null?o:r.b +return q}else return this.kI(b)}, +kI(a){var s,r,q=this.d +if(q==null)return null +s=q[this.d3(a)] +r=this.d4(s,a) +if(r<0)return null +return s[r].b}, +t(a,b,c){var s,r,q=this +if(typeof b=="string"){s=q.b +q.eZ(s==null?q.b=q.dY():s,b,c)}else if(typeof b=="number"&&(b&0x3fffffff)===b){r=q.c +q.eZ(r==null?q.c=q.dY():r,b,c)}else q.kK(b,c)}, +kK(a,b){var s,r,q,p=this,o=p.d +if(o==null)o=p.d=p.dY() +s=p.d3(a) +r=o[s] +if(r==null)o[s]=[p.du(a,b)] +else{q=p.d4(r,a) +if(q>=0)r[q].b=b +else r.push(p.du(a,b))}}, +hk(a,b){var s,r,q=this +if(q.a3(a)){s=q.j(0,a) +return s==null?A.r(q).y[1].a(s):s}r=b.$0() +q.t(0,a,r) +return r}, +F(a,b){var s=this +if(typeof b=="string")return s.f_(s.b,b) +else if(typeof b=="number"&&(b&0x3fffffff)===b)return s.f_(s.c,b) +else return s.kJ(b)}, +kJ(a){var s,r,q,p,o=this,n=o.d +if(n==null)return null +s=o.d3(a) +r=n[s] +q=o.d4(r,a) +if(q<0)return null +p=r.splice(q,1)[0] +o.f0(p) +if(r.length===0)delete n[s] +return p.b}, +c3(a){var s=this +if(s.a>0){s.b=s.c=s.d=s.e=s.f=null +s.a=0 +s.dt()}}, +ar(a,b){var s=this,r=s.e,q=s.r +while(r!=null){b.$2(r.a,r.b) +if(q!==s.r)throw A.b(A.an(s)) +r=r.c}}, +eZ(a,b,c){var s=a[b] +if(s==null)a[b]=this.du(b,c) +else s.b=c}, +f_(a,b){var s +if(a==null)return null +s=a[b] +if(s==null)return null +this.f0(s) +delete a[b] +return s.b}, +dt(){this.r=this.r+1&1073741823}, +du(a,b){var s,r=this,q=new A.kv(a,b) +if(r.e==null)r.e=r.f=q +else{s=r.f +s.toString +q.d=s +r.f=s.c=q}++r.a +r.dt() +return q}, +f0(a){var s=this,r=a.d,q=a.c +if(r==null)s.e=q +else r.c=q +if(q==null)s.f=r +else q.d=r;--s.a +s.dt()}, +d3(a){return J.aE(a)&1073741823}, +d4(a,b){var s,r +if(a==null)return-1 +s=a.length +for(r=0;r"]=s +delete s[""] +return s}} +A.ks.prototype={ +$2(a,b){this.a.t(0,a,b)}, +$S(){return A.r(this.a).h("~(1,2)")}} +A.kv.prototype={} +A.bB.prototype={ +gl(a){return this.a.a}, +gB(a){return this.a.a===0}, +gq(a){var s=this.a +return new A.hr(s,s.r,s.e)}} +A.hr.prototype={ +gm(){return this.d}, +k(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.b(A.an(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=s.a +r.c=s.c +return!0}}} +A.ew.prototype={ +gl(a){return this.a.a}, +gB(a){return this.a.a===0}, +gq(a){var s=this.a +return new A.db(s,s.r,s.e)}} +A.db.prototype={ +gm(){return this.d}, +k(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.b(A.an(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=s.b +r.c=s.c +return!0}}} +A.ev.prototype={ +gl(a){return this.a.a}, +gB(a){return this.a.a===0}, +gq(a){var s=this.a +return new A.hq(s,s.r,s.e,this.$ti.h("hq<1,2>"))}} +A.hq.prototype={ +gm(){var s=this.d +s.toString +return s}, +k(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.b(A.an(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=new A.aP(s.a,s.b,r.$ti.h("aP<1,2>")) +r.c=s.c +return!0}}} +A.nS.prototype={ +$1(a){return this.a(a)}, +$S:74} +A.nT.prototype={ +$2(a,b){return this.a(a,b)}, +$S:118} +A.nU.prototype={ +$1(a){return this.a(a)}, +$S:59} +A.fi.prototype={ +i(a){return this.fP(!1)}, +fP(a){var s,r,q,p,o,n=this.iA(),m=this.fj(),l=(a?"Record ":"")+"(" +for(s=n.length,r="",q=0;q0;){--q;--s +k[q]=r[s]}}return A.aO(k,t.K)}} +A.iD.prototype={ +fj(){return[this.a,this.b]}, +T(a,b){if(b==null)return!1 +return b instanceof A.iD&&this.$s===b.$s&&J.am(this.a,b.a)&&J.am(this.b,b.b)}, +gA(a){return A.eB(this.$s,this.a,this.b,B.f)}} +A.cx.prototype={ +i(a){return"RegExp/"+this.a+"/"+this.b.flags}, +gfq(){var s=this,r=s.c +if(r!=null)return r +r=s.b +return s.c=A.om(s.a,r.multiline,!r.ignoreCase,r.unicode,r.dotAll,"g")}, +giQ(){var s=this,r=s.d +if(r!=null)return r +r=s.b +return s.d=A.om(s.a,r.multiline,!r.ignoreCase,r.unicode,r.dotAll,"y")}, +ij(){var s,r=this.a +if(!B.a.H(r,"("))return!1 +s=this.b.unicode?"u":"" +return new RegExp("(?:)|"+r,s).exec("").length>1}, +a8(a){var s=this.b.exec(a) +if(s==null)return null +return new A.dJ(s)}, +cS(a,b,c){var s=b.length +if(c>s)throw A.b(A.W(c,0,s,null,null)) +return new A.i9(this,b,c)}, +ed(a,b){return this.cS(0,b,0)}, +ff(a,b){var s,r=this.gfq() +r.lastIndex=b +s=r.exec(a) +if(s==null)return null +return new A.dJ(s)}, +iz(a,b){var s,r=this.giQ() +r.lastIndex=b +s=r.exec(a) +if(s==null)return null +return new A.dJ(s)}, +he(a,b,c){if(c<0||c>b.length)throw A.b(A.W(c,0,b.length,null,null)) +return this.iz(b,c)}} +A.dJ.prototype={ +gcv(){return this.b.index}, +gbz(){var s=this.b +return s.index+s[0].length}, +j(a,b){return this.b[b]}, +aM(a){var s,r=this.b.groups +if(r!=null){s=r[a] +if(s!=null||a in r)return s}throw A.b(A.ad(a,"name","Not a capture group name"))}, +$iex:1, +$ihI:1} +A.i9.prototype={ +gq(a){return new A.m5(this.a,this.b,this.c)}} +A.m5.prototype={ +gm(){var s=this.d +return s==null?t.cz.a(s):s}, +k(){var s,r,q,p,o,n,m=this,l=m.b +if(l==null)return!1 +s=m.c +r=l.length +if(s<=r){q=m.a +p=q.ff(l,s) +if(p!=null){m.d=p +o=p.gbz() +if(p.b.index===o){s=!1 +if(q.b.unicode){q=m.c +n=q+1 +if(n=55296&&r<=56319){s=l.charCodeAt(n) +s=s>=56320&&s<=57343}}}o=(s?o+1:o)+1}m.c=o +return!0}}m.b=m.d=null +return!1}} +A.ds.prototype={ +gbz(){return this.a+this.c.length}, +j(a,b){if(b!==0)throw A.b(A.kJ(b,null)) +return this.c}, +$iex:1, +gcv(){return this.a}} +A.iM.prototype={ +gq(a){return new A.ne(this.a,this.b,this.c)}, +gE(a){var s=this.b,r=this.a.indexOf(s,this.c) +if(r>=0)return new A.ds(r,s) +throw A.b(A.aw())}} +A.ne.prototype={ +k(){var s,r,q=this,p=q.c,o=q.b,n=o.length,m=q.a,l=m.length +if(p+n>l){q.d=null +return!1}s=m.indexOf(o,p) +if(s<0){q.c=l+1 +q.d=null +return!1}r=s+n +q.d=new A.ds(s,o) +q.c=r===q.c?r+1:r +return!0}, +gm(){var s=this.d +s.toString +return s}} +A.ml.prototype={ +ae(){var s=this.b +if(s===this)throw A.b(A.pU(this.a)) +return s}} +A.de.prototype={ +gS(a){return B.aV}, +fV(a,b,c){A.fB(a,b,c) +return c==null?new Uint8Array(a,b):new Uint8Array(a,b,c)}, +jW(a,b,c){var s +A.fB(a,b,c) +s=new DataView(a,b) +return s}, +fU(a){return this.jW(a,0,null)}, +$iK:1, +$ico:1} +A.dd.prototype={$idd:1} +A.ey.prototype={ +gaX(a){if(((a.$flags|0)&2)!==0)return new A.iS(a.buffer) +else return a.buffer}, +iM(a,b,c,d){var s=A.W(b,0,c,d,null) +throw A.b(s)}, +f6(a,b,c,d){if(b>>>0!==b||b>c)this.iM(a,b,c,d)}} +A.iS.prototype={ +fV(a,b,c){var s=A.bE(this.a,b,c) +s.$flags=3 +return s}, +fU(a){var s=A.pV(this.a,0,null) +s.$flags=3 +return s}, +$ico:1} +A.cz.prototype={ +gS(a){return B.aW}, +$iK:1, +$icz:1, +$ioc:1} +A.dg.prototype={ +gl(a){return a.length}, +fI(a,b,c,d,e){var s,r,q=a.length +this.f6(a,b,q,"start") +this.f6(a,c,q,"end") +if(b>c)throw A.b(A.W(b,0,c,null,null)) +s=c-b +if(e<0)throw A.b(A.J(e,null)) +r=d.length +if(r-e0){s=Date.now()-r.c +if(s>(p+1)*o)p=B.b.eY(s,o)}q.c=p +r.d.$1(q)}, +$S:3} +A.ia.prototype={ +O(a){var s,r=this +if(a==null)a=r.$ti.c.a(a) +if(!r.b)r.a.b3(a) +else{s=r.a +if(r.$ti.h("C<1>").b(a))s.f5(a) +else s.bL(a)}}, +by(a,b){var s=this.a +if(this.b)s.V(new A.U(a,b)) +else s.aR(new A.U(a,b))}} +A.nu.prototype={ +$1(a){return this.a.$2(0,a)}, +$S:15} +A.nv.prototype={ +$2(a,b){this.a.$2(1,new A.el(a,b))}, +$S:40} +A.nJ.prototype={ +$2(a,b){this.a(a,b)}, +$S:41} +A.iN.prototype={ +gm(){return this.b}, +jf(a,b){var s,r,q +a=a +b=b +s=this.a +for(;;)try{r=s(this,a,b) +return r}catch(q){b=q +a=1}}, +k(){var s,r,q,p,o=this,n=null,m=0 +for(;;){s=o.d +if(s!=null)try{if(s.k()){o.b=s.gm() +return!0}else o.d=null}catch(r){n=r +m=1 +o.d=null}q=o.jf(m,n) +if(1===q)return!0 +if(0===q){o.b=null +p=o.e +if(p==null||p.length===0){o.a=A.qM +return!1}o.a=p.pop() +m=0 +n=null +continue}if(2===q){m=0 +n=null +continue}if(3===q){n=o.c +o.c=null +p=o.e +if(p==null||p.length===0){o.b=null +o.a=A.qM +throw n +return!1}o.a=p.pop() +m=1 +continue}throw A.b(A.A("sync*"))}return!1}, +lZ(a){var s,r,q=this +if(a instanceof A.dR){s=a.a() +r=q.e +if(r==null)r=q.e=[] +r.push(q.a) +q.a=s +return 2}else{q.d=J.Z(a) +return 2}}} +A.dR.prototype={ +gq(a){return new A.iN(this.a())}} +A.U.prototype={ +i(a){return A.t(this.a)}, +$iL:1, +gaP(){return this.b}} +A.eV.prototype={} +A.cJ.prototype={ +al(){}, +am(){}} +A.cI.prototype={ +gbN(){return this.c<4}, +fD(a){var s=a.CW,r=a.ch +if(s==null)this.d=r +else s.ch=r +if(r==null)this.e=s +else r.CW=s +a.CW=a +a.ch=a}, +fJ(a,b,c,d){var s,r,q,p,o,n,m,l,k,j=this +if((j.c&4)!==0){s=$.m +r=new A.f0(s) +A.pe(r.gfs()) +if(c!=null)r.c=s.az(c,t.H) +return r}s=A.r(j) +r=$.m +q=d?1:0 +p=b!=null?32:0 +o=A.ih(r,a,s.c) +n=A.ii(r,b) +m=c==null?A.rw():c +l=new A.cJ(j,o,n,r.az(m,t.H),r,q|p,s.h("cJ<1>")) +l.CW=l +l.ch=l +l.ay=j.c&1 +k=j.e +j.e=l +l.ch=null +l.CW=k +if(k==null)j.d=l +else k.ch=l +if(j.d===l)A.iV(j.a) +return l}, +fv(a){var s,r=this +A.r(r).h("cJ<1>").a(a) +if(a.ch===a)return null +s=a.ay +if((s&2)!==0)a.ay=s|4 +else{r.fD(a) +if((r.c&2)===0&&r.d==null)r.dA()}return null}, +fw(a){}, +fz(a){}, +bK(){if((this.c&4)!==0)return new A.aI("Cannot add new events after calling close") +return new A.aI("Cannot add new events while doing an addStream")}, +v(a,b){if(!this.gbN())throw A.b(this.bK()) +this.b5(b)}, +a2(a,b){var s +if(!this.gbN())throw A.b(this.bK()) +s=A.nB(a,b) +this.b7(s.a,s.b)}, +n(){var s,r,q=this +if((q.c&4)!==0){s=q.r +s.toString +return s}if(!q.gbN())throw A.b(q.bK()) +q.c|=4 +r=q.r +if(r==null)r=q.r=new A.n($.m,t.D) +q.b6() +return r}, +dO(a){var s,r,q,p=this,o=p.c +if((o&2)!==0)throw A.b(A.A(u.o)) +s=p.d +if(s==null)return +r=o&1 +p.c=o^3 +while(s!=null){o=s.ay +if((o&1)===r){s.ay=o|2 +a.$1(s) +o=s.ay^=1 +q=s.ch +if((o&4)!==0)p.fD(s) +s.ay&=4294967293 +s=q}else s=s.ch}p.c&=4294967293 +if(p.d==null)p.dA()}, +dA(){if((this.c&4)!==0){var s=this.r +if((s.a&30)===0)s.b3(null)}A.iV(this.b)}, +$iae:1} +A.fp.prototype={ +gbN(){return A.cI.prototype.gbN.call(this)&&(this.c&2)===0}, +bK(){if((this.c&2)!==0)return new A.aI(u.o) +return this.hU()}, +b5(a){var s=this,r=s.d +if(r==null)return +if(r===s.e){s.c|=2 +r.aQ(a) +s.c&=4294967293 +if(s.d==null)s.dA() +return}s.dO(new A.nf(s,a))}, +b7(a,b){if(this.d==null)return +this.dO(new A.nh(this,a,b))}, +b6(){var s=this +if(s.d!=null)s.dO(new A.ng(s)) +else s.r.b3(null)}} +A.nf.prototype={ +$1(a){a.aQ(this.b)}, +$S(){return this.a.$ti.h("~(af<1>)")}} +A.nh.prototype={ +$1(a){a.a7(this.b,this.c)}, +$S(){return this.a.$ti.h("~(af<1>)")}} +A.ng.prototype={ +$1(a){a.bo()}, +$S(){return this.a.$ti.h("~(af<1>)")}} +A.kc.prototype={ +$0(){this.c.a(null) +this.b.b4(null)}, +$S:0} +A.ke.prototype={ +$2(a,b){var s=this,r=s.a,q=--r.b +if(r.a!=null){r.a=null +r.d=a +r.c=b +if(q===0||s.c)s.d.V(new A.U(a,b))}else if(q===0&&!s.c){q=r.d +q.toString +r=r.c +r.toString +s.d.V(new A.U(q,r))}}, +$S:6} +A.kd.prototype={ +$1(a){var s,r,q,p,o,n,m=this,l=m.a,k=--l.b,j=l.a +if(j!=null){J.pr(j,m.b,a) +if(J.am(k,0)){l=m.d +s=A.f([],l.h("u<0>")) +for(q=j,p=q.length,o=0;o")) +for(r=m.b,q=r.length,p=0;p")) +for(n=r.length,p=0;p1 +if(r)s="("+s+" errors)" +else s="" +return q+s+": "+A.t(p.a)}, +gaP(){var s=this.c +s=s==null?null:s.b +return s==null?A.L.prototype.gaP.call(this):s}} +A.f7.prototype={ +jD(a){this.a.bf(new A.mD(this,a),new A.mE(this,a),t.P)}} +A.mD.prototype={ +$1(a){this.a.b=a +this.b.$1(0)}, +$S(){return this.a.$ti.h("N(1)")}} +A.mE.prototype={ +$2(a,b){this.a.c=new A.U(a,b) +this.b.$1(1)}, +$S:29} +A.mC.prototype={ +$1(a){var s=this.a,r=s.a+=a +if(++s.b===this.b.length)this.c.$1(r)}, +$S:4} +A.dB.prototype={ +by(a,b){if((this.a.a&30)!==0)throw A.b(A.A("Future already completed")) +this.V(A.nB(a,b))}, +ag(a){return this.by(a,null)}} +A.a7.prototype={ +O(a){var s=this.a +if((s.a&30)!==0)throw A.b(A.A("Future already completed")) +s.b3(a)}, +aJ(){return this.O(null)}, +V(a){this.a.aR(a)}} +A.a1.prototype={ +O(a){var s=this.a +if((s.a&30)!==0)throw A.b(A.A("Future already completed")) +s.b4(a)}, +aJ(){return this.O(null)}, +V(a){this.a.V(a)}} +A.cg.prototype={ +kU(a){if((this.c&15)!==6)return!0 +return this.b.b.be(this.d,a.a,t.y,t.K)}, +kF(a){var s,r=this.e,q=null,p=t.z,o=t.K,n=a.a,m=this.b.b +if(t._.b(r))q=m.eK(r,n,a.b,p,o,t.l) +else q=m.be(r,n,p,o) +try{p=q +return p}catch(s){if(t.eK.b(A.H(s))){if((this.c&1)!==0)throw A.b(A.J("The error handler of Future.then must return a value of the returned future's type","onError")) +throw A.b(A.J("The error handler of Future.catchError must return a value of the future's type","onError"))}else throw s}}} +A.n.prototype={ +bf(a,b,c){var s,r,q=$.m +if(q===B.d){if(b!=null&&!t._.b(b)&&!t.bI.b(b))throw A.b(A.ad(b,"onError",u.c))}else{a=q.bb(a,c.h("0/"),this.$ti.c) +if(b!=null)b=A.ws(b,q)}s=new A.n($.m,c.h("n<0>")) +r=b==null?1:3 +this.cB(new A.cg(s,r,a,b,this.$ti.h("@<1>").G(c).h("cg<1,2>"))) +return s}, +bG(a,b){return this.bf(a,null,b)}, +fN(a,b,c){var s=new A.n($.m,c.h("n<0>")) +this.cB(new A.cg(s,19,a,b,this.$ti.h("@<1>").G(c).h("cg<1,2>"))) +return s}, +ai(a){var s=this.$ti,r=$.m,q=new A.n(r,s) +if(r!==B.d)a=r.az(a,t.z) +this.cB(new A.cg(q,8,a,null,s.h("cg<1,1>"))) +return q}, +jr(a){this.a=this.a&1|16 +this.c=a}, +cC(a){this.a=a.a&30|this.a&1 +this.c=a.c}, +cB(a){var s=this,r=s.a +if(r<=3){a.a=s.c +s.c=a}else{if((r&4)!==0){r=s.c +if((r.a&24)===0){r.cB(a) +return}s.cC(r)}s.b.b1(new A.mF(s,a))}}, +ft(a){var s,r,q,p,o,n=this,m={} +m.a=a +if(a==null)return +s=n.a +if(s<=3){r=n.c +n.c=a +if(r!=null){q=a.a +for(p=a;q!=null;p=q,q=o)o=q.a +p.a=r}}else{if((s&4)!==0){s=n.c +if((s.a&24)===0){s.ft(a) +return}n.cC(s)}m.a=n.cJ(a) +n.b.b1(new A.mK(m,n))}}, +bS(){var s=this.c +this.c=null +return this.cJ(s)}, +cJ(a){var s,r,q +for(s=a,r=null;s!=null;r=s,s=q){q=s.a +s.a=r}return r}, +b4(a){var s,r=this +if(r.$ti.h("C<1>").b(a))A.mI(a,r,!0) +else{s=r.bS() +r.a=8 +r.c=a +A.cM(r,s)}}, +bL(a){var s=this,r=s.bS() +s.a=8 +s.c=a +A.cM(s,r)}, +ih(a){var s,r,q,p=this +if((a.a&16)!==0){s=p.b +r=a.b +s=!(s===r||s.gaK()===r.gaK())}else s=!1 +if(s)return +q=p.bS() +p.cC(a) +A.cM(p,q)}, +V(a){var s=this.bS() +this.jr(a) +A.cM(this,s)}, +ig(a,b){this.V(new A.U(a,b))}, +b3(a){if(this.$ti.h("C<1>").b(a)){this.f5(a) +return}this.f4(a)}, +f4(a){this.a^=2 +this.b.b1(new A.mH(this,a))}, +f5(a){A.mI(a,this,!1) +return}, +aR(a){this.a^=2 +this.b.b1(new A.mG(this,a))}, +$iC:1} +A.mF.prototype={ +$0(){A.cM(this.a,this.b)}, +$S:0} +A.mK.prototype={ +$0(){A.cM(this.b,this.a.a)}, +$S:0} +A.mJ.prototype={ +$0(){A.mI(this.a.a,this.b,!0)}, +$S:0} +A.mH.prototype={ +$0(){this.a.bL(this.b)}, +$S:0} +A.mG.prototype={ +$0(){this.a.V(this.b)}, +$S:0} +A.mN.prototype={ +$0(){var s,r,q,p,o,n,m,l,k=this,j=null +try{q=k.a.a +j=q.b.b.bd(q.d,t.z)}catch(p){s=A.H(p) +r=A.a5(p) +if(k.c&&k.b.a.c.a===s){q=k.a +q.c=k.b.a.c}else{q=s +o=r +if(o==null)o=A.fP(q) +n=k.a +n.c=new A.U(q,o) +q=n}q.b=!0 +return}if(j instanceof A.n&&(j.a&24)!==0){if((j.a&16)!==0){q=k.a +q.c=j.c +q.b=!0}return}if(j instanceof A.n){m=k.b.a +l=new A.n(m.b,m.$ti) +j.bf(new A.mO(l,m),new A.mP(l),t.H) +q=k.a +q.c=l +q.b=!1}}, +$S:0} +A.mO.prototype={ +$1(a){this.a.ih(this.b)}, +$S:27} +A.mP.prototype={ +$2(a,b){this.a.V(new A.U(a,b))}, +$S:29} +A.mM.prototype={ +$0(){var s,r,q,p,o,n +try{q=this.a +p=q.a +o=p.$ti +q.c=p.b.b.be(p.d,this.b,o.h("2/"),o.c)}catch(n){s=A.H(n) +r=A.a5(n) +q=s +p=r +if(p==null)p=A.fP(q) +o=this.a +o.c=new A.U(q,p) +o.b=!0}}, +$S:0} +A.mL.prototype={ +$0(){var s,r,q,p,o,n,m,l=this +try{s=l.a.a.c +p=l.b +if(p.a.kU(s)&&p.a.e!=null){p.c=p.a.kF(s) +p.b=!1}}catch(o){r=A.H(o) +q=A.a5(o) +p=l.a.a.c +if(p.a===r){n=l.b +n.c=p +p=n}else{p=r +n=q +if(n==null)n=A.fP(p) +m=l.b +m.c=new A.U(p,n) +p=m}p.b=!0}}, +$S:0} +A.ib.prototype={} +A.X.prototype={ +gl(a){var s={},r=new A.n($.m,t.gR) +s.a=0 +this.P(new A.lg(s,this),!0,new A.lh(s,r),r.gdF()) +return r}, +gE(a){var s=new A.n($.m,A.r(this).h("n")),r=this.P(null,!0,new A.le(s),s.gdF()) +r.cd(new A.lf(this,r,s)) +return s}, +en(a,b){var s=new A.n($.m,A.r(this).h("n")),r=this.P(null,!0,new A.lc(null,s),s.gdF()) +r.cd(new A.ld(this,b,r,s)) +return s}} +A.lg.prototype={ +$1(a){++this.a.a}, +$S(){return A.r(this.b).h("~(X.T)")}} +A.lh.prototype={ +$0(){this.b.b4(this.a.a)}, +$S:0} +A.le.prototype={ +$0(){var s,r=A.l8(),q=new A.aI("No element") +A.eF(q,r) +s=A.dY(q,r) +if(s==null)s=new A.U(q,r) +this.a.V(s)}, +$S:0} +A.lf.prototype={ +$1(a){A.r7(this.b,this.c,a)}, +$S(){return A.r(this.a).h("~(X.T)")}} +A.lc.prototype={ +$0(){var s,r=A.l8(),q=new A.aI("No element") +A.eF(q,r) +s=A.dY(q,r) +if(s==null)s=new A.U(q,r) +this.b.V(s)}, +$S:0} +A.ld.prototype={ +$1(a){var s=this.c,r=this.d +A.wy(new A.la(this.b,a),new A.lb(s,r,a),A.vU(s,r))}, +$S(){return A.r(this.a).h("~(X.T)")}} +A.la.prototype={ +$0(){return this.a.$1(this.b)}, +$S:31} +A.lb.prototype={ +$1(a){if(a)A.r7(this.a,this.b,this.c)}, +$S:71} +A.hP.prototype={} +A.cS.prototype={ +gj2(){if((this.b&8)===0)return this.a +return this.a.ge8()}, +dL(){var s,r=this +if((r.b&8)===0){s=r.a +return s==null?r.a=new A.fh():s}s=r.a.ge8() +return s}, +gaV(){var s=this.a +return(this.b&8)!==0?s.ge8():s}, +dw(){if((this.b&4)!==0)return new A.aI("Cannot add event after closing") +return new A.aI("Cannot add event while adding a stream")}, +fc(){var s=this.c +if(s==null)s=this.c=(this.b&2)!==0?$.cm():new A.n($.m,t.D) +return s}, +v(a,b){var s=this,r=s.b +if(r>=4)throw A.b(s.dw()) +if((r&1)!==0)s.b5(b) +else if((r&3)===0)s.dL().v(0,new A.dD(b))}, +a2(a,b){var s,r,q=this +if(q.b>=4)throw A.b(q.dw()) +s=A.nB(a,b) +a=s.a +b=s.b +r=q.b +if((r&1)!==0)q.b7(a,b) +else if((r&3)===0)q.dL().v(0,new A.eZ(a,b))}, +jU(a){return this.a2(a,null)}, +n(){var s=this,r=s.b +if((r&4)!==0)return s.fc() +if(r>=4)throw A.b(s.dw()) +r=s.b=r|4 +if((r&1)!==0)s.b6() +else if((r&3)===0)s.dL().v(0,B.v) +return s.fc()}, +fJ(a,b,c,d){var s,r,q,p=this +if((p.b&3)!==0)throw A.b(A.A("Stream has already been listened to.")) +s=A.v6(p,a,b,c,d,A.r(p).c) +r=p.gj2() +if(((p.b|=1)&8)!==0){q=p.a +q.se8(s) +q.bc()}else p.a=s +s.js(r) +s.dP(new A.nc(p)) +return s}, +fv(a){var s,r,q,p,o,n,m,l=this,k=null +if((l.b&8)!==0)k=l.a.J() +l.a=null +l.b=l.b&4294967286|2 +s=l.r +if(s!=null)if(k==null)try{r=s.$0() +if(r instanceof A.n)k=r}catch(o){q=A.H(o) +p=A.a5(o) +n=new A.n($.m,t.D) +n.aR(new A.U(q,p)) +k=n}else k=k.ai(s) +m=new A.nb(l) +if(k!=null)k=k.ai(m) +else m.$0() +return k}, +fw(a){if((this.b&8)!==0)this.a.bD() +A.iV(this.e)}, +fz(a){if((this.b&8)!==0)this.a.bc() +A.iV(this.f)}, +$iae:1} +A.nc.prototype={ +$0(){A.iV(this.a.d)}, +$S:0} +A.nb.prototype={ +$0(){var s=this.a.c +if(s!=null&&(s.a&30)===0)s.b3(null)}, +$S:0} +A.iO.prototype={ +b5(a){this.gaV().aQ(a)}, +b7(a,b){this.gaV().a7(a,b)}, +b6(){this.gaV().bo()}} +A.ic.prototype={ +b5(a){this.gaV().bn(new A.dD(a))}, +b7(a,b){this.gaV().bn(new A.eZ(a,b))}, +b6(){this.gaV().bn(B.v)}} +A.dA.prototype={} +A.dS.prototype={} +A.at.prototype={ +gA(a){return(A.eE(this.a)^892482866)>>>0}, +T(a,b){if(b==null)return!1 +if(this===b)return!0 +return b instanceof A.at&&b.a===this.a}} +A.cf.prototype={ +cH(){return this.w.fv(this)}, +al(){this.w.fw(this)}, +am(){this.w.fz(this)}} +A.dP.prototype={ +v(a,b){this.a.v(0,b)}, +a2(a,b){this.a.a2(a,b)}, +n(){return this.a.n()}, +$iae:1} +A.af.prototype={ +js(a){var s=this +if(a==null)return +s.r=a +if(a.c!=null){s.e=(s.e|128)>>>0 +a.cu(s)}}, +cd(a){this.a=A.ih(this.d,a,A.r(this).h("af.T"))}, +eG(a){var s=this +s.e=(s.e&4294967263)>>>0 +s.b=A.ii(s.d,a)}, +bD(){var s,r,q=this,p=q.e +if((p&8)!==0)return +s=(p+256|4)>>>0 +q.e=s +if(p<256){r=q.r +if(r!=null)if(r.a===1)r.a=3}if((p&4)===0&&(s&64)===0)q.dP(q.gbO())}, +bc(){var s=this,r=s.e +if((r&8)!==0)return +if(r>=256){r=s.e=r-256 +if(r<256)if((r&128)!==0&&s.r.c!=null)s.r.cu(s) +else{r=(r&4294967291)>>>0 +s.e=r +if((r&64)===0)s.dP(s.gbP())}}}, +J(){var s=this,r=(s.e&4294967279)>>>0 +s.e=r +if((r&8)===0)s.dB() +r=s.f +return r==null?$.cm():r}, +dB(){var s,r=this,q=r.e=(r.e|8)>>>0 +if((q&128)!==0){s=r.r +if(s.a===1)s.a=3}if((q&64)===0)r.r=null +r.f=r.cH()}, +aQ(a){var s=this.e +if((s&8)!==0)return +if(s<64)this.b5(a) +else this.bn(new A.dD(a))}, +a7(a,b){var s +if(t.C.b(a))A.eF(a,b) +s=this.e +if((s&8)!==0)return +if(s<64)this.b7(a,b) +else this.bn(new A.eZ(a,b))}, +bo(){var s=this,r=s.e +if((r&8)!==0)return +r=(r|2)>>>0 +s.e=r +if(r<64)s.b6() +else s.bn(B.v)}, +al(){}, +am(){}, +cH(){return null}, +bn(a){var s,r=this,q=r.r +if(q==null)q=r.r=new A.fh() +q.v(0,a) +s=r.e +if((s&128)===0){s=(s|128)>>>0 +r.e=s +if(s<256)q.cu(r)}}, +b5(a){var s=this,r=s.e +s.e=(r|64)>>>0 +s.d.cm(s.a,a,A.r(s).h("af.T")) +s.e=(s.e&4294967231)>>>0 +s.dC((r&4)!==0)}, +b7(a,b){var s,r=this,q=r.e,p=new A.mk(r,a,b) +if((q&1)!==0){r.e=(q|16)>>>0 +r.dB() +s=r.f +if(s!=null&&s!==$.cm())s.ai(p) +else p.$0()}else{p.$0() +r.dC((q&4)!==0)}}, +b6(){var s,r=this,q=new A.mj(r) +r.dB() +r.e=(r.e|16)>>>0 +s=r.f +if(s!=null&&s!==$.cm())s.ai(q) +else q.$0()}, +dP(a){var s=this,r=s.e +s.e=(r|64)>>>0 +a.$0() +s.e=(s.e&4294967231)>>>0 +s.dC((r&4)!==0)}, +dC(a){var s,r,q=this,p=q.e +if((p&128)!==0&&q.r.c==null){p=q.e=(p&4294967167)>>>0 +s=!1 +if((p&4)!==0)if(p<256){s=q.r +s=s==null?null:s.c==null +s=s!==!1}if(s){p=(p&4294967291)>>>0 +q.e=p}}for(;;a=r){if((p&8)!==0){q.r=null +return}r=(p&4)!==0 +if(a===r)break +q.e=(p^64)>>>0 +if(r)q.al() +else q.am() +p=(q.e&4294967231)>>>0 +q.e=p}if((p&128)!==0&&p<256)q.r.cu(q)}} +A.mk.prototype={ +$0(){var s,r,q,p=this.a,o=p.e +if((o&8)!==0&&(o&16)===0)return +p.e=(o|64)>>>0 +s=p.b +o=this.b +r=t.K +q=p.d +if(t.da.b(s))q.hr(s,o,this.c,r,t.l) +else q.cm(s,o,r) +p.e=(p.e&4294967231)>>>0}, +$S:0} +A.mj.prototype={ +$0(){var s=this.a,r=s.e +if((r&16)===0)return +s.e=(r|74)>>>0 +s.d.cl(s.c) +s.e=(s.e&4294967231)>>>0}, +$S:0} +A.dN.prototype={ +P(a,b,c,d){return this.a.fJ(a,d,c,b===!0)}, +b_(a,b,c){return this.P(a,null,b,c)}, +kO(a){return this.P(a,null,null,null)}, +eB(a,b){return this.P(a,null,b,null)}} +A.il.prototype={ +gcc(){return this.a}, +scc(a){return this.a=a}} +A.dD.prototype={ +eI(a){a.b5(this.b)}} +A.eZ.prototype={ +eI(a){a.b7(this.b,this.c)}} +A.mu.prototype={ +eI(a){a.b6()}, +gcc(){return null}, +scc(a){throw A.b(A.A("No events after a done."))}} +A.fh.prototype={ +cu(a){var s=this,r=s.a +if(r===1)return +if(r>=1){s.a=1 +return}A.pe(new A.n1(s,a)) +s.a=1}, +v(a,b){var s=this,r=s.c +if(r==null)s.b=s.c=b +else{r.scc(b) +s.c=b}}} +A.n1.prototype={ +$0(){var s,r,q=this.a,p=q.a +q.a=0 +if(p===3)return +s=q.b +r=s.gcc() +q.b=r +if(r==null)q.c=null +s.eI(this.b)}, +$S:0} +A.f0.prototype={ +cd(a){}, +eG(a){}, +bD(){var s=this.a +if(s>=0)this.a=s+2}, +bc(){var s=this,r=s.a-2 +if(r<0)return +if(r===0){s.a=1 +A.pe(s.gfs())}else s.a=r}, +J(){this.a=-1 +this.c=null +return $.cm()}, +iZ(){var s,r=this,q=r.a-1 +if(q===0){r.a=-1 +s=r.c +if(s!=null){r.c=null +r.b.cl(s)}}else r.a=q}} +A.dO.prototype={ +gm(){if(this.c)return this.b +return null}, +k(){var s,r=this,q=r.a +if(q!=null){if(r.c){s=new A.n($.m,t.k) +r.b=s +r.c=!1 +q.bc() +return s}throw A.b(A.A("Already waiting for next."))}return r.iL()}, +iL(){var s,r,q=this,p=q.b +if(p!=null){s=new A.n($.m,t.k) +q.b=s +r=p.P(q.giT(),!0,q.giV(),q.giX()) +if(q.b!=null)q.a=r +return s}return $.rU()}, +J(){var s=this,r=s.a,q=s.b +s.b=null +if(r!=null){s.a=null +if(!s.c)q.b3(!1) +else s.c=!1 +return r.J()}return $.cm()}, +iU(a){var s,r,q=this +if(q.a==null)return +s=q.b +q.b=a +q.c=!0 +s.b4(!0) +if(q.c){r=q.a +if(r!=null)r.bD()}}, +iY(a,b){var s=this,r=s.a,q=s.b +s.b=s.a=null +if(r!=null)q.V(new A.U(a,b)) +else q.aR(new A.U(a,b))}, +iW(){var s=this,r=s.a,q=s.b +s.b=s.a=null +if(r!=null)q.bL(!1) +else q.f4(!1)}} +A.nx.prototype={ +$0(){return this.a.V(this.b)}, +$S:0} +A.nw.prototype={ +$2(a,b){A.vT(this.a,this.b,new A.U(a,b))}, +$S:6} +A.ny.prototype={ +$0(){return this.a.b4(this.b)}, +$S:0} +A.f5.prototype={ +P(a,b,c,d){var s=this.$ti,r=$.m,q=b===!0?1:0,p=d!=null?32:0,o=A.ih(r,a,s.y[1]),n=A.ii(r,d) +s=new A.dE(this,o,n,r.az(c,t.H),r,q|p,s.h("dE<1,2>")) +s.x=this.a.b_(s.gdQ(),s.gdS(),s.gdU()) +return s}, +b_(a,b,c){return this.P(a,null,b,c)}} +A.dE.prototype={ +aQ(a){if((this.e&2)!==0)return +this.ds(a)}, +a7(a,b){if((this.e&2)!==0)return +this.eW(a,b)}, +al(){var s=this.x +if(s!=null)s.bD()}, +am(){var s=this.x +if(s!=null)s.bc()}, +cH(){var s=this.x +if(s!=null){this.x=null +return s.J()}return null}, +dR(a){this.w.iF(a,this)}, +dV(a,b){this.a7(a,b)}, +dT(){this.bo()}} +A.fc.prototype={ +iF(a,b){var s,r,q,p,o,n,m=null +try{m=this.b.$1(a)}catch(q){s=A.H(q) +r=A.a5(q) +p=s +o=r +n=A.dY(p,o) +if(n!=null){p=n.a +o=n.b}b.a7(p,o) +return}b.aQ(m)}} +A.f2.prototype={ +v(a,b){var s=this.a +if((s.e&2)!==0)A.E(A.A("Stream is already closed")) +s.ds(b)}, +a2(a,b){this.a.a7(a,b)}, +n(){var s=this.a +if((s.e&2)!==0)A.E(A.A("Stream is already closed")) +s.eX()}, +$iae:1} +A.dL.prototype={ +aQ(a){if((this.e&2)!==0)throw A.b(A.A("Stream is already closed")) +this.ds(a)}, +a7(a,b){if((this.e&2)!==0)throw A.b(A.A("Stream is already closed")) +this.eW(a,b)}, +bo(){if((this.e&2)!==0)throw A.b(A.A("Stream is already closed")) +this.eX()}, +al(){var s=this.x +if(s!=null)s.bD()}, +am(){var s=this.x +if(s!=null)s.bc()}, +cH(){var s=this.x +if(s!=null){this.x=null +return s.J()}return null}, +dR(a){var s,r,q,p +try{q=this.w +q===$&&A.x() +q.v(0,a)}catch(p){s=A.H(p) +r=A.a5(p) +this.a7(s,r)}}, +dV(a,b){var s,r,q,p +try{q=this.w +q===$&&A.x() +q.a2(a,b)}catch(p){s=A.H(p) +r=A.a5(p) +if(s===a)this.a7(a,b) +else this.a7(s,r)}}, +dT(){var s,r,q,p +try{this.x=null +q=this.w +q===$&&A.x() +q.n()}catch(p){s=A.H(p) +r=A.a5(p) +this.a7(s,r)}}} +A.fo.prototype={ +ee(a){return new A.eU(this.a,a,this.$ti.h("eU<1,2>"))}} +A.eU.prototype={ +P(a,b,c,d){var s=this.$ti,r=$.m,q=b===!0?1:0,p=d!=null?32:0,o=A.ih(r,a,s.y[1]),n=A.ii(r,d),m=new A.dL(o,n,r.az(c,t.H),r,q|p,s.h("dL<1,2>")) +m.w=this.a.$1(new A.f2(m)) +m.x=this.b.b_(m.gdQ(),m.gdS(),m.gdU()) +return m}, +b_(a,b,c){return this.P(a,null,b,c)}} +A.dF.prototype={ +v(a,b){var s=this.d +if(s==null)throw A.b(A.A("Sink is closed")) +this.$ti.y[1].a(b) +s.a.aQ(b)}, +a2(a,b){var s=this.d +if(s==null)throw A.b(A.A("Sink is closed")) +s.a2(a,b)}, +n(){var s=this.d +if(s==null)return +this.d=null +this.c.$1(s)}, +$iae:1} +A.dM.prototype={ +ee(a){return this.hV(a)}} +A.nd.prototype={ +$1(a){var s=this +return new A.dF(s.a,s.b,s.c,a,s.e.h("@<0>").G(s.d).h("dF<1,2>"))}, +$S(){return this.e.h("@<0>").G(this.d).h("dF<1,2>(ae<2>)")}} +A.av.prototype={} +A.iU.prototype={ +bQ(a,b,c){var s,r,q,p,o,n,m,l,k=this.gdW(),j=k.a +if(j===B.d){A.fF(b,c) +return}s=k.b +r=j.ga0() +m=j.ghi() +m.toString +q=m +p=$.m +try{$.m=q +s.$5(j,r,a,b,c) +$.m=p}catch(l){o=A.H(l) +n=A.a5(l) +$.m=p +m=b===o?c:n +q.bQ(j,o,m)}}, +$iv:1} +A.ij.prototype={ +gf3(){var s=this.at +return s==null?this.at=new A.dV(this):s}, +ga0(){return this.ax.gf3()}, +gaK(){return this.as.a}, +cl(a){var s,r,q +try{this.bd(a,t.H)}catch(q){s=A.H(q) +r=A.a5(q) +this.bQ(this,s,r)}}, +cm(a,b,c){var s,r,q +try{this.be(a,b,t.H,c)}catch(q){s=A.H(q) +r=A.a5(q) +this.bQ(this,s,r)}}, +hr(a,b,c,d,e){var s,r,q +try{this.eK(a,b,c,t.H,d,e)}catch(q){s=A.H(q) +r=A.a5(q) +this.bQ(this,s,r)}}, +ef(a,b){return new A.mr(this,this.az(a,b),b)}, +fX(a,b,c){return new A.mt(this,this.bb(a,b,c),c,b)}, +c2(a){return new A.mq(this,this.az(a,t.H))}, +eg(a,b){return new A.ms(this,this.bb(a,t.H,b),b)}, +j(a,b){var s,r=this.ay,q=r.j(0,b) +if(q!=null||r.a3(b))return q +s=this.ax.j(0,b) +if(s!=null)r.t(0,b,s) +return s}, +c7(a,b){this.bQ(this,a,b)}, +h8(a,b){var s=this.Q,r=s.a +return s.b.$5(r,r.ga0(),this,a,b)}, +bd(a){var s=this.a,r=s.a +return s.b.$4(r,r.ga0(),this,a)}, +be(a,b){var s=this.b,r=s.a +return s.b.$5(r,r.ga0(),this,a,b)}, +eK(a,b,c){var s=this.c,r=s.a +return s.b.$6(r,r.ga0(),this,a,b,c)}, +az(a){var s=this.d,r=s.a +return s.b.$4(r,r.ga0(),this,a)}, +bb(a){var s=this.e,r=s.a +return s.b.$4(r,r.ga0(),this,a)}, +dc(a){var s=this.f,r=s.a +return s.b.$4(r,r.ga0(),this,a)}, +h4(a,b){var s=this.r,r=s.a +if(r===B.d)return null +return s.b.$5(r,r.ga0(),this,a,b)}, +b1(a){var s=this.w,r=s.a +return s.b.$4(r,r.ga0(),this,a)}, +ei(a,b){var s=this.x,r=s.a +return s.b.$5(r,r.ga0(),this,a,b)}, +hj(a){var s=this.z,r=s.a +return s.b.$4(r,r.ga0(),this,a)}, +gfF(){return this.a}, +gfH(){return this.b}, +gfG(){return this.c}, +gfB(){return this.d}, +gfC(){return this.e}, +gfA(){return this.f}, +gfe(){return this.r}, +ge3(){return this.w}, +gf9(){return this.x}, +gf8(){return this.y}, +gfu(){return this.z}, +gfh(){return this.Q}, +gdW(){return this.as}, +ghi(){return this.ax}, +gfn(){return this.ay}} +A.mr.prototype={ +$0(){return this.a.bd(this.b,this.c)}, +$S(){return this.c.h("0()")}} +A.mt.prototype={ +$1(a){var s=this +return s.a.be(s.b,a,s.d,s.c)}, +$S(){return this.d.h("@<0>").G(this.c).h("1(2)")}} +A.mq.prototype={ +$0(){return this.a.cl(this.b)}, +$S:0} +A.ms.prototype={ +$1(a){return this.a.cm(this.b,a,this.c)}, +$S(){return this.c.h("~(0)")}} +A.iI.prototype={ +gfF(){return B.bp}, +gfH(){return B.br}, +gfG(){return B.bq}, +gfB(){return B.bo}, +gfC(){return B.bj}, +gfA(){return B.bt}, +gfe(){return B.bl}, +ge3(){return B.bs}, +gf9(){return B.bk}, +gf8(){return B.bi}, +gfu(){return B.bn}, +gfh(){return B.bm}, +gdW(){return B.bh}, +ghi(){return null}, +gfn(){return $.tc()}, +gf3(){var s=$.n4 +return s==null?$.n4=new A.dV(this):s}, +ga0(){var s=$.n4 +return s==null?$.n4=new A.dV(this):s}, +gaK(){return this}, +cl(a){var s,r,q +try{if(B.d===$.m){a.$0() +return}A.nD(null,null,this,a)}catch(q){s=A.H(q) +r=A.a5(q) +A.fF(s,r)}}, +cm(a,b){var s,r,q +try{if(B.d===$.m){a.$1(b) +return}A.nF(null,null,this,a,b)}catch(q){s=A.H(q) +r=A.a5(q) +A.fF(s,r)}}, +hr(a,b,c){var s,r,q +try{if(B.d===$.m){a.$2(b,c) +return}A.nE(null,null,this,a,b,c)}catch(q){s=A.H(q) +r=A.a5(q) +A.fF(s,r)}}, +ef(a,b){return new A.n6(this,a,b)}, +fX(a,b,c){return new A.n8(this,a,c,b)}, +c2(a){return new A.n5(this,a)}, +eg(a,b){return new A.n7(this,a,b)}, +j(a,b){return null}, +c7(a,b){A.fF(a,b)}, +h8(a,b){return A.rl(null,null,this,a,b)}, +bd(a){if($.m===B.d)return a.$0() +return A.nD(null,null,this,a)}, +be(a,b){if($.m===B.d)return a.$1(b) +return A.nF(null,null,this,a,b)}, +eK(a,b,c){if($.m===B.d)return a.$2(b,c) +return A.nE(null,null,this,a,b,c)}, +az(a){return a}, +bb(a){return a}, +dc(a){return a}, +h4(a,b){return null}, +b1(a){A.nG(null,null,this,a)}, +ei(a,b){return A.oA(a,b)}, +hj(a){A.pd(a)}} +A.n6.prototype={ +$0(){return this.a.bd(this.b,this.c)}, +$S(){return this.c.h("0()")}} +A.n8.prototype={ +$1(a){var s=this +return s.a.be(s.b,a,s.d,s.c)}, +$S(){return this.d.h("@<0>").G(this.c).h("1(2)")}} +A.n5.prototype={ +$0(){return this.a.cl(this.b)}, +$S:0} +A.n7.prototype={ +$1(a){return this.a.cm(this.b,a,this.c)}, +$S(){return this.c.h("~(0)")}} +A.dV.prototype={$iT:1} +A.nC.prototype={ +$0(){A.pH(this.a,this.b)}, +$S:0} +A.fz.prototype={$ioE:1} +A.cN.prototype={ +gl(a){return this.a}, +gB(a){return this.a===0}, +gX(){return new A.cO(this,A.r(this).h("cO<1>"))}, +gbH(){var s=A.r(this) +return A.ht(new A.cO(this,s.h("cO<1>")),new A.mQ(this),s.c,s.y[1])}, +a3(a){var s,r +if(typeof a=="string"&&a!=="__proto__"){s=this.b +return s==null?!1:s[a]!=null}else if(typeof a=="number"&&(a&1073741823)===a){r=this.c +return r==null?!1:r[a]!=null}else return this.im(a)}, +im(a){var s=this.d +if(s==null)return!1 +return this.aS(this.fi(s,a),a)>=0}, +j(a,b){var s,r,q +if(typeof b=="string"&&b!=="__proto__"){s=this.b +r=s==null?null:A.qF(s,b) +return r}else if(typeof b=="number"&&(b&1073741823)===b){q=this.c +r=q==null?null:A.qF(q,b) +return r}else return this.iD(b)}, +iD(a){var s,r,q=this.d +if(q==null)return null +s=this.fi(q,a) +r=this.aS(s,a) +return r<0?null:s[r+1]}, +t(a,b,c){var s,r,q=this +if(typeof b=="string"&&b!=="__proto__"){s=q.b +q.f2(s==null?q.b=A.oL():s,b,c)}else if(typeof b=="number"&&(b&1073741823)===b){r=q.c +q.f2(r==null?q.c=A.oL():r,b,c)}else q.jq(b,c)}, +jq(a,b){var s,r,q,p=this,o=p.d +if(o==null)o=p.d=A.oL() +s=p.dG(a) +r=o[s] +if(r==null){A.oM(o,s,[a,b]);++p.a +p.e=null}else{q=p.aS(r,a) +if(q>=0)r[q+1]=b +else{r.push(a,b);++p.a +p.e=null}}}, +ar(a,b){var s,r,q,p,o,n=this,m=n.f7() +for(s=m.length,r=A.r(n).y[1],q=0;q"))}} +A.is.prototype={ +gm(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +k(){var s=this,r=s.b,q=s.c,p=s.a +if(r!==p.e)throw A.b(A.an(p)) +else if(q>=r.length){s.d=null +return!1}else{s.d=r[q] +s.c=q+1 +return!0}}} +A.fa.prototype={ +gq(a){var s=this,r=new A.dI(s,s.r,s.$ti.h("dI<1>")) +r.c=s.e +return r}, +gl(a){return this.a}, +gB(a){return this.a===0}, +H(a,b){var s,r +if(b!=="__proto__"){s=this.b +if(s==null)return!1 +return s[b]!=null}else{r=this.il(b) +return r}}, +il(a){var s=this.d +if(s==null)return!1 +return this.aS(s[B.a.gA(a)&1073741823],a)>=0}, +gE(a){var s=this.e +if(s==null)throw A.b(A.A("No elements")) +return s.a}, +gD(a){var s=this.f +if(s==null)throw A.b(A.A("No elements")) +return s.a}, +v(a,b){var s,r,q=this +if(typeof b=="string"&&b!=="__proto__"){s=q.b +return q.f1(s==null?q.b=A.oN():s,b)}else if(typeof b=="number"&&(b&1073741823)===b){r=q.c +return q.f1(r==null?q.c=A.oN():r,b)}else return q.i4(b)}, +i4(a){var s,r,q=this,p=q.d +if(p==null)p=q.d=A.oN() +s=J.aE(a)&1073741823 +r=p[s] +if(r==null)p[s]=[q.dZ(a)] +else{if(q.aS(r,a)>=0)return!1 +r.push(q.dZ(a))}return!0}, +F(a,b){var s +if(typeof b=="string"&&b!=="__proto__")return this.jc(this.b,b) +else{s=this.jb(b) +return s}}, +jb(a){var s,r,q,p,o=this.d +if(o==null)return!1 +s=J.aE(a)&1073741823 +r=o[s] +q=this.aS(r,a) +if(q<0)return!1 +p=r.splice(q,1)[0] +if(0===r.length)delete o[s] +this.fR(p) +return!0}, +f1(a,b){if(a[b]!=null)return!1 +a[b]=this.dZ(b) +return!0}, +jc(a,b){var s +if(a==null)return!1 +s=a[b] +if(s==null)return!1 +this.fR(s) +delete a[b] +return!0}, +fp(){this.r=this.r+1&1073741823}, +dZ(a){var s,r=this,q=new A.n_(a) +if(r.e==null)r.e=r.f=q +else{s=r.f +s.toString +q.c=s +r.f=s.b=q}++r.a +r.fp() +return q}, +fR(a){var s=this,r=a.c,q=a.b +if(r==null)s.e=q +else r.b=q +if(q==null)s.f=r +else q.c=r;--s.a +s.fp()}, +aS(a,b){var s,r +if(a==null)return-1 +s=a.length +for(r=0;r"))}, +gl(a){return this.b}, +c3(a){var s,r,q,p=this;++p.a +if(p.b===0)return +s=p.c +s.toString +r=s +do{q=r.b +q.toString +r.b=r.c=r.a=null +if(q!==s){r=q +continue}else break}while(!0) +p.c=null +p.b=0}, +gE(a){var s +if(this.b===0)throw A.b(A.A("No such element")) +s=this.c +s.toString +return s}, +gD(a){var s +if(this.b===0)throw A.b(A.A("No such element")) +s=this.c.c +s.toString +return s}, +gB(a){return this.b===0}, +cD(a,b,c){var s,r,q=this +if(b.a!=null)throw A.b(A.A("LinkedListEntry is already in a LinkedList"));++q.a +b.a=q +s=q.b +if(s===0){b.b=b +q.c=b.c=b +q.b=s+1 +return}r=a.c +r.toString +b.c=r +b.b=a +a.c=r.b=b +q.b=s+1}, +e6(a){var s,r,q=this;++q.a +s=a.b +s.c=a.c +a.c.b=s +r=--q.b +a.a=a.b=a.c=null +if(r===0)q.c=null +else if(a===q.c)q.c=s}} +A.iz.prototype={ +gm(){var s=this.c +return s==null?this.$ti.c.a(s):s}, +k(){var s=this,r=s.a +if(s.b!==r.a)throw A.b(A.an(s)) +if(r.b!==0)r=s.e&&s.d===r.gE(0) +else r=!0 +if(r){s.c=null +return!1}s.e=!0 +r=s.d +s.c=r +s.d=r.b +return!0}} +A.ay.prototype={ +gcf(){var s=this.a +if(s==null||this===s.gE(0))return null +return this.c}} +A.w.prototype={ +gq(a){return new A.b4(a,this.gl(a),A.aU(a).h("b4"))}, +K(a,b){return this.j(a,b)}, +gB(a){return this.gl(a)===0}, +gE(a){if(this.gl(a)===0)throw A.b(A.aw()) +return this.j(a,0)}, +gD(a){if(this.gl(a)===0)throw A.b(A.aw()) +return this.j(a,this.gl(a)-1)}, +ba(a,b,c){return new A.D(a,b,A.aU(a).h("@").G(c).h("D<1,2>"))}, +U(a,b){return A.b6(a,b,null,A.aU(a).h("w.E"))}, +ah(a,b){return A.b6(a,0,A.cV(b,"count",t.S),A.aU(a).h("w.E"))}, +aC(a,b){var s,r,q,p,o=this +if(o.gB(a)){s=J.pR(0,A.aU(a).h("w.E")) +return s}r=o.j(a,0) +q=A.b5(o.gl(a),r,!0,A.aU(a).h("w.E")) +for(p=1;p").G(b).h("ai<1,2>"))}, +a_(a,b,c){var s,r=this.gl(a) +A.bd(b,c,r) +s=A.ak(this.ct(a,b,c),A.aU(a).h("w.E")) +return s}, +ct(a,b,c){A.bd(b,c,this.gl(a)) +return A.b6(a,b,c,A.aU(a).h("w.E"))}, +em(a,b,c,d){var s +A.bd(b,c,this.gl(a)) +for(s=b;sp.gl(q))throw A.b(A.pP()) +if(r=0;--o)this.t(a,b+o,p.j(q,r+o)) +else for(o=0;o"))}, +gl(a){return J.aC(this.gX())}, +gB(a){return J.oa(this.gX())}, +gbH(){return new A.fb(this,A.r(this).h("fb"))}, +i(a){return A.oq(this)}, +$iap:1} +A.kz.prototype={ +$1(a){var s=this.a,r=s.j(0,a) +if(r==null)r=A.r(s).h("S.V").a(r) +return new A.aP(a,r,A.r(s).h("aP"))}, +$S(){return A.r(this.a).h("aP(S.K)")}} +A.kA.prototype={ +$2(a,b){var s,r=this.a +if(!r.a)this.b.a+=", " +r.a=!1 +r=this.b +s=A.t(a) +r.a=(r.a+=s)+": " +s=A.t(b) +r.a+=s}, +$S:98} +A.fb.prototype={ +gl(a){var s=this.a +return s.gl(s)}, +gB(a){var s=this.a +return s.gB(s)}, +gE(a){var s=this.a +s=s.j(0,J.j0(s.gX())) +return s==null?this.$ti.y[1].a(s):s}, +gD(a){var s=this.a +s=s.j(0,J.ob(s.gX())) +return s==null?this.$ti.y[1].a(s):s}, +gq(a){var s=this.a +return new A.iA(J.Z(s.gX()),s,this.$ti.h("iA<1,2>"))}} +A.iA.prototype={ +k(){var s=this,r=s.a +if(r.k()){s.c=s.b.j(0,r.gm()) +return!0}s.c=null +return!1}, +gm(){var s=this.c +return s==null?this.$ti.y[1].a(s):s}} +A.dp.prototype={ +gB(a){return this.a===0}, +ba(a,b,c){return new A.cu(this,b,this.$ti.h("@<1>").G(c).h("cu<1,2>"))}, +i(a){return A.ol(this,"{","}")}, +ah(a,b){return A.oz(this,b,this.$ti.c)}, +U(a,b){return A.qe(this,b,this.$ti.c)}, +gE(a){var s,r=A.iy(this,this.r,this.$ti.c) +if(!r.k())throw A.b(A.aw()) +s=r.d +return s==null?r.$ti.c.a(s):s}, +gD(a){var s,r,q=A.iy(this,this.r,this.$ti.c) +if(!q.k())throw A.b(A.aw()) +s=q.$ti.c +do{r=q.d +if(r==null)r=s.a(r)}while(q.k()) +return r}, +K(a,b){var s,r,q,p=this +A.ab(b,"index") +s=A.iy(p,p.r,p.$ti.c) +for(r=b;s.k();){if(r===0){q=s.d +return q==null?s.$ti.c.a(q):q}--r}throw A.b(A.hf(b,b-r,p,null,"index"))}, +$iq:1, +$id:1} +A.fk.prototype={} +A.nr.prototype={ +$0(){var s,r +try{s=new TextDecoder("utf-8",{fatal:true}) +return s}catch(r){}return null}, +$S:23} +A.nq.prototype={ +$0(){var s,r +try{s=new TextDecoder("utf-8",{fatal:false}) +return s}catch(r){}return null}, +$S:23} +A.fM.prototype={ +kB(a){return B.ac.a4(a)}} +A.iR.prototype={ +a4(a){var s,r,q,p=A.bd(0,null,a.length),o=new Uint8Array(p) +for(s=~this.a,r=0;r=0){g="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charCodeAt(f) +if(g===k)continue +k=g}else{if(f===-1){if(o<0){e=p==null?null:p.a.length +if(e==null)e=0 +o=e+(r-q) +n=r}++m +if(k===61)continue}k=g}if(f!==-2){if(p==null){p=new A.aD("") +e=p}else e=p +e.a+=B.a.p(a0,q,r) +d=A.aR(k) +e.a+=d +q=l +continue}}throw A.b(A.aj("Invalid base64 data",a0,r))}if(p!=null){e=B.a.p(a0,q,a2) +e=p.a+=e +d=e.length +if(o>=0)A.pt(a0,n,a2,o,m,d) +else{c=B.b.ab(d-1,4)+1 +if(c===1)throw A.b(A.aj(a,a0,a2)) +while(c<4){e+="=" +p.a=e;++c}}e=p.a +return B.a.aO(a0,a1,a2,e.charCodeAt(0)==0?e:e)}b=a2-a1 +if(o>=0)A.pt(a0,n,a2,o,m,b) +else{c=B.b.ab(b,4) +if(c===1)throw A.b(A.aj(a,a0,a2)) +if(c>1)a0=B.a.aO(a0,a2,a2,c===2?"==":"=")}return a0}} +A.fR.prototype={} +A.cr.prototype={} +A.cs.prototype={} +A.h8.prototype={} +A.i0.prototype={ +cX(a){return new A.fy(!1).dH(a,0,null,!0)}} +A.i1.prototype={ +a4(a){var s,r,q=A.bd(0,null,a.length) +if(q===0)return new Uint8Array(0) +s=new Uint8Array(q*3) +r=new A.ns(s) +if(r.iC(a,0,q)!==q)r.e9() +return B.e.a_(s,0,r.b)}} +A.ns.prototype={ +e9(){var s=this,r=s.c,q=s.b,p=s.b=q+1 +r.$flags&2&&A.y(r) +r[q]=239 +q=s.b=p+1 +r[p]=191 +s.b=q+1 +r[q]=189}, +jG(a,b){var s,r,q,p,o=this +if((b&64512)===56320){s=65536+((a&1023)<<10)|b&1023 +r=o.c +q=o.b +p=o.b=q+1 +r.$flags&2&&A.y(r) +r[q]=s>>>18|240 +q=o.b=p+1 +r[p]=s>>>12&63|128 +p=o.b=q+1 +r[q]=s>>>6&63|128 +o.b=p+1 +r[p]=s&63|128 +return!0}else{o.e9() +return!1}}, +iC(a,b,c){var s,r,q,p,o,n,m,l,k=this +if(b!==c&&(a.charCodeAt(c-1)&64512)===55296)--c +for(s=k.c,r=s.$flags|0,q=s.length,p=b;p=q)break +k.b=n+1 +r&2&&A.y(s) +s[n]=o}else{n=o&64512 +if(n===55296){if(k.b+4>q)break +m=p+1 +if(k.jG(o,a.charCodeAt(m)))p=m}else if(n===56320){if(k.b+3>q)break +k.e9()}else if(o<=2047){n=k.b +l=n+1 +if(l>=q)break +k.b=l +r&2&&A.y(s) +s[n]=o>>>6|192 +k.b=l+1 +s[l]=o&63|128}else{n=k.b +if(n+2>=q)break +l=k.b=n+1 +r&2&&A.y(s) +s[n]=o>>>12|224 +n=k.b=l+1 +s[l]=o>>>6&63|128 +k.b=n+1 +s[n]=o&63|128}}}return p}} +A.fy.prototype={ +dH(a,b,c,d){var s,r,q,p,o,n,m=this,l=A.bd(b,c,J.aC(a)) +if(b===l)return"" +if(a instanceof Uint8Array){s=a +r=s +q=0}else{r=A.vE(a,b,l) +l-=b +q=b +b=0}if(d&&l-b>=15){p=m.a +o=A.vD(p,r,b,l) +if(o!=null){if(!p)return o +if(o.indexOf("\ufffd")<0)return o}}o=m.dJ(r,b,l,d) +p=m.b +if((p&1)!==0){n=A.vF(p) +m.b=0 +throw A.b(A.aj(n,a,q+m.c))}return o}, +dJ(a,b,c,d){var s,r,q=this +if(c-b>1000){s=B.b.I(b+c,2) +r=q.dJ(a,b,s,!1) +if((q.b&1)!==0)return r +return r+q.dJ(a,s,c,d)}return q.k9(a,b,c,d)}, +k9(a,b,c,d){var s,r,q,p,o,n,m,l=this,k=65533,j=l.b,i=l.c,h=new A.aD(""),g=b+1,f=a[b] +A:for(s=l.a;;){for(;;g=p){r="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHIHHHJEEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBKCCCCCCCCCCCCDCLONNNMEEEEEEEEEEE".charCodeAt(f)&31 +i=j<=32?f&61694>>>r:(f&63|i<<6)>>>0 +j=" \x000:XECCCCCN:lDb \x000:XECCCCCNvlDb \x000:XECCCCCN:lDb AAAAA\x00\x00\x00\x00\x00AAAAA00000AAAAA:::::AAAAAGG000AAAAA00KKKAAAAAG::::AAAAA:IIIIAAAAA000\x800AAAAA\x00\x00\x00\x00 AAAAA".charCodeAt(j+r) +if(j===0){q=A.aR(i) +h.a+=q +if(g===c)break A +break}else if((j&1)!==0){if(s)switch(j){case 69:case 67:q=A.aR(k) +h.a+=q +break +case 65:q=A.aR(k) +h.a+=q;--g +break +default:q=A.aR(k) +h.a=(h.a+=q)+q +break}else{l.b=j +l.c=g-1 +return""}j=0}if(g===c)break A +p=g+1 +f=a[g]}p=g+1 +f=a[g] +if(f<128){for(;;){if(!(p=128){o=n-1 +p=n +break}p=n}if(o-g<20)for(m=g;m32)if(s){s=A.aR(k) +h.a+=s}else{l.b=77 +l.c=c +return""}l.b=j +l.c=i +s=h.a +return s.charCodeAt(0)==0?s:s}} +A.a8.prototype={ +aj(a){var s,r,q=this,p=q.c +if(p===0)return q +s=!q.a +r=q.b +p=A.aS(p,r) +return new A.a8(p===0?!1:s,r,p)}, +iw(a){var s,r,q,p,o,n,m=this.c +if(m===0)return $.bb() +s=m+a +r=this.b +q=new Uint16Array(s) +for(p=m-1;p>=0;--p)q[p+a]=r[p] +o=this.a +n=A.aS(s,q) +return new A.a8(n===0?!1:o,q,n)}, +ix(a){var s,r,q,p,o,n,m,l=this,k=l.c +if(k===0)return $.bb() +s=k-a +if(s<=0)return l.a?$.po():$.bb() +r=l.b +q=new Uint16Array(s) +for(p=a;p>>0!==0)return l.cw(0,$.d_()) +for(k=0;k=0)return q.cA(b,r) +return b.cA(q,!r)}, +cw(a,b){var s,r,q=this,p=q.c +if(p===0)return b.aj(0) +s=b.c +if(s===0)return q +r=q.a +if(r!==b.a)return q.dv(b,r) +if(A.mg(q.b,p,b.b,s)>=0)return q.cA(b,r) +return b.cA(q,!r)}, +bI(a,b){var s,r,q,p,o,n,m,l=this.c,k=b.c +if(l===0||k===0)return $.bb() +s=l+k +r=this.b +q=b.b +p=new Uint16Array(s) +for(o=0;o0?p.aj(0):p}, +ja(a){var s,r,q,p=this +if(p.c0)q=q.bl(0,$.oH.ae()) +return p.a&&q.c>0?q.aj(0):q}, +fb(a){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c=this,b=c.c +if(b===$.qz&&a.c===$.qB&&c.b===$.qy&&a.b===$.qA)return +s=a.b +r=a.c +q=16-B.b.gfY(s[r-1]) +if(q>0){p=new Uint16Array(r+5) +o=A.qx(s,r,q,p) +n=new Uint16Array(b+5) +m=A.qx(c.b,b,q,n)}else{n=A.oI(c.b,0,b,b+2) +o=r +p=s +m=b}l=p[o-1] +k=m-o +j=new Uint16Array(m) +i=A.oJ(p,o,k,j) +h=m+1 +g=n.$flags|0 +if(A.mg(n,m,j,i)>=0){g&2&&A.y(n) +n[m]=1 +A.ig(n,h,j,i,n)}else{g&2&&A.y(n) +n[m]=0}f=new Uint16Array(o+2) +f[o]=1 +A.ig(f,o+1,p,o,f) +e=m-1 +while(k>0){d=A.v1(l,n,e);--k +A.qD(d,f,0,n,k,o) +if(n[e]1){q=$.pn() +if(q.c===0)A.E(B.ag) +p=r.ja(q).i(0) +s.push(p) +o=p.length +if(o===1)s.push("000") +if(o===2)s.push("00") +if(o===3)s.push("0") +r=r.iv(q)}s.push(B.b.i(r.b[0])) +if(m)s.push("-") +return new A.eG(s,t.bJ).c8(0)}} +A.mh.prototype={ +$2(a,b){a=a+b&536870911 +a=a+((a&524287)<<10)&536870911 +return a^a>>>6}, +$S:119} +A.mi.prototype={ +$1(a){a=a+((a&67108863)<<3)&536870911 +a^=a>>>11 +return a+((a&16383)<<15)&536870911}, +$S:37} +A.iq.prototype={ +fW(a,b,c){var s=this.a +if(s!=null)s.register(a,b,c)}, +h2(a){var s=this.a +if(s!=null)s.unregister(a)}} +A.eh.prototype={ +T(a,b){if(b==null)return!1 +return b instanceof A.eh&&this.a===b.a&&this.b===b.b&&this.c===b.c}, +gA(a){return A.eB(this.a,this.b,B.f,B.f)}, +af(a,b){var s=B.b.af(this.a,b.a) +if(s!==0)return s +return B.b.af(this.b,b.b)}, +i(a){var s=this,r=A.tX(A.q4(s)),q=A.h0(A.q2(s)),p=A.h0(A.q_(s)),o=A.h0(A.q0(s)),n=A.h0(A.q1(s)),m=A.h0(A.q3(s)),l=A.pC(A.uu(s)),k=s.b,j=k===0?"":A.pC(k) +k=r+"-"+q +if(s.c)return k+"-"+p+" "+o+":"+n+":"+m+"."+l+j+"Z" +else return k+"-"+p+" "+o+":"+n+":"+m+"."+l+j}} +A.bx.prototype={ +T(a,b){if(b==null)return!1 +return b instanceof A.bx&&this.a===b.a}, +gA(a){return B.b.gA(this.a)}, +af(a,b){return B.b.af(this.a,b.a)}, +i(a){var s,r,q,p,o,n=this.a,m=B.b.I(n,36e8),l=n%36e8 +if(n<0){m=0-m +n=0-l +s="-"}else{n=l +s=""}r=B.b.I(n,6e7) +n%=6e7 +q=r<10?"0":"" +p=B.b.I(n,1e6) +o=p<10?"0":"" +return s+m+":"+q+r+":"+o+p+"."+B.a.l0(B.b.i(n%1e6),6,"0")}} +A.mv.prototype={ +i(a){return this.ad()}} +A.L.prototype={ +gaP(){return A.ut(this)}} +A.fO.prototype={ +i(a){var s=this.a +if(s!=null)return"Assertion failed: "+A.h9(s) +return"Assertion failed"}} +A.bL.prototype={} +A.bc.prototype={ +gdN(){return"Invalid argument"+(!this.a?"(s)":"")}, +gdM(){return""}, +i(a){var s=this,r=s.c,q=r==null?"":" ("+r+")",p=s.d,o=p==null?"":": "+A.t(p),n=s.gdN()+q+o +if(!s.a)return n +return n+s.gdM()+": "+A.h9(s.gex())}, +gex(){return this.b}} +A.dk.prototype={ +gex(){return this.b}, +gdN(){return"RangeError"}, +gdM(){var s,r=this.e,q=this.f +if(r==null)s=q!=null?": Not less than or equal to "+A.t(q):"" +else if(q==null)s=": Not greater than or equal to "+A.t(r) +else if(q>r)s=": Not in inclusive range "+A.t(r)+".."+A.t(q) +else s=qe.length +else s=!1 +if(s)f=null +if(f==null){if(e.length>78)e=B.a.p(e,0,75)+"..." +return g+"\n"+e}for(r=1,q=0,p=!1,o=0;o1?g+(" (at line "+r+", character "+(f-q+1)+")\n"):g+(" (at character "+(f+1)+")\n") +m=e.length +for(o=f;o78){k="..." +if(f-q<75){j=q+75 +i=q}else{if(m-f<75){i=m-75 +j=m +k=""}else{i=f-36 +j=f+36}l="..."}}else{j=m +i=q +k=""}return g+l+B.a.p(e,i,j)+k+"\n"+B.a.bI(" ",f-i+l.length)+"^\n"}else return f!=null?g+(" (at offset "+A.t(f)+")"):g}, +$ia6:1} +A.hh.prototype={ +gaP(){return null}, +i(a){return"IntegerDivisionByZeroException"}, +$iL:1, +$ia6:1} +A.d.prototype={ +bx(a,b){return A.ed(this,A.r(this).h("d.E"),b)}, +ba(a,b,c){return A.ht(this,b,A.r(this).h("d.E"),c)}, +aC(a,b){var s=A.r(this).h("d.E") +if(b)s=A.ak(this,s) +else{s=A.ak(this,s) +s.$flags=1 +s=s}return s}, +cn(a){return this.aC(0,!0)}, +gl(a){var s,r=this.gq(this) +for(s=0;r.k();)++s +return s}, +gB(a){return!this.gq(this).k()}, +ah(a,b){return A.oz(this,b,A.r(this).h("d.E"))}, +U(a,b){return A.qe(this,b,A.r(this).h("d.E"))}, +hL(a,b){return new A.eI(this,b,A.r(this).h("eI"))}, +gE(a){var s=this.gq(this) +if(!s.k())throw A.b(A.aw()) +return s.gm()}, +gD(a){var s,r=this.gq(this) +if(!r.k())throw A.b(A.aw()) +do s=r.gm() +while(r.k()) +return s}, +K(a,b){var s,r +A.ab(b,"index") +s=this.gq(this) +for(r=b;s.k();){if(r===0)return s.gm();--r}throw A.b(A.hf(b,b-r,this,null,"index"))}, +i(a){return A.ud(this,"(",")")}} +A.aP.prototype={ +i(a){return"MapEntry("+A.t(this.a)+": "+A.t(this.b)+")"}} +A.N.prototype={ +gA(a){return A.e.prototype.gA.call(this,0)}, +i(a){return"null"}} +A.e.prototype={$ie:1, +T(a,b){return this===b}, +gA(a){return A.eE(this)}, +i(a){return"Instance of '"+A.hH(this)+"'"}, +gS(a){return A.xh(this)}, +toString(){return this.i(this)}} +A.dQ.prototype={ +i(a){return this.a}, +$ia_:1} +A.aD.prototype={ +gl(a){return this.a.length}, +i(a){var s=this.a +return s.charCodeAt(0)==0?s:s}} +A.ly.prototype={ +$2(a,b){throw A.b(A.aj("Illegal IPv6 address, "+a,this.a,b))}, +$S:57} +A.fv.prototype={ +gfM(){var s,r,q,p,o=this,n=o.w +if(n===$){s=o.a +r=s.length!==0?s+":":"" +q=o.c +p=q==null +if(!p||s==="file"){s=r+"//" +r=o.b +if(r.length!==0)s=s+r+"@" +if(!p)s+=q +r=o.d +if(r!=null)s=s+":"+A.t(r)}else s=r +s+=o.e +r=o.f +if(r!=null)s=s+"?"+r +r=o.r +if(r!=null)s=s+"#"+r +n=o.w=s.charCodeAt(0)==0?s:s}return n}, +gl1(){var s,r,q=this,p=q.x +if(p===$){s=q.e +if(s.length!==0&&s.charCodeAt(0)===47)s=B.a.L(s,1) +r=s.length===0?B.x:A.aO(new A.D(A.f(s.split("/"),t.s),A.x6(),t.do),t.N) +q.x!==$&&A.pj() +p=q.x=r}return p}, +gA(a){var s,r=this,q=r.y +if(q===$){s=B.a.gA(r.gfM()) +r.y!==$&&A.pj() +r.y=s +q=s}return q}, +geO(){return this.b}, +gb9(){var s=this.c +if(s==null)return"" +if(B.a.u(s,"[")&&!B.a.C(s,"v",1))return B.a.p(s,1,s.length-1) +return s}, +gce(){var s=this.d +return s==null?A.qT(this.a):s}, +gcg(){var s=this.f +return s==null?"":s}, +gd0(){var s=this.r +return s==null?"":s}, +kL(a){var s=this.a +if(a.length!==s.length)return!1 +return A.vV(a,s,0)>=0}, +ho(a){var s,r,q,p,o,n,m,l=this +a=A.np(a,0,a.length) +s=a==="file" +r=l.b +q=l.d +if(a!==l.a)q=A.no(q,a) +p=l.c +if(!(p!=null))p=r.length!==0||q!=null||s?"":null +o=l.e +if(!s)n=p!=null&&o.length!==0 +else n=!0 +if(n&&!B.a.u(o,"/"))o="/"+o +m=o +return A.fw(a,r,p,q,m,l.f,l.r)}, +fo(a,b){var s,r,q,p,o,n,m +for(s=0,r=0;B.a.C(b,"../",r);){r+=3;++s}q=B.a.d5(a,"/") +for(;;){if(!(q>0&&s>0))break +p=B.a.hd(a,"/",q-1) +if(p<0)break +o=q-p +n=o!==2 +m=!1 +if(!n||o===3)if(a.charCodeAt(p+1)===46)n=!n||a.charCodeAt(p+2)===46 +else n=m +else n=m +if(n)break;--s +q=p}return B.a.aO(a,q+1,null,B.a.L(b,r-3*s))}, +hq(a){return this.cj(A.bu(a))}, +cj(a){var s,r,q,p,o,n,m,l,k,j,i,h=this +if(a.gW().length!==0)return a +else{s=h.a +if(a.geq()){r=a.ho(s) +return r}else{q=h.b +p=h.c +o=h.d +n=h.e +if(a.gh9())m=a.gd1()?a.gcg():h.f +else{l=A.vB(h,n) +if(l>0){k=B.a.p(n,0,l) +n=a.gep()?k+A.cT(a.ga9()):k+A.cT(h.fo(B.a.L(n,k.length),a.ga9()))}else if(a.gep())n=A.cT(a.ga9()) +else if(n.length===0)if(p==null)n=s.length===0?a.ga9():A.cT(a.ga9()) +else n=A.cT("/"+a.ga9()) +else{j=h.fo(n,a.ga9()) +r=s.length===0 +if(!r||p!=null||B.a.u(n,"/"))n=A.cT(j) +else n=A.oS(j,!r||p!=null)}m=a.gd1()?a.gcg():null}}}i=a.ger()?a.gd0():null +return A.fw(s,q,p,o,n,m,i)}, +geq(){return this.c!=null}, +gd1(){return this.f!=null}, +ger(){return this.r!=null}, +gh9(){return this.e.length===0}, +gep(){return B.a.u(this.e,"/")}, +eL(){var s,r=this,q=r.a +if(q!==""&&q!=="file")throw A.b(A.a4("Cannot extract a file path from a "+q+" URI")) +q=r.f +if((q==null?"":q)!=="")throw A.b(A.a4(u.y)) +q=r.r +if((q==null?"":q)!=="")throw A.b(A.a4(u.l)) +if(r.c!=null&&r.gb9()!=="")A.E(A.a4(u.j)) +s=r.gl1() +A.vt(s,!1) +q=A.ox(B.a.u(r.e,"/")?"/":"",s,"/") +q=q.charCodeAt(0)==0?q:q +return q}, +i(a){return this.gfM()}, +T(a,b){var s,r,q,p=this +if(b==null)return!1 +if(p===b)return!0 +s=!1 +if(t.dD.b(b))if(p.a===b.gW())if(p.c!=null===b.geq())if(p.b===b.geO())if(p.gb9()===b.gb9())if(p.gce()===b.gce())if(p.e===b.ga9()){r=p.f +q=r==null +if(!q===b.gd1()){if(q)r="" +if(r===b.gcg()){r=p.r +q=r==null +if(!q===b.ger()){s=q?"":r +s=s===b.gd0()}}}}return s}, +$ihX:1, +gW(){return this.a}, +ga9(){return this.e}} +A.nn.prototype={ +$1(a){return A.vC(64,a,B.j,!1)}, +$S:8} +A.hY.prototype={ +geN(){var s,r,q,p,o=this,n=null,m=o.c +if(m==null){m=o.a +s=o.b[0]+1 +r=B.a.aY(m,"?",s) +q=m.length +if(r>=0){p=A.fx(m,r+1,q,256,!1,!1) +q=r}else p=n +m=o.c=new A.ik("data","",n,n,A.fx(m,s,q,128,!1,!1),p,n)}return m}, +i(a){var s=this.a +return this.b[0]===-1?"data:"+s:s}} +A.b7.prototype={ +geq(){return this.c>0}, +ges(){return this.c>0&&this.d+1r?B.a.p(this.a,r,s-1):""}, +gb9(){var s=this.c +return s>0?B.a.p(this.a,s,this.d):""}, +gce(){var s,r=this +if(r.ges())return A.bj(B.a.p(r.a,r.d+1,r.e),null) +s=r.b +if(s===4&&B.a.u(r.a,"http"))return 80 +if(s===5&&B.a.u(r.a,"https"))return 443 +return 0}, +ga9(){return B.a.p(this.a,this.e,this.f)}, +gcg(){var s=this.f,r=this.r +return s=q.length)return s +return new A.b7(B.a.p(q,0,r),s.b,s.c,s.d,s.e,s.f,r,s.w)}, +ho(a){var s,r,q,p,o,n,m,l,k,j,i,h=this,g=null +a=A.np(a,0,a.length) +s=!(h.b===a.length&&B.a.u(h.a,a)) +r=a==="file" +q=h.c +p=q>0?B.a.p(h.a,h.b+3,q):"" +o=h.ges()?h.gce():g +if(s)o=A.no(o,a) +q=h.c +if(q>0)n=B.a.p(h.a,q,h.d) +else n=p.length!==0||o!=null||r?"":g +q=h.a +m=h.f +l=B.a.p(q,h.e,m) +if(!r)k=n!=null&&l.length!==0 +else k=!0 +if(k&&!B.a.u(l,"/"))l="/"+l +k=h.r +j=m0)return b +s=b.c +if(s>0){r=a.b +if(r<=0)return b +q=r===4 +if(q&&B.a.u(a.a,"file"))p=b.e!==b.f +else if(q&&B.a.u(a.a,"http"))p=!b.fl("80") +else p=!(r===5&&B.a.u(a.a,"https"))||!b.fl("443") +if(p){o=r+1 +return new A.b7(B.a.p(a.a,0,o)+B.a.L(b.a,c+1),r,s+o,b.d+o,b.e+o,b.f+o,b.r+o,a.w)}else return this.fO().cj(b)}n=b.e +c=b.f +if(n===c){s=b.r +if(c0?l:m +o=k-n +return new A.b7(B.a.p(a.a,0,k)+B.a.L(s,n),a.b,a.c,a.d,m,c+o,b.r+o,a.w)}j=a.e +i=a.f +if(j===i&&a.c>0){while(B.a.C(s,"../",n))n+=3 +o=j-n+1 +return new A.b7(B.a.p(a.a,0,j)+"/"+B.a.L(s,n),a.b,a.c,a.d,j,c+o,b.r+o,a.w)}h=a.a +l=A.qL(this) +if(l>=0)g=l +else for(g=j;B.a.C(h,"../",g);)g+=3 +f=0 +for(;;){e=n+3 +if(!(e<=c&&B.a.C(s,"../",n)))break;++f +n=e}for(d="";i>g;){--i +if(h.charCodeAt(i)===47){if(f===0){d="/" +break}--f +d="/"}}if(i===g&&a.b<=0&&!B.a.C(h,"/",j)){n-=f*3 +d=""}o=i-n+d.length +return new A.b7(B.a.p(h,0,i)+d+B.a.L(s,n),a.b,a.c,a.d,j,c+o,b.r+o,a.w)}, +eL(){var s,r=this,q=r.b +if(q>=0){s=!(q===4&&B.a.u(r.a,"file")) +q=s}else q=!1 +if(q)throw A.b(A.a4("Cannot extract a file path from a "+r.gW()+" URI")) +q=r.f +s=r.a +if(q0?s.gb9():r,n=s.ges()?s.gce():r,m=s.a,l=s.f,k=B.a.p(m,s.e,l),j=s.r +l=l4294967296)throw A.b(new A.dk(k,k,!1,k,k,"max must be in range 0 < max \u2264 2^32, was "+a)) +if(a>255)if(a>65535)s=a>16777215?4:3 +else s=2 +else s=1 +r=this.a +r.$flags&2&&A.y(r,11) +r.setUint32(0,0,!1) +q=4-s +p=A.B(Math.pow(256,s)) +for(o=a-1,n=(a&o)===0;;){crypto.getRandomValues(J.d0(B.aF.gaX(r),q,s)) +m=r.getUint32(0,!1) +if(n)return(m&o)>>>0 +l=m%a +if(m-l+a>>0)&2147483647 +r^=r>>>6}r=r+(r<<3>>>0)&2147483647 +r^=r>>>11 +return r+(r<<15>>>0)&2147483647}} +A.hB.prototype={} +A.hW.prototype={} +A.ej.prototype={ +hW(a,b,c){var s=this.a.a +s===$&&A.x() +s.eB(this.giH(),new A.jS(this))}, +hf(){return this.d++}, +n(){var s=0,r=A.k(t.H),q,p=this,o +var $async$n=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:if(p.r||(p.w.a.a&30)!==0){s=1 +break}p.r=!0 +o=p.a.b +o===$&&A.x() +o.n() +s=3 +return A.c(p.w.a,$async$n) +case 3:case 1:return A.i(q,r)}}) +return A.j($async$n,r)}, +iI(a){var s,r=this +if(r.c){a.toString +a=B.F.ej(a)}if(a instanceof A.bf){s=r.e.F(0,a.a) +if(s!=null)s.a.O(a.b)}else if(a instanceof A.bo){s=r.e.F(0,a.a) +if(s!=null)s.h_(new A.h5(a.b),a.c)}else if(a instanceof A.ar)r.f.v(0,a) +else if(a instanceof A.bw){s=r.e.F(0,a.a) +if(s!=null)s.fZ(B.E)}}, +bu(a){var s,r,q=this +if(q.r||(q.w.a.a&30)!==0)throw A.b(A.A("Tried to send "+a.i(0)+" over isolate channel, but the connection was closed!")) +s=q.a.b +s===$&&A.x() +r=q.c?B.F.dr(a):a +s.a.v(0,r)}, +l6(a,b,c){var s,r=this +if(r.r||(r.w.a.a&30)!==0)return +s=a.a +if(b instanceof A.ec)r.bu(new A.bw(s)) +else r.bu(new A.bo(s,b,c))}, +hI(a){var s=this.f +new A.at(s,A.r(s).h("at<1>")).kO(new A.jT(this,a))}} +A.jS.prototype={ +$0(){var s,r,q +for(s=this.a,r=s.e,q=new A.db(r,r.r,r.e);q.k();)q.d.fZ(B.af) +r.c3(0) +s.w.aJ()}, +$S:0} +A.jT.prototype={ +$1(a){return this.hy(a)}, +hy(a){var s=0,r=A.k(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h +var $async$$1=A.l(function(b,c){if(b===1){o.push(c) +s=p}for(;;)switch(s){case 0:i=null +p=4 +k=n.b.$1(a) +s=7 +return A.c(t.cG.b(k)?k:A.cL(k,t.O),$async$$1) +case 7:i=c +p=2 +s=6 +break +case 4:p=3 +h=o.pop() +m=A.H(h) +l=A.a5(h) +k=n.a.l6(a,m,l) +q=k +s=1 +break +s=6 +break +case 3:s=2 +break +case 6:k=n.a +if(!(k.r||(k.w.a.a&30)!==0))k.bu(new A.bf(a.a,i)) +case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$$1,r)}, +$S:42} +A.iC.prototype={ +h_(a,b){var s +if(b==null)s=this.b +else{s=A.f([],t.J) +if(b instanceof A.bm)B.c.aI(s,b.a) +else s.push(A.ql(b)) +s.push(A.ql(this.b)) +s=new A.bm(A.aO(s,t.a))}this.a.by(a,s)}, +fZ(a){return this.h_(a,null)}} +A.fX.prototype={ +i(a){return"Channel was closed before receiving a response"}, +$ia6:1} +A.h5.prototype={ +i(a){return J.b1(this.a)}, +$ia6:1} +A.h4.prototype={ +dr(a){var s,r +if(a instanceof A.ar)return[0,a.a,this.h3(a.b)] +else if(a instanceof A.bo){s=J.b1(a.b) +r=a.c +r=r==null?null:r.i(0) +return[2,a.a,s,r]}else if(a instanceof A.bf)return[1,a.a,this.h3(a.b)] +else if(a instanceof A.bw)return A.f([3,a.a],t.t) +else return null}, +ej(a){var s,r,q,p +if(!t.j.b(a))throw A.b(B.ar) +s=J.a3(a) +r=A.B(s.j(a,0)) +q=A.B(s.j(a,1)) +switch(r){case 0:return new A.ar(q,t.ah.a(this.h1(s.j(a,2)))) +case 2:p=A.r6(s.j(a,3)) +s=s.j(a,2) +if(s==null)s=A.oV(s) +return new A.bo(q,s,p!=null?new A.dQ(p):null) +case 1:return new A.bf(q,t.O.a(this.h1(s.j(a,2)))) +case 3:return new A.bw(q)}throw A.b(B.aq)}, +h3(a){var s,r,q,p,o,n,m,l,k,j,i,h,g,f +if(a==null)return a +if(a instanceof A.dh)return a.a +else if(a instanceof A.bW){s=a.a +r=a.b +q=[] +for(p=a.c,o=p.length,n=0;n")),g=g.h("Q.E");s.k();){a8=s.d +h.push(this.dI(a8==null?g.a(a8):a8))}m.push(new A.d2(i,h))}f=J.ob(a7.a) +A:{if(f==null){s=a6 +break A}A.B(f) +s=f +break A}return new A.bp(new A.ea(n,m),s) +case 5:return new A.c4(B.P[q.$1(1)],p.$1(2)) +case 6:return new A.bV(q.$1(1),p.$1(2)) +case 13:s.toString +return new A.c5(A.oe(B.N,A.a2(J.aM(s,1)))) +case 7:return new A.c3(new A.eC(p.$1(1),q.$1(2)),q.$1(3)) +case 8:e=A.f([],t.be) +s=t.j +k=1 +for(;;){l=a7.a +l.toString +if(!(k")).en(0,new A.kR(r))}, +kj(a,b){var s,r,q +for(s=this.z,s=A.iy(s,s.r,s.$ti.c),r=s.$ti.c;s.k();){q=s.d +if(q==null)q=r.a(q) +if(q!==b)q.bu(new A.ar(q.d++,a))}}} +A.kT.prototype={ +$1(a){var s=this.a +s.ic() +s.as.n()}, +$S:50} +A.kU.prototype={ +$1(a){return this.a.iK(this.b,a)}, +$S:51} +A.kV.prototype={ +$1(a){return this.a.z.F(0,this.b)}, +$S:25} +A.kP.prototype={ +$0(){var s=this.b +return this.a.aG(s.a,s.b,s.c,s.d)}, +$S:68} +A.kQ.prototype={ +$0(){return this.a.r.F(0,this.b.a)}, +$S:70} +A.kS.prototype={ +$0(){var s,r=this.b +if(r==null)return this.a.w.length===0 +else{s=this.a.w +return s.length!==0&&B.c.gE(s)===r}}, +$S:31} +A.kR.prototype={ +$1(a){return this.a.$0()}, +$S:25} +A.fj.prototype={ +cT(a,b){return this.jY(a,b)}, +jY(a,b){var s=0,r=A.k(t.H),q=1,p=[],o=[],n=this,m,l,k,j,i +var $async$cT=A.l(function(c,d){if(c===1){p.push(d) +s=q}for(;;)switch(s){case 0:j=n.a +i=j.e_(a,!0) +q=2 +m=n.b +l=m.hf() +k=new A.n($.m,t.D) +m.e.t(0,l,new A.iC(new A.a7(k,t.h),A.l8())) +m.bu(new A.ar(l,new A.c3(b,i))) +s=5 +return A.c(k,$async$cT) +case 5:o.push(4) +s=3 +break +case 2:o=[1] +case 3:q=1 +j.cI(i) +s=o.pop() +break +case 4:return A.i(null,r) +case 1:return A.h(p.at(-1),r)}}) +return A.j($async$cT,r)}} +A.i7.prototype={ +dr(a1){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a=this,a0=null +A:{if(a1 instanceof A.ar){s=new A.ag(0,{i:a1.a,p:a.jn(a1.b)}) +break A}if(a1 instanceof A.bf){s=new A.ag(1,{i:a1.a,p:a.jo(a1.b)}) +break A}r=a1 instanceof A.bo +q=a0 +p=a0 +o=!1 +n=a0 +m=a0 +s=!1 +if(r){l=a1.a +q=a1.b +o=q instanceof A.c8 +if(o){t.f_.a(q) +p=a1.c +s=a.a.c>=4 +m=p +n=q}k=l}else{k=a0 +l=k}if(s){s=m==null?a0:m.i(0) +j=n.a +i=n.b +if(i==null)i=a0 +h=n.c +g=n.e +if(g==null)g=a0 +f=n.f +if(f==null)f=a0 +e=n.r +B:{if(e==null){d=a0 +break B}d=[] +for(c=e.length,b=0;b") +s=A.ak(new A.D(s,new A.lV(),q),q.h("Q.E")) +s=new A.bF(s) +break A}s=A.E(A.J("Unknown request tag "+r,m))}return s}, +jo(a){var s,r +A:{s=null +if(a==null)break A +if(a instanceof A.aQ){r=a.a +s=A.bQ(r)?r:A.B(r) +break A}if(a instanceof A.bI){s=this.jp(a) +break A}}return s}, +jp(a){var s,r,q,p=a.a,o=J.a3(p) +if(o.gB(p)){p=v.G +return{c:new p.Array(),r:new p.Array()}}else{s=J.d1(o.gE(p).gX(),new A.m0(),t.N).cn(0) +r=A.f([],t.fk) +for(p=o.gq(p);p.k();){q=[] +for(o=J.Z(p.gm().gbH());o.k();)q.push(this.cL(o.gm())) +r.push(q)}return{c:s,r:r}}}, +iu(a){var s,r,q,p,o,n,m,l,k,j +if(a==null)return null +else if(typeof a==="boolean")return new A.aQ(A.bh(a)) +else if(typeof a==="number")return new A.aQ(A.B(A.Y(a))) +else{A.a9(a) +s=a.c +s=t.q.b(s)?s:new A.ai(s,A.O(s).h("ai<1,p>")) +r=t.N +s=J.d1(s,new A.lZ(),r) +q=A.ak(s,s.$ti.h("Q.E")) +p=A.f([],t.d) +s=a.r +s=J.Z(t.e9.b(s)?s:new A.ai(s,A.O(s).h("ai<1,u>"))) +o=t.X +while(s.k()){n=s.gm() +m=A.ao(r,o) +n=A.uc(n,0,o) +l=J.Z(n.a) +n=n.b +k=new A.eq(l,n) +while(k.k()){j=k.c +j=j>=0?new A.ag(n+j,l.gm()):A.E(A.aw()) +m.t(0,q[j.a],this.cK(j.b))}p.push(m)}return new A.bI(p)}}, +cL(a){var s +A:{if(a==null){s=null +break A}if(A.bv(a)){s=a +break A}if(A.bQ(a)){s=a +break A}if(typeof a=="string"){s=a +break A}if(typeof a=="number"){s=A.f([15,a],t.n) +break A}if(a instanceof A.a8){s=A.f([14,a.i(0)],t.f) +break A}if(t.I.b(a)){s=new Uint8Array(A.fC(a)) +break A}s=A.E(A.J("Unknown db value: "+A.t(a),null))}return s}, +cK(a){var s,r,q,p=null +if(a!=null)if(typeof a==="number")return A.B(A.Y(a)) +else if(typeof a==="boolean")return A.bh(a) +else if(typeof a==="string")return A.a2(a) +else if(A.kq(a,"Uint8Array"))return t.Z.a(a) +else{t.c.a(a) +s=a.length===2 +if(s){r=a[0] +q=a[1]}else{q=p +r=q}if(!s)throw A.b(A.A("Pattern matching error")) +if(r==14)return A.oK(A.a2(q),p) +else return A.Y(q)}else return p}, +fa(a){var s,r=a!=null?A.a2(a):null +A:{if(r!=null){s=new A.dQ(r) +break A}s=null +break A}return s}, +iq(a){var s,r,q,p,o=null,n=a.length>=8,m=o,l=o,k=o,j=o,i=o,h=o,g=o +if(n){s=a[0] +m=a[1] +l=a[2] +k=a[3] +j=a[4] +i=a[5] +h=a[6] +g=a[7]}else s=o +if(!n)throw A.b(A.A("Pattern matching error")) +s=A.B(A.Y(s)) +j=A.B(A.Y(j)) +A.a2(l) +n=k!=null?A.a2(k):o +r=h!=null?A.a2(h):o +if(g!=null){q=[] +t.c.a(g) +p=B.c.gq(g) +while(p.k())q.push(this.cK(p.gm()))}else q=o +p=i!=null?A.a2(i):o +return new A.bo(s,new A.c8(l,n,j,o,p,r,q),this.fa(m))}} +A.m1.prototype={ +$0(){var s=A.a9(this.a.a) +return new A.ar(s.i,this.b.it(s.p))}, +$S:72} +A.m2.prototype={ +$0(){var s=A.a9(this.a.a) +return new A.bf(s.i,this.b.iu(s.p))}, +$S:79} +A.m_.prototype={ +$1(a){return a}, +$S:8} +A.lW.prototype={ +$0(){var s,r,q,p,o,n,m=this.b,l=J.a3(m),k=t.c,j=k.a(l.j(m,1)),i=t.q.b(j)?j:new A.ai(j,A.O(j).h("ai<1,p>")) +i=J.d1(i,new A.lX(),t.N) +s=A.ak(i,i.$ti.h("Q.E")) +i=l.gl(m) +r=A.f([],t.b) +for(i=l.U(m,2).ah(0,i-3),k=A.ed(i,i.$ti.h("d.E"),k),k=A.ht(k,new A.lY(),A.r(k).h("d.E"),t.ee),i=k.a,q=A.r(k),k=new A.dc(i.gq(i),k.b,q.h("dc<1,2>")),i=this.a.gjF(),q=q.y[1];k.k();){p=k.a +if(p==null)p=q.a(p) +o=J.a3(p) +n=A.B(A.Y(o.j(p,0))) +p=o.U(p,1) +o=p.$ti.h("D") +p=A.ak(new A.D(p,i,o),o.h("Q.E")) +r.push(new A.d2(n,p))}m=l.j(m,l.gl(m)-1) +m=m==null?null:A.B(A.Y(m)) +return new A.bp(new A.ea(s,r),m)}, +$S:84} +A.lX.prototype={ +$1(a){return a}, +$S:8} +A.lY.prototype={ +$1(a){return a}, +$S:91} +A.lV.prototype={ +$1(a){var s,r,q +t.c.a(a) +s=a.length===2 +if(s){r=a[0] +q=a[1]}else{r=null +q=null}if(!s)throw A.b(A.A("Pattern matching error")) +A.a2(r) +return new A.bK(q==null?null:B.M[A.B(A.Y(q))],r)}, +$S:95} +A.m0.prototype={ +$1(a){return a}, +$S:8} +A.lZ.prototype={ +$1(a){return a}, +$S:8} +A.dv.prototype={ +ad(){return"UpdateKind."+this.b}} +A.bK.prototype={ +gA(a){return A.eB(this.a,this.b,B.f,B.f)}, +T(a,b){if(b==null)return!1 +return b instanceof A.bK&&b.a==this.a&&b.b===this.b}, +i(a){return"TableUpdate("+this.b+", kind: "+A.t(this.a)+")"}} +A.o2.prototype={ +$0(){return this.a.a.a.O(A.oi(this.b,this.c))}, +$S:0} +A.bU.prototype={ +J(){var s,r +if(this.c)return +for(s=this.b,r=0;!1;++r)s[r].$0() +this.c=!0}} +A.ec.prototype={ +i(a){return"Operation was cancelled"}, +$ia6:1} +A.aq.prototype={ +n(){var s=0,r=A.k(t.H) +var $async$n=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:return A.i(null,r)}}) +return A.j($async$n,r)}} +A.ea.prototype={ +gA(a){return A.eB(B.m.ha(this.a),B.m.ha(this.b),B.f,B.f)}, +T(a,b){if(b==null)return!1 +return b instanceof A.ea&&B.m.el(b.a,this.a)&&B.m.el(b.b,this.b)}, +i(a){return"BatchedStatements("+A.t(this.a)+", "+A.t(this.b)+")"}} +A.d2.prototype={ +gA(a){return A.eB(this.a,B.m,B.f,B.f)}, +T(a,b){if(b==null)return!1 +return b instanceof A.d2&&b.a===this.a&&B.m.el(b.b,this.b)}, +i(a){return"ArgumentsForBatchedStatement("+this.a+", "+A.t(this.b)+")"}} +A.jJ.prototype={} +A.kG.prototype={} +A.ls.prototype={} +A.kB.prototype={} +A.jM.prototype={} +A.hA.prototype={} +A.k0.prototype={} +A.id.prototype={ +gez(){return!1}, +gc9(){return!1}, +fK(a,b,c){if(this.gez()||this.b>0)return this.a.cz(new A.ma(b,a,c),c) +else return a.$0()}, +bv(a,b){return this.fK(a,!0,b)}, +cF(a,b){this.gc9()}, +aa(a,b){return this.lg(a,b)}, +lg(a,b){var s=0,r=A.k(t.aS),q,p=this,o +var $async$aa=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.bv(new A.mf(p,a,b),t.aj),$async$aa) +case 3:o=d.gjX(0) +o=A.ak(o,o.$ti.h("Q.E")) +q=o +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$aa,r)}, +ck(a,b){return this.bv(new A.md(this,a,b),t.S)}, +aB(a,b){return this.bv(new A.me(this,a,b),t.S)}, +a6(a,b){return this.bv(new A.mc(this,b,a),t.H)}, +lc(a){return this.a6(a,null)}, +aA(a){return this.bv(new A.mb(this,a),t.H)}, +cU(){return new A.f4(this,new A.a7(new A.n($.m,t.D),t.h),new A.bq())}, +cV(){return this.aW(this)}} +A.ma.prototype={ +$0(){return this.hC(this.c)}, +hC(a){var s=0,r=A.k(a),q,p=this +var $async$$0=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:if(p.a)A.p2() +s=3 +return A.c(p.b.$0(),$async$$0) +case 3:q=c +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$$0,r)}, +$S(){return this.c.h("C<0>()")}} +A.mf.prototype={ +$0(){var s=this.a,r=this.b,q=this.c +s.cF(r,q) +return s.gaL().aa(r,q)}, +$S:97} +A.md.prototype={ +$0(){var s=this.a,r=this.b,q=this.c +s.cF(r,q) +return s.gaL().de(r,q)}, +$S:26} +A.me.prototype={ +$0(){var s=this.a,r=this.b,q=this.c +s.cF(r,q) +return s.gaL().aB(r,q)}, +$S:26} +A.mc.prototype={ +$0(){var s,r,q=this.b +if(q==null)q=B.o +s=this.a +r=this.c +s.cF(r,q) +return s.gaL().a6(r,q)}, +$S:10} +A.mb.prototype={ +$0(){var s=this.a +s.gc9() +return s.gaL().aA(this.b)}, +$S:10} +A.iQ.prototype={ +ib(){this.c=!0 +if(this.d)throw A.b(A.A("A transaction was used after being closed. Please check that you're awaiting all database operations inside a `transaction` block."))}, +aW(a){throw A.b(A.a4("Nested transactions aren't supported."))}, +gap(){return B.l}, +gc9(){return!1}, +gez(){return!0}, +$ihS:1} +A.fn.prototype={ +aq(a){var s,r,q=this +q.ib() +s=q.z +if(s==null){s=q.z=new A.a7(new A.n($.m,t.k),t.co) +r=q.as;++r.b +r.fK(new A.n9(q),!1,t.P).ai(new A.na(r))}return s.a}, +gaL(){return this.e.e}, +aW(a){var s=this.at+1 +return new A.fn(this.y,new A.a7(new A.n($.m,t.D),t.h),a,s,A.rb(s),A.r9(s),A.ra(s),this.e,new A.bq())}, +bj(){var s=0,r=A.k(t.H),q,p=this +var $async$bj=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:if(!p.c){s=1 +break}s=3 +return A.c(p.a6(p.ay,B.o),$async$bj) +case 3:p.e2() +case 1:return A.i(q,r)}}) +return A.j($async$bj,r)}, +bE(){var s=0,r=A.k(t.H),q,p=2,o=[],n=[],m=this +var $async$bE=A.l(function(a,b){if(a===1){o.push(b) +s=p}for(;;)switch(s){case 0:if(!m.c){s=1 +break}p=3 +s=6 +return A.c(m.a6(m.ch,B.o),$async$bE) +case 6:n.push(5) +s=4 +break +case 3:n=[2] +case 4:p=2 +m.e2() +s=n.pop() +break +case 5:case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$bE,r)}, +e2(){var s=this +if(s.at===0)s.e.e.a=!1 +s.Q.aJ() +s.d=!0}} +A.n9.prototype={ +$0(){var s=0,r=A.k(t.P),q=1,p=[],o=this,n,m,l,k,j +var $async$$0=A.l(function(a,b){if(a===1){p.push(b) +s=q}for(;;)switch(s){case 0:q=3 +A.p2() +l=o.a +s=6 +return A.c(l.lc(l.ax),$async$$0) +case 6:l.e.e.a=!0 +l.z.O(!0) +q=1 +s=5 +break +case 3:q=2 +j=p.pop() +n=A.H(j) +m=A.a5(j) +l=o.a +l.z.by(n,m) +l.e2() +s=5 +break +case 2:s=1 +break +case 5:s=7 +return A.c(o.a.Q.a,$async$$0) +case 7:return A.i(null,r) +case 1:return A.h(p.at(-1),r)}}) +return A.j($async$$0,r)}, +$S:17} +A.na.prototype={ +$0(){return this.a.b--}, +$S:63} +A.h2.prototype={ +gaL(){return this.e}, +gap(){return B.l}, +aq(a){return this.x.cz(new A.jR(this,a),t.y)}, +br(a){return this.ji(a)}, +ji(a){var s=0,r=A.k(t.H),q=this,p,o,n,m +var $async$br=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:n=q.e +m=n.y +m===$&&A.x() +p=a.c +s=m instanceof A.hA?2:4 +break +case 2:o=p +s=3 +break +case 4:s=m instanceof A.fl?5:7 +break +case 5:s=8 +return A.c(A.b3(m.a.glm(),t.S),$async$br) +case 8:o=c +s=6 +break +case 7:throw A.b(A.k2("Invalid delegate: "+n.i(0)+". The versionDelegate getter must not subclass DBVersionDelegate directly")) +case 6:case 3:if(o===0)o=null +s=9 +return A.c(a.cT(new A.ie(q,new A.bq()),new A.eC(o,p)),$async$br) +case 9:s=m instanceof A.fl&&o!==p?10:11 +break +case 10:m.a.h5("PRAGMA user_version = "+p+";") +s=12 +return A.c(A.b3(null,t.H),$async$br) +case 12:case 11:return A.i(null,r)}}) +return A.j($async$br,r)}, +aW(a){var s=$.m +return new A.fn(B.an,new A.a7(new A.n(s,t.D),t.h),a,0,"BEGIN IMMEDIATE","COMMIT TRANSACTION","ROLLBACK TRANSACTION",this,new A.bq())}, +n(){return this.x.cz(new A.jQ(this),t.H)}, +gc9(){return this.r}, +gez(){return this.w}} +A.jR.prototype={ +$0(){var s=0,r=A.k(t.y),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e +var $async$$0=A.l(function(a,b){if(a===1){o.push(b) +s=p}for(;;)switch(s){case 0:f=n.a +if(f.d){f=A.nB(new A.aI("Can't re-open a database after closing it. Please create a new database connection and open that instead."),null) +k=new A.n($.m,t.k) +k.aR(f) +q=k +s=1 +break}j=f.f +if(j!=null)A.pH(j.a,j.b) +k=f.e +i=t.y +h=A.b3(k.d,i) +s=3 +return A.c(t.bF.b(h)?h:A.cL(h,i),$async$$0) +case 3:if(b){q=f.c=!0 +s=1 +break}i=n.b +s=4 +return A.c(k.bB(i),$async$$0) +case 4:f.c=!0 +p=6 +s=9 +return A.c(f.br(i),$async$$0) +case 9:q=!0 +s=1 +break +p=2 +s=8 +break +case 6:p=5 +e=o.pop() +m=A.H(e) +l=A.a5(e) +f.f=new A.ag(m,l) +throw e +s=8 +break +case 5:s=2 +break +case 8:case 1:return A.i(q,r) +case 2:return A.h(o.at(-1),r)}}) +return A.j($async$$0,r)}, +$S:43} +A.jQ.prototype={ +$0(){var s=this.a +if(s.c&&!s.d){s.d=!0 +s.c=!1 +return s.e.n()}else return A.b3(null,t.H)}, +$S:10} +A.ie.prototype={ +aW(a){return this.e.aW(a)}, +aq(a){this.c=!0 +return A.b3(!0,t.y)}, +gaL(){return this.e.e}, +gc9(){return!1}, +gap(){return B.l}} +A.f4.prototype={ +gap(){return this.e.gap()}, +aq(a){var s,r,q,p=this,o=p.f +if(o!=null)return o.a +else{p.c=!0 +s=new A.n($.m,t.k) +r=new A.a7(s,t.co) +p.f=r +q=p.e;++q.b +q.bv(new A.my(p,r),t.P) +return s}}, +gaL(){return this.e.gaL()}, +aW(a){return this.e.aW(a)}, +n(){this.r.aJ() +return A.b3(null,t.H)}} +A.my.prototype={ +$0(){var s=0,r=A.k(t.P),q=this,p +var $async$$0=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:q.b.O(!0) +p=q.a +s=2 +return A.c(p.r.a,$async$$0) +case 2:--p.e.b +return A.i(null,r)}}) +return A.j($async$$0,r)}, +$S:17} +A.dj.prototype={ +gjX(a){var s=this.b +return new A.D(s,new A.kI(this),A.O(s).h("D<1,ap>"))}} +A.kI.prototype={ +$1(a){var s,r,q,p,o,n,m,l=A.ao(t.N,t.z) +for(s=this.a,r=s.a,q=r.length,s=s.c,p=J.a3(a),o=0;o")).gE(0)).n() +q.t(0,a,s)}return new A.ag(s,o.sqlite3_stmt_isexplain(r)===0)}} +A.fl.prototype={} +A.kF.prototype={ +kA(){var s,r,q,p +for(s=this.b,r=new A.db(s,s.r,s.e);r.k();){q=r.d +if(!q.r){q.r=!0 +if(!q.f){p=q.a +p.c.d.sqlite3_reset(p.b) +q.f=!0}q=q.a +p=q.c +p.d.sqlite3_finalize(q.b) +p=p.w +if(p!=null){p=p.a +if(p!=null)p.unregister(q.d)}}}s.c3(0)}} +A.k1.prototype={ +$1(a){return Date.now()}, +$S:45} +A.nH.prototype={ +$1(a){var s=a.j(0,0) +if(typeof s=="number")return this.a.$1(s) +else return null}, +$S:28} +A.ho.prototype={ +gis(){var s=this.a +s===$&&A.x() +return s}, +gap(){if(this.b){var s=this.a +s===$&&A.x() +s=B.l!==s.gap()}else s=!1 +if(s)throw A.b(A.k2("LazyDatabase created with "+B.l.i(0)+", but underlying database is "+this.gis().gap().i(0)+".")) +return B.l}, +i6(){var s,r,q=this +if(q.b)return A.b3(null,t.H) +else{s=q.d +if(s!=null)return s.a +else{s=new A.n($.m,t.D) +r=q.d=new A.a7(s,t.h) +A.oi(q.e,t.x).bf(new A.kt(q,r),r.gk6(),t.P) +return s}}}, +cU(){var s=this.a +s===$&&A.x() +return s.cU()}, +cV(){var s=this.a +s===$&&A.x() +return s.cV()}, +aq(a){return this.i6().bG(new A.ku(this,a),t.y)}, +aA(a){var s=this.a +s===$&&A.x() +return s.aA(a)}, +a6(a,b){var s=this.a +s===$&&A.x() +return s.a6(a,b)}, +ck(a,b){var s=this.a +s===$&&A.x() +return s.ck(a,b)}, +aB(a,b){var s=this.a +s===$&&A.x() +return s.aB(a,b)}, +aa(a,b){var s=this.a +s===$&&A.x() +return s.aa(a,b)}, +n(){var s=0,r=A.k(t.H),q,p=this,o,n +var $async$n=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:s=p.b?3:5 +break +case 3:o=p.a +o===$&&A.x() +s=6 +return A.c(o.n(),$async$n) +case 6:q=b +s=1 +break +s=4 +break +case 5:n=p.d +s=n!=null?7:8 +break +case 7:s=9 +return A.c(n.a,$async$n) +case 9:o=p.a +o===$&&A.x() +s=10 +return A.c(o.n(),$async$n) +case 10:case 8:case 4:case 1:return A.i(q,r)}}) +return A.j($async$n,r)}} +A.kt.prototype={ +$1(a){var s=this.a +s.a!==$&&A.iZ() +s.a=a +s.b=!0 +this.b.aJ()}, +$S:47} +A.ku.prototype={ +$1(a){var s=this.a.a +s===$&&A.x() +return s.aq(this.b)}, +$S:48} +A.bq.prototype={ +cz(a,b){var s,r=this.a,q=new A.n($.m,t.D) +this.a=q +s=new A.kw(this,a,new A.a7(q,t.h),q,b) +if(r!=null)return r.bG(new A.ky(s,b),b) +else return s.$0()}} +A.kw.prototype={ +$0(){var s=this +return A.oi(s.b,s.e).ai(new A.kx(s.a,s.c,s.d))}, +$S(){return this.e.h("C<0>()")}} +A.kx.prototype={ +$0(){this.b.aJ() +var s=this.a +if(s.a===this.c)s.a=null}, +$S:3} +A.ky.prototype={ +$1(a){return this.a.$0()}, +$S(){return this.b.h("C<0>(~)")}} +A.lS.prototype={ +$1(a){var s,r=this,q=a.data +if(r.a&&J.am(q,"_disconnect")){s=r.b.a +s===$&&A.x() +s=s.a +s===$&&A.x() +s.n()}else{s=r.b.a +if(r.c){s===$&&A.x() +s=s.a +s===$&&A.x() +s.v(0,r.d.ej(t.c.a(q)))}else{s===$&&A.x() +s=s.a +s===$&&A.x() +s.v(0,A.ry(q))}}}, +$S:11} +A.lT.prototype={ +$1(a){var s=this.c +if(this.a)s.postMessage(this.b.dr(t.fJ.a(a))) +else s.postMessage(A.xp(a))}, +$S:7} +A.lU.prototype={ +$0(){if(this.a)this.b.postMessage("_disconnect") +this.b.close()}, +$S:0} +A.jN.prototype={ +R(){A.aL(this.a,"message",new A.jP(this),!1)}, +ak(a){return this.iJ(a)}, +iJ(a6){var s=0,r=A.k(t.H),q=1,p=[],o=this,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5 +var $async$ak=A.l(function(a7,a8){if(a7===1){p.push(a8) +s=q}for(;;)switch(s){case 0:k=a6 instanceof A.dl +j=k?a6.a:null +s=k?3:4 +break +case 3:i={} +i.a=i.b=!1 +s=5 +return A.c(o.b.cz(new A.jO(i,o),t.P),$async$ak) +case 5:h=o.c.a.j(0,j) +g=A.f([],t.L) +f=!1 +s=i.b?6:7 +break +case 6:a5=J +s=8 +return A.c(A.e6(),$async$ak) +case 8:k=a5.Z(a8) +case 9:if(!k.k()){s=10 +break}e=k.gm() +g.push(new A.ag(B.C,e)) +if(e===j)f=!0 +s=9 +break +case 10:case 7:s=h!=null?11:13 +break +case 11:k=h.a +d=k===B.r||k===B.B +f=k===B.W||k===B.X +s=12 +break +case 13:a5=i.a +if(a5){s=14 +break}else a8=a5 +s=15 +break +case 14:s=16 +return A.c(A.e4(j),$async$ak) +case 16:case 15:d=a8 +case 12:k=v.G +c="Worker" in k +e=i.b +b=i.a +new A.ei(c,e,"SharedArrayBuffer" in k,b,g,B.q,d,f).dn(o.a) +s=2 +break +case 4:if(a6 instanceof A.dn){o.c.eS(a6) +s=2 +break}k=a6 instanceof A.eL +a=k?a6.a:null +s=k?17:18 +break +case 17:s=19 +return A.c(A.i3(a),$async$ak) +case 19:a0=a8 +o.a.postMessage(!0) +s=20 +return A.c(a0.R(),$async$ak) +case 20:s=2 +break +case 18:n=null +m=null +a1=a6 instanceof A.h3 +if(a1){a2=a6.a +n=a2.a +m=a2.b}s=a1?21:22 +break +case 21:q=24 +case 27:switch(n){case B.Y:s=29 +break +case B.C:s=30 +break +default:s=28 +break}break +case 29:s=31 +return A.c(A.nN(m),$async$ak) +case 31:s=28 +break +case 30:s=32 +return A.c(A.fG(m),$async$ak) +case 32:s=28 +break +case 28:a6.dn(o.a) +q=1 +s=26 +break +case 24:q=23 +a4=p.pop() +l=A.H(a4) +new A.dz(J.b1(l)).dn(o.a) +s=26 +break +case 23:s=1 +break +case 26:s=2 +break +case 22:s=2 +break +case 2:return A.i(null,r) +case 1:return A.h(p.at(-1),r)}}) +return A.j($async$ak,r)}} +A.jP.prototype={ +$1(a){this.a.ak(A.oB(A.a9(a.data)))}, +$S:1} +A.jO.prototype={ +$0(){var s=0,r=A.k(t.P),q=this,p,o,n,m,l +var $async$$0=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:o=q.b +n=o.d +m=q.a +s=n!=null?2:4 +break +case 2:m.b=n.b +m.a=n.a +s=3 +break +case 4:l=m +s=5 +return A.c(A.cW(),$async$$0) +case 5:l.b=b +s=6 +return A.c(A.iW(),$async$$0) +case 6:p=b +m.a=p +o.d=new A.lF(p,m.b) +case 3:return A.i(null,r)}}) +return A.j($async$$0,r)}, +$S:17} +A.cA.prototype={ +ad(){return"ProtocolVersion."+this.b}} +A.lH.prototype={ +dq(a){this.aD(new A.lK(a))}, +eR(a){this.aD(new A.lJ(a))}, +dn(a){this.aD(new A.lI(a))}} +A.lK.prototype={ +$2(a,b){var s=b==null?B.w:b +this.a.postMessage(a,s)}, +$S:19} +A.lJ.prototype={ +$2(a,b){var s=b==null?B.w:b +this.a.postMessage(a,s)}, +$S:19} +A.lI.prototype={ +$2(a,b){var s=b==null?B.w:b +this.a.postMessage(a,s)}, +$S:19} +A.ji.prototype={} +A.c6.prototype={ +aD(a){var s=this +A.dW(a,"SharedWorkerCompatibilityResult",A.f([s.e,s.f,s.r,s.c,s.d,A.pF(s.a),s.b.c],t.f),null)}} +A.l1.prototype={ +$1(a){return A.bh(J.aM(this.a,a))}, +$S:52} +A.dz.prototype={ +aD(a){A.dW(a,"Error",this.a,null)}, +i(a){return"Error in worker: "+this.a}, +$ia6:1} +A.dn.prototype={ +aD(a){var s,r,q=this,p={} +p.sqlite=q.a.i(0) +s=q.b +p.port=s +p.storage=q.c.b +p.database=q.d +r=q.e +p.initPort=r +p.migrations=q.r +p.new_serialization=q.w +p.v=q.f.c +s=A.f([s],t.W) +if(r!=null)s.push(r) +A.dW(a,"ServeDriftDatabase",p,s)}} +A.dl.prototype={ +aD(a){A.dW(a,"RequestCompatibilityCheck",this.a,null)}} +A.ei.prototype={ +aD(a){var s=this,r={} +r.supportsNestedWorkers=s.e +r.canAccessOpfs=s.f +r.supportsIndexedDb=s.w +r.supportsSharedArrayBuffers=s.r +r.indexedDbExists=s.c +r.opfsExists=s.d +r.existing=A.pF(s.a) +r.v=s.b.c +A.dW(a,"DedicatedWorkerCompatibilityResult",r,null)}} +A.eL.prototype={ +aD(a){A.dW(a,"StartFileSystemServer",this.a,null)}} +A.h3.prototype={ +aD(a){var s=this.a +A.dW(a,"DeleteDatabase",A.f([s.a.b,s.b],t.s),null)}} +A.nK.prototype={ +$1(a){this.b.transaction.abort() +this.a.a=!1}, +$S:11} +A.nZ.prototype={ +$1(a){return A.a9(a[1])}, +$S:53} +A.h6.prototype={ +eS(a){var s=a.f.c,r=a.w +this.a.hk(a.d,new A.k_(this,a)).hG(A.uV(a.b,s>=1,s,r),!r)}, +aN(a,b,c,d,e){return this.kZ(a,b,c,d,e)}, +kZ(a,b,c,d,e){var s=0,r=A.k(t.x),q,p=this,o,n,m,l,k,j,i,h,g +var $async$aN=A.l(function(f,a0){if(f===1)return A.h(a0,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.lO(d.i(0)),$async$aN) +case 3:i=a0 +h=null +g=null +case 4:switch(e.a){case 0:s=6 +break +case 1:s=7 +break +case 3:s=8 +break +case 2:s=9 +break +case 4:s=10 +break +default:s=11 +break}break +case 6:s=12 +return A.c(A.l3("drift_db/"+a),$async$aN) +case 12:o=a0 +g=o.gc4() +s=5 +break +case 7:s=13 +return A.c(p.cE(a),$async$aN) +case 13:o=a0 +g=o.gc4() +s=5 +break +case 8:case 9:s=14 +return A.c(A.hg(a,!1),$async$aN) +case 14:o=a0 +g=o.gc4() +h=o +s=5 +break +case 10:o=A.ok(null) +s=5 +break +case 11:o=null +case 5:s=c!=null&&o.co("/database",0)===0?15:16 +break +case 15:n=c.$0() +s=17 +return A.c(t.eY.b(n)?n:A.cL(n,t.aD),$async$aN) +case 17:m=a0 +if(m!=null){l=o.b0(new A.eJ("/database"),4).a +l.bi(m,0) +l.cp()}n=h==null?null:h.aU(!1) +s=18 +return A.c(n instanceof A.n?n:A.cL(n,t.H),$async$aN) +case 18:case 16:i.hb() +n=i.a +n=n.a +k=n.d.dart_sqlite3_register_vfs(n.c1(B.i.a4(o.a),1),o,1) +if(k===0)A.E(A.A("could not register vfs")) +n=$.t6() +n.a.set(o,k) +n=A.uj(t.N,t.eT) +j=new A.i4(new A.iT(i,"/database",h,p.b,!0,b,new A.kF(n)),!1,!0,new A.bq(),new A.bq()) +if(g!=null){q=A.tM(j,new A.mn(g,j)) +s=1 +break}else{q=j +s=1 +break}case 1:return A.i(q,r)}}) +return A.j($async$aN,r)}, +cE(a){return this.iO(a)}, +iO(a){var s=0,r=A.k(t.aT),q,p,o,n,m,l,k,j +var $async$cE=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:l=v.G +k=new l.SharedArrayBuffer(8) +j=l.Int32Array +j=t.ha.a(A.e3(j,[k])) +l.Atomics.store(j,0,-1) +j={clientVersion:1,root:"drift_db/"+a,synchronizationBuffer:k,communicationBuffer:new l.SharedArrayBuffer(67584)} +p=new l.Worker(A.i_().i(0)) +new A.eL(j).dq(p) +s=3 +return A.c(new A.f3(p,"message",!1,t.fF).gE(0),$async$cE) +case 3:o=A.qb(j.synchronizationBuffer) +j=j.communicationBuffer +n=A.qd(j,65536,2048) +l=l.Uint8Array +l=t.Z.a(A.e3(l,[j])) +m=$.fI() +q=new A.dy(o,new A.br(j,n,l),m,"dart-sqlite3-vfs") +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$cE,r)}} +A.k_.prototype={ +$0(){var s=this.b,r=s.e,q=r!=null?new A.jX(r):null,p=this.a,o=A.uC(new A.ho(new A.jY(p,s,q)),!1,!0),n=new A.n($.m,t.D),m=new A.dm(s.c,o,new A.a1(n,t.F)) +n.ai(new A.jZ(p,s,m)) +return m}, +$S:54} +A.jX.prototype={ +$0(){var s=new A.n($.m,t.fX),r=this.a +r.postMessage(!0) +r.onmessage=A.bi(new A.jW(new A.a7(s,t.fu))) +return s}, +$S:55} +A.jW.prototype={ +$1(a){var s=t.dE.a(a.data),r=s==null?null:s +this.a.O(r)}, +$S:11} +A.jY.prototype={ +$0(){var s=this.b +return this.a.aN(s.d,s.r,this.c,s.a,s.c)}, +$S:56} +A.jZ.prototype={ +$0(){this.a.a.F(0,this.b.d) +this.c.b.hJ()}, +$S:3} +A.mn.prototype={ +c5(a){return this.k0(a)}, +k0(a){var s=0,r=A.k(t.H),q=this,p +var $async$c5=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:s=2 +return A.c(a.n(),$async$c5) +case 2:s=q.b===a?3:4 +break +case 3:p=q.a.$0() +s=5 +return A.c(p instanceof A.n?p:A.cL(p,t.H),$async$c5) +case 5:case 4:return A.i(null,r)}}) +return A.j($async$c5,r)}} +A.dm.prototype={ +hG(a,b){var s,r,q;++this.c +s=t.X +s=A.vg(new A.kM(this),s,s).gjZ().$1(a.ghP()) +r=a.$ti +q=new A.ee(r.h("ee<1>")) +q.b=new A.eX(q,a.ghK()) +q.a=new A.eY(s,q,r.h("eY<1>")) +this.b.hH(q,b)}} +A.kM.prototype={ +$1(a){var s=this.a +if(--s.c===0)s.d.aJ() +a.a.bo()}, +$S:39} +A.lF.prototype={} +A.jm.prototype={ +$1(a){this.a.O(this.c.a(this.b.result))}, +$S:1} +A.jn.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ag(s)}, +$S:1} +A.jo.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ag(s)}, +$S:1} +A.kW.prototype={ +R(){A.aL(this.a,"connect",new A.l0(this),!1)}, +dX(a){return this.iS(a)}, +iS(a){var s=0,r=A.k(t.H),q=this,p,o +var $async$dX=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:p=a.ports +o=J.aM(t.cl.b(p)?p:new A.ai(p,A.O(p).h("ai<1,z>")),0) +o.start() +A.aL(o,"message",new A.kX(q,o),!1) +return A.i(null,r)}}) +return A.j($async$dX,r)}, +cG(a,b){return this.iP(a,b)}, +iP(a,b){var s=0,r=A.k(t.H),q=1,p=[],o=this,n,m,l,k,j,i,h,g +var $async$cG=A.l(function(c,d){if(c===1){p.push(d) +s=q}for(;;)switch(s){case 0:q=3 +n=A.oB(A.a9(b.data)) +m=n +l=null +i=m instanceof A.dl +if(i)l=m.a +s=i?7:8 +break +case 7:s=9 +return A.c(o.bX(l),$async$cG) +case 9:k=d +k.eR(a) +s=6 +break +case 8:if(m instanceof A.dn&&B.r===m.c){o.c.eS(n) +s=6 +break}if(m instanceof A.dn){i=o.b +i.toString +n.dq(i) +s=6 +break}i=A.J("Unknown message",null) +throw A.b(i) +case 6:q=1 +s=5 +break +case 3:q=2 +g=p.pop() +j=A.H(g) +new A.dz(J.b1(j)).eR(a) +a.close() +s=5 +break +case 2:s=1 +break +case 5:return A.i(null,r) +case 1:return A.h(p.at(-1),r)}}) +return A.j($async$cG,r)}, +bX(a){return this.jx(a)}, +jx(a){var s=0,r=A.k(t.fL),q,p=this,o,n,m,l,k,j,i,h,g,f,e,d,c +var $async$bX=A.l(function(b,a0){if(b===1)return A.h(a0,r) +for(;;)switch(s){case 0:k=v.G +j="Worker" in k +s=3 +return A.c(A.iW(),$async$bX) +case 3:i=a0 +s=!j?4:6 +break +case 4:k=p.c.a.j(0,a) +if(k==null)o=null +else{k=k.a +k=k===B.r||k===B.B +o=k}h=A +g=!1 +f=!1 +e=i +d=B.y +c=B.q +s=o==null?7:9 +break +case 7:s=10 +return A.c(A.e4(a),$async$bX) +case 10:s=8 +break +case 9:a0=o +case 8:q=new h.c6(g,f,e,d,c,a0,!1) +s=1 +break +s=5 +break +case 6:n={} +m=p.b +if(m==null)m=p.b=new k.Worker(A.i_().i(0)) +new A.dl(a).dq(m) +k=new A.n($.m,t.a9) +n.a=n.b=null +l=new A.l_(n,new A.a7(k,t.bi),i) +n.b=A.aL(m,"message",new A.kY(l),!1) +n.a=A.aL(m,"error",new A.kZ(p,l,m),!1) +q=k +s=1 +break +case 5:case 1:return A.i(q,r)}}) +return A.j($async$bX,r)}} +A.l0.prototype={ +$1(a){return this.a.dX(a)}, +$S:1} +A.kX.prototype={ +$1(a){return this.a.cG(this.b,a)}, +$S:1} +A.l_.prototype={ +$4(a,b,c,d){var s,r=this.b +if((r.a.a&30)===0){r.O(new A.c6(!0,a,this.c,d,B.q,c,b)) +r=this.a +s=r.b +if(s!=null)s.J() +r=r.a +if(r!=null)r.J()}}, +$S:58} +A.kY.prototype={ +$1(a){var s=t.ed.a(A.oB(A.a9(a.data))) +this.a.$4(s.f,s.d,s.c,s.a)}, +$S:1} +A.kZ.prototype={ +$1(a){this.b.$4(!1,!1,!1,B.y) +this.c.terminate() +this.a.b=null}, +$S:1} +A.cc.prototype={ +ad(){return"WasmStorageImplementation."+this.b}} +A.bO.prototype={ +ad(){return"WebStorageApi."+this.b}} +A.i4.prototype={} +A.iT.prototype={ +l_(){var s=this.Q.bB(this.as) +return s}, +bq(){var s=0,r=A.k(t.H),q=this,p +var $async$bq=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:p=q.at +p=p==null?null:p.aU(!1) +s=2 +return A.c(p instanceof A.n?p:A.cL(p,t.H),$async$bq) +case 2:return A.i(null,r)}}) +return A.j($async$bq,r)}, +bt(a,b){return this.jl(a,b)}, +jl(a,b){var s=0,r=A.k(t.z),q=this +var $async$bt=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:q.li(a,b) +s=!q.a?2:3 +break +case 2:s=4 +return A.c(q.bq(),$async$bt) +case 4:case 3:return A.i(null,r)}}) +return A.j($async$bt,r)}, +a6(a,b){return this.ld(a,b)}, +ld(a,b){var s=0,r=A.k(t.H),q=this +var $async$a6=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:s=2 +return A.c(q.bt(a,b),$async$a6) +case 2:return A.i(null,r)}}) +return A.j($async$a6,r)}, +aB(a,b){return this.le(a,b)}, +le(a,b){var s=0,r=A.k(t.S),q,p=this,o +var $async$aB=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.bt(a,b),$async$aB) +case 3:o=p.b.b +q=A.B(v.G.Number(o.a.d.sqlite3_last_insert_rowid(o.b))) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$aB,r)}, +de(a,b){return this.lh(a,b)}, +lh(a,b){var s=0,r=A.k(t.S),q,p=this,o +var $async$de=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:s=3 +return A.c(p.bt(a,b),$async$de) +case 3:o=p.b.b +q=o.a.d.sqlite3_changes(o.b) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$de,r)}, +aA(a){return this.lb(a)}, +lb(a){var s=0,r=A.k(t.H),q=this +var $async$aA=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:q.la(a) +s=!q.a?2:3 +break +case 2:s=4 +return A.c(q.bq(),$async$aA) +case 4:case 3:return A.i(null,r)}}) +return A.j($async$aA,r)}, +n(){var s=0,r=A.k(t.H),q=this +var $async$n=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:s=2 +return A.c(q.hT(),$async$n) +case 2:q.b.n() +s=3 +return A.c(q.bq(),$async$n) +case 3:return A.i(null,r)}}) +return A.j($async$n,r)}} +A.fY.prototype={ +fS(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var s +A.rt("absolute",A.f([a,b,c,d,e,f,g,h,i,j,k,l,m,n,o],t.d4)) +s=this.a +s=s.Y(a)>0&&!s.aZ(a) +if(s)return a +s=this.b +return this.hc(0,s==null?A.p5():s,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o)}, +jS(a){var s=null +return this.fS(a,s,s,s,s,s,s,s,s,s,s,s,s,s,s)}, +hc(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){var s=A.f([b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q],t.d4) +A.rt("join",s) +return this.kN(new A.eR(s,t.eJ))}, +kM(a,b,c){var s=null +return this.hc(0,b,c,s,s,s,s,s,s,s,s,s,s,s,s,s,s)}, +kN(a){var s,r,q,p,o,n,m,l,k +for(s=a.gq(0),r=new A.cG(s,new A.js()),q=this.a,p=!1,o=!1,n="";r.k();){m=s.gm() +if(q.aZ(m)&&o){l=A.di(m,q) +k=n.charCodeAt(0)==0?n:n +n=B.a.p(k,0,q.bF(k,!0)) +l.b=n +if(q.cb(n))l.e[0]=q.gbk() +n=l.i(0)}else if(q.Y(m)>0){o=!q.aZ(m) +n=m}else{if(!(m.length!==0&&q.eh(m[0])))if(p)n+=q.gbk() +n+=m}p=q.cb(m)}return n.charCodeAt(0)==0?n:n}, +bm(a,b){var s=A.di(b,this.a),r=s.d,q=A.O(r).h("aK<1>") +r=A.ak(new A.aK(r,new A.jt(),q),q.h("d.E")) +s.d=r +q=s.b +if(q!=null)B.c.d2(r,0,q) +return s.d}, +eF(a){var s +if(!this.iR(a))return a +s=A.di(a,this.a) +s.eE() +return s.i(0)}, +iR(a){var s,r,q,p,o,n,m,l=this.a,k=l.Y(a) +if(k!==0){if(l===$.fK())for(s=0;s0)return o.eF(a) +if(m.Y(a)<=0||m.aZ(a))a=o.jS(a) +if(m.Y(a)<=0&&m.Y(s)>0)throw A.b(A.pX(n+a+'" from "'+s+'".')) +r=A.di(s,m) +r.eE() +q=A.di(a,m) +q.eE() +l=r.d +if(l.length!==0&&l[0]===".")return q.i(0) +l=r.b +p=q.b +if(l!=p)l=l==null||p==null||!m.eH(l,p) +else l=!1 +if(l)return q.i(0) +for(;;){l=r.d +if(l.length!==0){p=q.d +l=p.length!==0&&m.eH(l[0],p[0])}else l=!1 +if(!l)break +B.c.dd(r.d,0) +B.c.dd(r.e,1) +B.c.dd(q.d,0) +B.c.dd(q.e,1)}l=r.d +p=l.length +if(p!==0&&l[0]==="..")throw A.b(A.pX(n+a+'" from "'+s+'".')) +l=t.N +B.c.ev(q.d,0,A.b5(p,"..",!1,l)) +p=q.e +p[0]="" +B.c.ev(p,1,A.b5(r.d.length,m.gbk(),!1,l)) +m=q.d +l=m.length +if(l===0)return"." +if(l>1&&B.c.gD(m)==="."){B.c.hm(q.d) +m=q.e +m.pop() +m.pop() +m.push("")}q.b="" +q.hn() +return q.i(0)}, +ht(a){var s,r=this.a +if(r.Y(a)<=0)return r.hl(a) +else{s=this.b +return r.ec(this.kM(0,s==null?A.p5():s,a))}}, +l3(a){var s,r,q=this,p=A.p_(a) +if(p.gW()==="file"&&q.a===$.fJ())return p.i(0) +else if(p.gW()!=="file"&&p.gW()!==""&&q.a!==$.fJ())return p.i(0) +s=q.eF(q.a.d9(A.p_(p))) +r=q.l4(s) +return q.bm(0,r).length>q.bm(0,s).length?s:r}} +A.js.prototype={ +$1(a){return a!==""}, +$S:2} +A.jt.prototype={ +$1(a){return a.length!==0}, +$S:2} +A.nI.prototype={ +$1(a){return a==null?"null":'"'+a+'"'}, +$S:60} +A.kp.prototype={ +hF(a){var s=this.Y(a) +if(s>0)return B.a.p(a,0,s) +return this.aZ(a)?a[0]:null}, +hl(a){var s,r=null,q=a.length +if(q===0)return A.al(r,r,r,r) +s=A.pB(this).bm(0,a) +if(this.au(a.charCodeAt(q-1)))B.c.v(s,"") +return A.al(r,r,s,r)}, +eH(a,b){return a===b}} +A.kD.prototype={ +geu(){var s=this.d +if(s.length!==0)s=B.c.gD(s)===""||B.c.gD(this.e)!=="" +else s=!1 +return s}, +hn(){var s,r,q=this +for(;;){s=q.d +if(!(s.length!==0&&B.c.gD(s)===""))break +B.c.hm(q.d) +q.e.pop()}s=q.e +r=s.length +if(r!==0)s[r-1]=""}, +eE(){var s,r,q,p,o,n=this,m=A.f([],t.s) +for(s=n.d,r=s.length,q=0,p=0;p0){s=B.a.aY(a,"\\",s+1) +if(s>0)return s}return r}if(r<3)return 0 +if(!A.rE(a.charCodeAt(0)))return 0 +if(a.charCodeAt(1)!==58)return 0 +r=a.charCodeAt(2) +if(!(r===47||r===92))return 0 +return 3}, +Y(a){return this.bF(a,!1)}, +aZ(a){return this.Y(a)===1}, +d9(a){var s,r +if(a.gW()!==""&&a.gW()!=="file")throw A.b(A.J("Uri "+a.i(0)+" must have scheme 'file:'.",null)) +s=a.ga9() +if(a.gb9()===""){if(s.length>=3&&B.a.u(s,"/")&&A.rz(s,1)!=null)s=B.a.hp(s,"/","")}else s="\\\\"+a.gb9()+s +r=A.bk(s,"/","\\") +return A.oT(r,0,r.length,B.j,!1)}, +ec(a){var s,r,q=A.di(a,this),p=q.b +p.toString +if(B.a.u(p,"\\\\")){s=new A.aK(A.f(p.split("\\"),t.s),new A.m4(),t.U) +B.c.d2(q.d,0,s.gD(0)) +if(q.geu())B.c.v(q.d,"") +return A.al(s.gE(0),null,q.d,"file")}else{if(q.d.length===0||q.geu())B.c.v(q.d,"") +p=q.d +r=q.b +r.toString +r=A.bk(r,"/","") +B.c.d2(p,0,A.bk(r,"\\","")) +return A.al(null,null,q.d,"file")}}, +k5(a,b){var s +if(a===b)return!0 +if(a===47)return b===92 +if(a===92)return b===47 +if((a^b)!==32)return!1 +s=a|32 +return s>=97&&s<=122}, +eH(a,b){var s,r +if(a===b)return!0 +s=a.length +if(s!==b.length)return!1 +for(r=0;r")).av(0,", ")):s}return p.charCodeAt(0)==0?p:p}, +$ia6:1} +A.l7.prototype={ +$1(a){if(t.E.b(a))return"blob ("+a.length+" bytes)" +else return J.b1(a)}, +$S:61} +A.cn.prototype={} +A.h_.prototype={ +glm(){var s,r,q=this.l2("PRAGMA user_version;") +try{s=q.eQ(new A.cw(B.aA)) +r=A.B(J.j0(s).b[0]) +return r}finally{q.n()}}, +h0(a,b,c,d,e){var s,r,q,p,o,n=null,m=this.b,l=B.i.a4(e) +if(l.length>255)A.E(A.ad(e,"functionName","Must not exceed 255 bytes when utf-8 encoded")) +s=new Uint8Array(A.fC(l)) +r=c?526337:2049 +q=m.a +p=q.c1(s,1) +s=q.d +o=A.p1(s,"dart_sqlite3_create_function_v2",[m.b,p,a.a,r,0,new A.bG(new A.jL(d),n,n)]) +s.dart_sqlite3_free(p) +if(o!==0)A.fH(this,o,n,n,n)}, +a5(a,b,c,d){return this.h0(a,b,!0,c,d)}, +n(){var s,r,q,p=this +if(p.r)return +p.r=!0 +s=p.b +r=s.eT() +q=r!==0?A.p4(p.a,s,r,"closing database",null,null):null +if(q!=null)throw A.b(q)}, +h5(a){var s,r,q,p=this,o=B.o +if(J.aC(o)===0){if(p.r)A.E(A.A("This database has already been closed")) +r=p.b +q=r.a +s=q.c1(B.i.a4(a),1) +q=q.d +r=A.p1(q,"sqlite3_exec",[r.b,s,0,0,0]) +q.dart_sqlite3_free(s) +if(r!==0)A.fH(p,r,"executing",a,o)}else{s=p.da(a,!0) +try{s.h6(new A.cw(o))}finally{s.n()}}}, +j4(a,b,c,d,a0){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e=this +if(e.r)A.E(A.A("This database has already been closed")) +s=B.i.a4(a) +r=e.b +q=r.a +p=q.bw(s) +o=q.d +n=o.dart_sqlite3_malloc(4) +o=o.dart_sqlite3_malloc(4) +m=new A.lR(r,p,n,o) +l=A.f([],t.bb) +k=new A.jK(m,l) +for(r=s.length,q=q.b,j=0;j."))}, +dz(a){A:{this.i8(a.a) +break A}}, +ci(){if(!this.f){var s=this.a +s.c.d.sqlite3_reset(s.b) +this.f=!0}}, +n(){var s,r,q=this +if(!q.r){q.r=!0 +q.ci() +s=q.a +r=s.c +r.d.sqlite3_finalize(s.b) +r=r.w +if(r!=null)r.h2(s.d)}}, +eQ(a){var s=this +s.fd() +s.ci() +s.dz(a) +return s.jm()}, +h6(a){var s=this +s.fd() +s.ci() +s.dz(a) +s.fg()}} +A.he.prototype={ +co(a,b){return this.d.a3(a)?1:0}, +dg(a,b){this.d.F(0,a)}, +dh(a){return new v.G.URL(a,"file:///").pathname}, +b0(a,b){var s,r=a.a +if(r==null)r=A.oj(this.b,"/") +s=this.d +if(!s.a3(r))if((b&4)!==0)s.t(0,r,new A.bg(new Uint8Array(0),0)) +else throw A.b(A.ca(14)) +return new A.cR(new A.it(this,r,(b&8)!==0),0)}, +dk(a){}} +A.it.prototype={ +eJ(a,b){var s,r=this.a.d.j(0,this.b) +if(r==null||r.b<=b)return 0 +s=Math.min(a.length,r.b-b) +B.e.N(a,0,s,J.d0(B.e.gaX(r.a),0,r.b),b) +return s}, +df(){return this.d>=2?1:0}, +cp(){if(this.c)this.a.d.F(0,this.b)}, +cr(){return this.a.d.j(0,this.b).b}, +di(a){this.d=a}, +dl(a){}, +cs(a){var s=this.a.d,r=this.b,q=s.j(0,r) +if(q==null){s.t(0,r,new A.bg(new Uint8Array(0),0)) +s.j(0,r).sl(0,a)}else q.sl(0,a)}, +dm(a){this.d=a}, +bi(a,b){var s,r=this.a.d,q=this.b,p=r.j(0,q) +if(p==null){p=new A.bg(new Uint8Array(0),0) +r.t(0,q,p)}s=b+a.length +if(s>p.b)p.sl(0,s) +p.ac(0,b,s,a)}} +A.o_.prototype={ +$1(a){return a.length!==0}, +$S:2} +A.ju.prototype={ +ia(){var s,r,q,p,o=A.ao(t.N,t.S) +for(s=this.a,r=s.length,q=0;qq.d)throw A.b(A.ca(14)) +s=q.a.b +s===$&&A.x() +s=A.bE(s.buffer,0,null) +r=q.e +B.e.b2(s,r,p) +s.$flags&2&&A.y(s) +s[r+o]=0}, +$S:0} +A.jC.prototype={ +$0(){var s,r=this,q=r.a.b +q===$&&A.x() +s=A.bE(q.buffer,r.b,r.c) +q=r.d +if(q!=null)A.pu(s,q.b) +else return A.pu(s,null)}, +$S:0} +A.jE.prototype={ +$0(){this.a.dk(A.pE(this.b,0))}, +$S:0} +A.jx.prototype={ +$0(){return this.a.cp()}, +$S:0} +A.jD.prototype={ +$0(){var s=this,r=s.a.b +r===$&&A.x() +s.b.eP(A.bE(r.buffer,s.c,s.d),A.B(v.G.Number(s.e)))}, +$S:0} +A.jI.prototype={ +$0(){var s=this,r=s.a.b +r===$&&A.x() +s.b.bi(A.bE(r.buffer,s.c,s.d),A.B(v.G.Number(s.e)))}, +$S:0} +A.jG.prototype={ +$0(){return this.a.cs(A.B(v.G.Number(this.b)))}, +$S:0} +A.jF.prototype={ +$0(){return this.a.dl(this.b)}, +$S:0} +A.jz.prototype={ +$0(){var s,r=this.b.cr(),q=this.a.b +q===$&&A.x() +q=A.bD(q.buffer,0,null) +s=B.b.M(this.c,2) +q.$flags&2&&A.y(q) +q[s]=r}, +$S:0} +A.jB.prototype={ +$0(){return this.a.di(this.b)}, +$S:0} +A.jH.prototype={ +$0(){return this.a.dm(this.b)}, +$S:0} +A.jw.prototype={ +$0(){var s,r=this.b.df(),q=this.a.b +q===$&&A.x() +q=A.bD(q.buffer,0,null) +s=B.b.M(this.c,2) +q.$flags&2&&A.y(q) +q[s]=r}, +$S:0} +A.bG.prototype={} +A.e9.prototype={ +P(a,b,c,d){var s,r=null,q={},p=A.a9(A.hm(this.a,v.G.Symbol.asyncIterator,r,r,r,r)),o=A.eN(r,r,!0,this.$ti.c) +q.a=null +s=new A.j3(q,this,p,o) +o.d=s +o.f=new A.j4(q,o,s) +return new A.at(o,A.r(o).h("at<1>")).P(a,b,c,d)}, +b_(a,b,c){return this.P(a,null,b,c)}} +A.j3.prototype={ +$0(){var s,r=this,q=r.c.next(),p=r.a +p.a=q +s=r.d +A.V(q,t.m).bf(new A.j5(p,r.b,s,r),s.gfT(),t.P)}, +$S:0} +A.j5.prototype={ +$1(a){var s,r,q=this,p=a.done +if(p==null)p=null +s=a.value +r=q.c +if(p===!0){r.n() +q.a.a=null}else{r.v(0,s==null?q.b.$ti.c.a(s):s) +q.a.a=null +p=r.b +if(!((p&1)!==0?(r.gaV().e&4)!==0:(p&2)===0))q.d.$0()}}, +$S:11} +A.j4.prototype={ +$0(){var s,r +if(this.a.a==null){s=this.b +r=s.b +s=!((r&1)!==0?(s.gaV().e&4)!==0:(r&2)===0)}else s=!1 +if(s)this.c.$0()}, +$S:0} +A.cK.prototype={ +J(){var s=0,r=A.k(t.H),q=this,p +var $async$J=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:p=q.b +if(p!=null)p.J() +p=q.c +if(p!=null)p.J() +q.c=q.b=null +return A.i(null,r)}}) +return A.j($async$J,r)}, +gm(){var s=this.a +return s==null?A.E(A.A("Await moveNext() first")):s}, +k(){var s,r,q=this,p=q.a +if(p!=null)p.continue() +p=new A.n($.m,t.k) +s=new A.a1(p,t.fa) +r=q.d +q.b=A.aL(r,"success",new A.mo(q,s),!1) +q.c=A.aL(r,"error",new A.mp(q,s),!1) +return p}} +A.mo.prototype={ +$1(a){var s,r=this.a +r.J() +s=r.$ti.h("1?").a(r.d.result) +r.a=s +this.b.O(s!=null)}, +$S:1} +A.mp.prototype={ +$1(a){var s=this.a +s.J() +s=s.d.error +if(s==null)s=a +this.b.ag(s)}, +$S:1} +A.jk.prototype={ +$1(a){this.a.O(this.c.a(this.b.result))}, +$S:1} +A.jl.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ag(s)}, +$S:1} +A.jp.prototype={ +$1(a){this.a.O(this.c.a(this.b.result))}, +$S:1} +A.jq.prototype={ +$1(a){var s=this.b.error +if(s==null)s=a +this.a.ag(s)}, +$S:1} +A.jr.prototype={ +$1(a){this.a.ag(new A.aI("IndexedDB open blocked"))}, +$S:1} +A.lL.prototype={ +k8(){var s={} +s.dart=new A.lM(this).$0() +return s}, +d7(a){return this.kP(a)}, +kP(a){var s=0,r=A.k(t.m),q,p=this,o,n +var $async$d7=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:s=3 +return A.c(A.V(v.G.WebAssembly.instantiateStreaming(a,p.k8()),t.m),$async$d7) +case 3:o=c +n=o.instance.exports +if("_initialize" in n)t.g.a(n._initialize).call() +q=o.instance +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$d7,r)}} +A.lM.prototype={ +$0(){var s=this.a.a,r=A.a9(v.G.Object),q=A.a9(r.create.apply(r,[null])) +q.error_log=A.bi(s.gkS()) +q.localtime=A.b9(s.gkQ()) +q.xOpen=A.oX(s.glH()) +q.xDelete=A.nA(s.glw()) +q.xAccess=A.dX(s.glo()) +q.xFullPathname=A.dX(s.glD()) +q.xRandomness=A.nA(s.glJ()) +q.xSleep=A.b9(s.glO()) +q.xCurrentTimeInt64=A.b9(s.glu()) +q.xClose=A.bi(s.gls()) +q.xRead=A.dX(s.glL()) +q.xWrite=A.dX(s.glW()) +q.xTruncate=A.b9(s.glS()) +q.xSync=A.b9(s.glQ()) +q.xFileSize=A.b9(s.glB()) +q.xLock=A.b9(s.glF()) +q.xUnlock=A.b9(s.glU()) +q.xCheckReservedLock=A.b9(s.glq()) +q.xDeviceCharacteristics=A.bi(s.gcq()) +q.xFileControl=A.nA(s.glz()) +q.xSectorSize=A.bi(s.gdj()) +q["dispatch_()v"]=A.bi(s.gkm()) +q["dispatch_()i"]=A.bi(s.gkh()) +q.dispatch_update=A.oX(s.gkk()) +q.dispatch_xFunc=A.dX(s.gks()) +q.dispatch_xStep=A.dX(s.gkw()) +q.dispatch_xInverse=A.dX(s.gku()) +q.dispatch_xValue=A.b9(s.gky()) +q.dispatch_xFinal=A.b9(s.gkq()) +q.dispatch_compare=A.oX(s.gko()) +q.dispatch_busy=A.b9(s.gkf()) +q.changeset_apply_filter=A.b9(s.gkd()) +q.changeset_apply_conflict=A.nA(s.gkb()) +return q}, +$S:126} +A.i6.prototype={} +A.dy.prototype={ +jh(a,b){var s,r,q=this.e +q.hu(b) +s=this.d.b +r=v.G +r.Atomics.store(s,1,-1) +r.Atomics.store(s,0,a.a) +A.tN(s,0) +r.Atomics.wait(s,1,-1) +s=r.Atomics.load(s,1) +if(s!==0)throw A.b(A.ca(s)) +return a.d.$1(q)}, +a1(a,b){var s=t.cb +return this.jh(a,b,s,s)}, +co(a,b){return this.a1(B.Z,new A.aW(a,b,0,0)).a}, +dg(a,b){this.a1(B.a_,new A.aW(a,b,0,0))}, +dh(a){return new v.G.URL(a,"file:///").pathname}, +b0(a,b){var s=a.a,r=this.a1(B.aa,new A.aW(s==null?A.oj(this.b,"/"):s,b,0,0)) +return new A.cR(new A.i5(this,r.b),r.a)}, +dk(a){this.a1(B.a4,new A.R(B.b.I(a.a,1000),0,0))}, +n(){this.a1(B.a0,B.h)}} +A.i5.prototype={ +gcq(){return 2048}, +eJ(a,b){var s,r,q,p,o,n,m,l,k,j,i=a.length +for(s=this.a,r=this.b,q=s.e.a,p=v.G,o=t.Z,n=0;i>0;){m=Math.min(65536,i) +i-=m +l=s.a1(B.a8,new A.R(r,b+n,m)).a +k=p.Uint8Array +j=[q] +j.push(0) +j.push(l) +A.hm(a,"set",o.a(A.e3(k,j)),n,null,null) +n+=l +if(l0;){o=Math.min(65536,n) +A.hm(r,"set",o===n&&p===0?a:J.d0(B.e.gaX(a),a.byteOffset+p,o),0,null,null) +s.a1(B.a3,new A.R(q,b+p,o)) +p+=o +n-=o}}} +A.kL.prototype={} +A.br.prototype={ +hu(a){var s,r +if(!(a instanceof A.b2))if(a instanceof A.R){s=this.b +s.$flags&2&&A.y(s,8) +s.setInt32(0,a.a,!1) +s.setInt32(4,a.b,!1) +s.setInt32(8,a.c,!1) +if(a instanceof A.aW){r=B.i.a4(a.d) +s.setInt32(12,r.length,!1) +B.e.b2(this.c,16,r)}}else throw A.b(A.a4("Message "+a.i(0)))}} +A.ac.prototype={ +ad(){return"WorkerOperation."+this.b}} +A.bC.prototype={} +A.b2.prototype={} +A.R.prototype={} +A.aW.prototype={} +A.iF.prototype={} +A.eQ.prototype={ +bT(a,b){return this.je(a,b)}, +fE(a){return this.bT(a,!1)}, +je(a,b){var s=0,r=A.k(t.eg),q,p=this,o,n,m,l,k,j,i,h +var $async$bT=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:k=A.ak(A.pc(a),t.N) +j=k.length +i=j>=1 +h=null +if(i){o=j-1 +n=B.c.a_(k,0,o) +h=k[o]}else n=null +if(!i)throw A.b(A.A("Pattern matching error")) +m=p.c +k=n.length,i=t.m,l=0 +case 3:if(!(l=i.length){s=5 +break}j=new A.mV(g,k,Math.min(4096,i.length-k)) +if(A.kq(m.value,"Blob"))b.push(A.kK(A.a9(m.value)).bG(j,n)) +else j.$1(h.a(m.value)) +s=4 +break +case 5:q=g +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$bJ,r)}, +cW(a){return this.k7(a)}, +k7(a){var s=0,r=A.k(t.S),q,p=this,o +var $async$cW=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:if((p.b.a.a&30)!==0)A.E(A.A("IDB transaction already completed")) +o=A +s=3 +return A.c(A.bn(p.d.put({name:a,length:0}),t.i),$async$cW) +case 3:q=o.B(c) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$cW,r)}, +bh(a,b){return this.ln(a,b)}, +ln(a,b){var s=0,r=A.k(t.H),q=this,p,o,n,m,l +var $async$bh=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:if((q.b.a.a&30)!==0)A.E(A.A("IDB transaction already completed")) +s=2 +return A.c(q.e1(a),$async$bh) +case 2:p=d +o=b.b +n=A.r(o).h("bB<1>") +m=A.ak(new A.bB(o,n),n.h("d.E")) +B.c.hM(m) +s=3 +return A.c(A.pL(new A.D(m,new A.mW(new A.mX(q,a),b),A.O(m).h("D<1,C<~>>")),t.H),$async$bh) +case 3:s=b.c!==p.length?4:5 +break +case 4:l=new A.cK(q.d.openCursor(a),t.V) +s=6 +return A.c(l.k(),$async$bh) +case 6:s=7 +return A.c(A.bn(l.gm().update({name:p.name,length:b.c}),t.X),$async$bh) +case 7:case 5:return A.i(null,r)}}) +return A.j($async$bh,r)}, +bg(a,b,c){return this.lk(0,b,c)}, +lk(a,b,c){var s=0,r=A.k(t.H),q=this,p,o +var $async$bg=A.l(function(d,e){if(d===1)return A.h(e,r) +for(;;)switch(s){case 0:if((q.b.a.a&30)!==0)A.E(A.A("IDB transaction already completed")) +s=2 +return A.c(q.e1(b),$async$bg) +case 2:p=e +s=p.length>c?3:4 +break +case 3:s=5 +return A.c(A.bn(q.e.delete(q.j6(b,B.b.I(c,4096)*4096)),t.X),$async$bg) +case 5:case 4:o=new A.cK(q.d.openCursor(b),t.V) +s=6 +return A.c(o.k(),$async$bg) +case 6:s=7 +return A.c(A.bn(o.gm().update({name:p.name,length:c}),t.X),$async$bg) +case 7:return A.i(null,r)}}) +return A.j($async$bg,r)}, +cY(a){return this.ka(a)}, +ka(a){var s=0,r=A.k(t.H),q=this,p +var $async$cY=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:if((q.b.a.a&30)!==0)A.E(A.A("IDB transaction already completed")) +p=t.X +s=2 +return A.c(A.pL(A.f([A.bn(q.e.delete(q.e0(a,9007199254740992,0)),p),A.bn(q.d.delete(a),p)],t.fG),t.H),$async$cY) +case 2:return A.i(null,r)}}) +return A.j($async$cY,r)}} +A.mT.prototype={ +$0(){this.a.b.aJ()}, +$S:3} +A.mU.prototype={ +$0(){var s=this.a,r=s.a.error +if(r==null)r=new v.G.DOMException("IDB transaction error") +s.b.ag(r)}, +$S:3} +A.mS.prototype={ +$1(a){if(a==null)throw A.b(A.ad(this.a,"fileId","File not found in database")) +else return a}, +$S:87} +A.mV.prototype={ +$1(a){var s=this.a +s.b2(s,this.b,J.d0(a,0,this.c))}, +$S:88} +A.mX.prototype={ +hE(a,b){var s=0,r=A.k(t.H),q=this,p,o,n,m,l,k +var $async$$2=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:p=q.a.e +o=q.b +n=t.n +s=2 +return A.c(A.bn(p.openCursor(v.G.IDBKeyRange.only(A.f([o,a],n))),t.A),$async$$2) +case 2:m=d +l=t.u.a(B.e.gaX(b)) +k=t.X +s=m==null?3:5 +break +case 3:s=6 +return A.c(A.bn(p.put(l,A.f([o,a],n)),k),$async$$2) +case 6:s=4 +break +case 5:s=7 +return A.c(A.bn(m.update(l),k),$async$$2) +case 7:case 4:return A.i(null,r)}}) +return A.j($async$$2,r)}, +$2(a,b){return this.hE(a,b)}, +$S:89} +A.mW.prototype={ +$1(a){var s=this.b.b.j(0,a) +s.toString +return this.a.$2(a,s)}, +$S:90} +A.mz.prototype={ +jC(a,b,c){B.e.b2(this.b.hk(a,new A.mA(this,a)),b,c)}, +jV(a,b){var s,r,q,p,o,n,m,l +for(s=b.length,r=0;rp)B.e.b2(s,0,J.d0(B.e.gaX(r),r.byteOffset+p,Math.min(4096,q-p))) +return s}, +$S:125} +A.iB.prototype={} +A.d7.prototype={ +bY(a){var s=this +if(s.e||s.d.a==null)A.E(A.ca(10)) +if(a.ew(s.x)){s.aU(!0) +return a.d.a}else return A.b3(null,t.H)}, +aU(a){return this.jz(a)}, +jz(a){var s=0,r=A.k(t.H),q,p=this,o,n +var $async$aU=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:if(a&&!p.r){s=1 +break}s=!p.f&&!p.x.gB(0)?3:4 +break +case 3:p.f=!0 +o=p.x +n=A.ak(o,o.$ti.h("d.E")) +o.c3(0) +s=5 +return A.c(p.d.j3(n).ai(new A.kj(p,n,a)),$async$aU) +case 5:case 4:case 1:return A.i(q,r)}}) +return A.j($async$aU,r)}, +n(){var s=0,r=A.k(t.H),q,p=this,o,n +var $async$n=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:if(!p.e){o=p.bY(new A.f6(new A.kk(),new A.a1(new A.n($.m,t.D),t.F))) +p.e=!0 +p.aU(!1) +q=o +s=1 +break}else{n=p.x +if(!n.gB(0)){q=n.gD(0).d.a +s=1 +break}}case 1:return A.i(q,r)}}) +return A.j($async$n,r)}, +bp(a,b){return this.iB(a,b)}, +iB(a,b){var s=0,r=A.k(t.S),q,p=this,o,n +var $async$bp=A.l(function(c,d){if(c===1)return A.h(d,r) +for(;;)switch(s){case 0:n=p.z +s=n.a3(b)?3:5 +break +case 3:n=n.j(0,b) +n.toString +q=n +s=1 +break +s=4 +break +case 5:s=6 +return A.c(a.d_(b),$async$bp) +case 6:o=d +o.toString +n.t(0,b,o) +q=o +s=1 +break +case 4:case 1:return A.i(q,r)}}) +return A.j($async$bp,r)}, +bR(){var s=0,r=A.k(t.H),q=this,p +var $async$bR=A.l(function(a,b){if(a===1)return A.h(b,r) +for(;;)switch(s){case 0:p=A.f([],t.fG) +s=2 +return A.c(q.d.bs(new A.ki(q,p),"readonly"),$async$bR) +case 2:s=3 +return A.c(A.ua(p,t.H),$async$bR) +case 3:return A.i(null,r)}}) +return A.j($async$bR,r)}, +co(a,b){return this.w.d.a3(a)?1:0}, +dg(a,b){var s=this +s.w.d.F(0,a) +if(!s.y.F(0,a))s.bY(new A.f_(s,a,new A.a1(new A.n($.m,t.D),t.F)))}, +dh(a){return new v.G.URL(a,"file:///").pathname}, +b0(a,b){var s,r,q,p=this,o=a.a +if(o==null)o=A.oj(p.b,"/") +s=p.w +r=s.d.a3(o)?1:0 +q=s.b0(new A.eJ(o),b) +if(r===0)if((b&8)!==0)p.y.v(0,o) +else p.bY(new A.dC(p,o,new A.a1(new A.n($.m,t.D),t.F))) +return new A.cR(new A.iu(p,q.a,o),0)}, +dk(a){}} +A.kj.prototype={ +$0(){var s,r,q,p,o=this.a +o.f=!1 +for(s=this.b,r=s.length,q=0;q=2?1:0}, +cp(){}, +cr(){return this.b.cr()}, +di(a){this.b.d=a +return null}, +dl(a){}, +hv(a,b){return 12}, +cs(a){var s=this,r=s.a +if(r.e||r.d.a==null)A.E(A.ca(10)) +s.b.cs(a) +if(!r.y.H(0,s.c))r.bY(new A.f6(new A.mR(s,a),new A.a1(new A.n($.m,t.D),t.F)))}, +dm(a){this.b.d=a +return null}, +bi(a,b){var s,r,q,p,o,n,m=this,l=m.a +if(l.e||l.d.a==null)A.E(A.ca(10)) +s=m.c +if(l.y.H(0,s)){m.b.bi(a,b) +return}r=l.w.d.j(0,s) +if(r==null)r=new A.bg(new Uint8Array(0),0) +q=J.d0(B.e.gaX(r.a),0,r.b) +m.b.bi(a,b) +p=new Uint8Array(a.length) +B.e.b2(p,0,a) +o=A.f([],t.gQ) +n=$.m +o.push(new A.iB(b,p)) +l.bY(new A.dU(l,s,q,o,new A.a1(new A.n(n,t.D),t.F)))}, +$iaA:1, +$idw:1} +A.mR.prototype={ +$1(a){return this.hD(a)}, +hD(a){var s=0,r=A.k(t.H),q,p=this,o,n +var $async$$1=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:o=p.a +n=a +s=3 +return A.c(o.a.bp(a,o.c),$async$$1) +case 3:q=n.bg(0,c,p.b) +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$$1,r)}, +$S:18} +A.au.prototype={ +ew(a){a.cD(a.c,this,!1) +return!0}} +A.f6.prototype={ +Z(a){return this.w.$1(a)}} +A.f_.prototype={ +ew(a){var s,r,q,p +if(!a.gB(0)){s=a.gD(0) +for(r=this.x;s!=null;)if(s instanceof A.f_)if(s.x===r)return!1 +else s=s.gcf() +else if(s instanceof A.dU){q=s.gcf() +if(s.x===r){p=s.a +p.toString +p.e6(A.r(s).h("ay.E").a(s))}s=q}else if(s instanceof A.dC){if(s.x===r){r=s.a +r.toString +r.e6(A.r(s).h("ay.E").a(s)) +return!1}s=s.gcf()}else break}a.cD(a.c,this,!1) +return!0}, +Z(a){return this.l8(a)}, +l8(a){var s=0,r=A.k(t.H),q=this,p,o,n +var $async$Z=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:p=q.w +o=q.x +s=2 +return A.c(p.bp(a,o),$async$Z) +case 2:n=c +p.z.F(0,o) +s=3 +return A.c(a.cY(n),$async$Z) +case 3:return A.i(null,r)}}) +return A.j($async$Z,r)}} +A.dC.prototype={ +Z(a){return this.l7(a)}, +l7(a){var s=0,r=A.k(t.H),q=this,p,o,n +var $async$Z=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:p=q.x +o=q.w.z +n=p +s=2 +return A.c(a.cW(p),$async$Z) +case 2:o.t(0,n,c) +return A.i(null,r)}}) +return A.j($async$Z,r)}} +A.dU.prototype={ +ew(a){var s,r=a.b===0?null:a.gD(0) +for(s=this.x;r!=null;)if(r instanceof A.dU)if(r.x===s){B.c.aI(r.z,this.z) +return!1}else r=r.gcf() +else if(r instanceof A.dC){if(r.x===s)break +r=r.gcf()}else break +a.cD(a.c,this,!1) +return!0}, +Z(a){return this.l9(a)}, +l9(a){var s=0,r=A.k(t.H),q=this,p,o,n,m,l,k +var $async$Z=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:m=q.y +l=new A.mz(m,A.ao(t.S,t.E),m.length) +for(m=q.z,p=m.length,o=0;o0) +n.ca(B.L,o.getSize()>0)}return A.i(null,r)}}) +return A.j($async$bC,r)}} +A.l4.prototype={ +hB(a){var s=0,r=A.k(t.m),q,p=this,o,n +var $async$$1=A.l(function(b,c){if(b===1)return A.h(c,r) +for(;;)switch(s){case 0:o=t.m +s=3 +return A.c(A.V(p.a.getFileHandle(a,{create:!0}),o),$async$$1) +case 3:n=c.createSyncAccessHandle() +s=4 +return A.c(A.V(n,o),$async$$1) +case 4:q=c +s=1 +break +case 1:return A.i(q,r)}}) +return A.j($async$$1,r)}, +$1(a){return this.hB(a)}, +$S:92} +A.iL.prototype={ +eJ(a,b){return A.og(this.a.an().b8(this.b),a,{at:b})}, +df(){return this.d>=2?1:0}, +cp(){var s=this.a,r=this.b +s.an().b8(r).flush() +if(this.c)s.an().ca(r,!1)}, +cr(){return this.a.an().b8(this.b).getSize()}, +di(a){this.d=a}, +dl(a){this.a.an().b8(this.b).flush()}, +cs(a){this.a.an().b8(this.b).truncate(a)}, +dm(a){this.d=a}, +bi(a,b){if(A.oh(this.a.an().b8(this.b),a,{at:b})")),null)}, +i(a){var s=this.a,r=A.O(s) +return new A.D(s,new A.jd(new A.D(s,new A.je(),r.h("D<1,a>")).eo(0,0,B.u)),r.h("D<1,p>")).av(0,u.q)}, +$ia_:1} +A.ja.prototype={ +$1(a){return a.length!==0}, +$S:2} +A.jf.prototype={ +$1(a){return a.gc6()}, +$S:93} +A.je.prototype={ +$1(a){var s=a.gc6() +return new A.D(s,new A.jc(),A.O(s).h("D<1,a>")).eo(0,0,B.u)}, +$S:94} +A.jc.prototype={ +$1(a){return a.gbA().length}, +$S:38} +A.jd.prototype={ +$1(a){var s=a.gc6() +return new A.D(s,new A.jb(this.a),A.O(s).h("D<1,p>")).c8(0)}, +$S:96} +A.jb.prototype={ +$1(a){return B.a.hh(a.gbA(),this.a)+" "+A.t(a.geC())+"\n"}, +$S:24} +A.M.prototype={ +geA(){var s=this.a +if(s.gW()==="data")return"data:..." +return $.pp().l3(s)}, +gbA(){var s,r=this,q=r.b +if(q==null)return r.geA() +s=r.c +if(s==null)return r.geA()+" "+A.t(q) +return r.geA()+" "+A.t(q)+":"+A.t(s)}, +i(a){return this.gbA()+" in "+A.t(this.d)}, +geC(){return this.d}} +A.ka.prototype={ +$0(){var s,r,q,p,o,n,m,l=null,k=this.a +if(k==="...")return new A.M(A.al(l,l,l,l),l,l,"...") +s=$.tz().a8(k) +if(s==null)return new A.bt(A.al(l,"unparsed",l,l),k) +k=s.b +r=k[1] +r.toString +q=$.th() +r=A.bk(r,q,"") +p=A.bk(r,"","") +r=k[2] +q=r +q.toString +if(B.a.u(q,"1?A.bj(n[1],l):l +return new A.M(o,m,k>2?A.bj(n[2],l):l,p)}, +$S:13} +A.k8.prototype={ +$0(){var s,r,q,p,o,n="",m=this.a,l=$.ty().a8(m) +if(l!=null){s=l.aM("member") +m=l.aM("uri") +m.toString +r=A.hd(m) +m=l.aM("index") +m.toString +q=l.aM("offset") +q.toString +p=A.bj(q,16) +if(!(s==null))m=s +return new A.M(r,1,p+1,m)}l=$.tu().a8(m) +if(l!=null){m=new A.k9(m) +q=l.b +o=q[2] +if(o!=null){o=o +o.toString +q=q[1] +q.toString +q=A.bk(q,"",n) +q=A.bk(q,"Anonymous function",n) +return m.$2(o,A.bk(q,"(anonymous function)",n))}else{q=q[3] +q.toString +return m.$2(q,n)}}return new A.bt(A.al(null,"unparsed",null,null),m)}, +$S:13} +A.k9.prototype={ +$2(a,b){var s,r,q,p,o,n=null,m=$.tt(),l=m.a8(a) +for(;l!=null;a=s){s=l.b[1] +s.toString +l=m.a8(s)}if(a==="native")return new A.M(A.bu("native"),n,n,b) +r=$.tv().a8(a) +if(r==null)return new A.bt(A.al(n,"unparsed",n,n),this.a) +m=r.b +s=m[1] +s.toString +q=A.hd(s) +s=m[2] +s.toString +p=A.bj(s,n) +o=m[3] +return new A.M(q,p,o!=null?A.bj(o,n):n,b)}, +$S:99} +A.k5.prototype={ +$0(){var s,r,q,p,o=null,n=this.a,m=$.ti().a8(n) +if(m==null)return new A.bt(A.al(o,"unparsed",o,o),n) +n=m.b +s=n[1] +s.toString +r=A.bk(s,"/<","") +s=n[2] +s.toString +q=A.hd(s) +n=n[3] +n.toString +p=A.bj(n,o) +return new A.M(q,p,o,r.length===0||r==="anonymous"?"":r)}, +$S:13} +A.k6.prototype={ +$0(){var s,r,q,p,o,n,m,l,k=null,j=this.a,i=$.tk().a8(j) +if(i!=null){s=i.b +r=s[3] +q=r +q.toString +if(B.a.H(q," line "))return A.u2(j) +j=r +j.toString +p=A.hd(j) +o=s[1] +if(o!=null){j=s[2] +j.toString +o+=B.c.c8(A.b5(B.a.ed("/",j).gl(0),".",!1,t.N)) +if(o==="")o="" +o=B.a.hp(o,$.tp(),"")}else o="" +j=s[4] +if(j==="")n=k +else{j=j +j.toString +n=A.bj(j,k)}j=s[5] +if(j==null||j==="")m=k +else{j=j +j.toString +m=A.bj(j,k)}return new A.M(p,n,m,o)}i=$.tm().a8(j) +if(i!=null){j=i.aM("member") +j.toString +s=i.aM("uri") +s.toString +p=A.hd(s) +s=i.aM("index") +s.toString +r=i.aM("offset") +r.toString +l=A.bj(r,16) +if(!(j.length!==0))j=s +return new A.M(p,1,l+1,j)}i=$.tq().a8(j) +if(i!=null){j=i.aM("member") +j.toString +return new A.M(A.al(k,"wasm code",k,k),k,k,j)}return new A.bt(A.al(k,"unparsed",k,k),j)}, +$S:13} +A.k7.prototype={ +$0(){var s,r,q,p,o=null,n=this.a,m=$.tn().a8(n) +if(m==null)throw A.b(A.aj("Couldn't parse package:stack_trace stack trace line '"+n+"'.",o,o)) +n=m.b +s=n[1] +if(s==="data:...")r=A.qq("") +else{s=s +s.toString +r=A.bu(s)}if(r.gW()===""){s=$.pp() +r=s.ht(s.fS(s.a.d9(A.p_(r)),o,o,o,o,o,o,o,o,o,o,o,o,o,o))}s=n[2] +if(s==null)q=o +else{s=s +s.toString +q=A.bj(s,o)}s=n[3] +if(s==null)p=o +else{s=s +s.toString +p=A.bj(s,o)}return new A.M(r,q,p,n[4])}, +$S:13} +A.hp.prototype={ +gfQ(){var s,r=this,q=r.b +if(q===$){s=r.a.$0() +r.b!==$&&A.pj() +r.b=s +q=s}return q}, +gc6(){return this.gfQ().gc6()}, +i(a){return this.gfQ().i(0)}, +$ia_:1, +$ia0:1} +A.a0.prototype={ +i(a){var s=this.a,r=A.O(s) +return new A.D(s,new A.lq(new A.D(s,new A.lr(),r.h("D<1,a>")).eo(0,0,B.u)),r.h("D<1,p>")).c8(0)}, +$ia_:1, +gc6(){return this.a}} +A.lo.prototype={ +$0(){return A.qm(this.a.i(0))}, +$S:100} +A.lp.prototype={ +$1(a){return a.length!==0}, +$S:2} +A.ln.prototype={ +$1(a){return!B.a.u(a,$.tx())}, +$S:2} +A.lm.prototype={ +$1(a){return a!=="\tat "}, +$S:2} +A.lk.prototype={ +$1(a){return a.length!==0&&a!=="[native code]"}, +$S:2} +A.ll.prototype={ +$1(a){return!B.a.u(a,"=====")}, +$S:2} +A.lr.prototype={ +$1(a){return a.gbA().length}, +$S:38} +A.lq.prototype={ +$1(a){if(a instanceof A.bt)return a.i(0)+"\n" +return B.a.hh(a.gbA(),this.a)+" "+A.t(a.geC())+"\n"}, +$S:24} +A.bt.prototype={ +i(a){return this.w}, +$iM:1, +gbA(){return"unparsed"}, +geC(){return this.w}} +A.ee.prototype={} +A.eY.prototype={ +P(a,b,c,d){var s,r=this.b +if(r.d){a=null +d=null}s=this.a.P(a,b,c,d) +if(!r.d)r.c=s +return s}, +b_(a,b,c){return this.P(a,null,b,c)}, +eB(a,b){return this.P(a,null,b,null)}} +A.eX.prototype={ +n(){var s,r=this.hQ(),q=this.b +q.d=!0 +s=q.c +if(s!=null){s.cd(null) +s.eG(null)}return r}} +A.eo.prototype={ +ghP(){var s=this.b +s===$&&A.x() +return new A.at(s,A.r(s).h("at<1>"))}, +ghK(){var s=this.a +s===$&&A.x() +return s}, +hX(a,b,c,d){var s=this,r=$.m +s.a!==$&&A.iZ() +s.a=new A.f8(a,s,new A.a7(new A.n(r,t.D),t.h),!0) +r=A.eN(null,new A.kg(c,s),!0,d) +s.b!==$&&A.iZ() +s.b=r}, +j_(){var s,r +this.d=!0 +s=this.c +if(s!=null)s.J() +r=this.b +r===$&&A.x() +r.n()}} +A.kg.prototype={ +$0(){var s,r,q=this.b +if(q.d)return +s=this.a.a +r=q.b +r===$&&A.x() +q.c=s.b_(r.gjT(r),new A.kf(q),r.gfT())}, +$S:0} +A.kf.prototype={ +$0(){var s=this.a,r=s.a +r===$&&A.x() +r.j0() +s=s.b +s===$&&A.x() +s.n()}, +$S:0} +A.f8.prototype={ +v(a,b){if(this.e)throw A.b(A.A("Cannot add event after closing.")) +if(this.d)return +this.a.a.v(0,b)}, +a2(a,b){if(this.e)throw A.b(A.A("Cannot add event after closing.")) +if(this.d)return +this.iE(a,b)}, +iE(a,b){this.a.a.a2(a,b) +return}, +n(){var s=this +if(s.e)return s.c.a +s.e=!0 +if(!s.d){s.b.j_() +s.c.O(s.a.a.n())}return s.c.a}, +j0(){this.d=!0 +var s=this.c +if((s.a.a&30)===0)s.aJ() +return}, +$iae:1} +A.hO.prototype={} +A.eM.prototype={} +A.dt.prototype={ +gl(a){return this.b}, +j(a,b){if(b>=this.b)throw A.b(A.pO(b,this)) +return this.a[b]}, +t(a,b,c){var s +if(b>=this.b)throw A.b(A.pO(b,this)) +s=this.a +s.$flags&2&&A.y(s) +s[b]=c}, +sl(a,b){var s,r,q,p,o=this,n=o.b +if(bn){if(n===0)p=new Uint8Array(b) +else p=o.io(b) +B.e.ac(p,0,o.b,o.a) +o.a=p}}o.b=b}, +io(a){var s=this.a.length*2 +if(a!=null&&ss)throw A.b(A.W(c,0,s,null,null)) +s=this.a +if(d instanceof A.bg)B.e.N(s,b,c,d.a,e) +else B.e.N(s,b,c,d,e)}, +ac(a,b,c,d){return this.N(0,b,c,d,0)}} +A.iv.prototype={} +A.bg.prototype={} +A.of.prototype={} +A.f3.prototype={ +P(a,b,c,d){return A.aL(this.a,this.b,a,!1)}, +b_(a,b,c){return this.P(a,null,b,c)}} +A.io.prototype={ +J(){var s=this,r=A.b3(null,t.H) +if(s.b==null)return r +s.e7() +s.d=s.b=null +return r}, +cd(a){var s,r=this +if(r.b==null)throw A.b(A.A("Subscription has been canceled.")) +r.e7() +if(a==null)s=null +else{s=A.ru(new A.mx(a),t.m) +s=s==null?null:A.bi(s)}r.d=s +r.e5()}, +eG(a){}, +bD(){if(this.b==null)return;++this.a +this.e7()}, +bc(){var s=this +if(s.b==null||s.a<=0)return;--s.a +s.e5()}, +e5(){var s=this,r=s.d +if(r!=null&&s.a<=0)s.b.addEventListener(s.c,r,!1)}, +e7(){var s=this.d +if(s!=null)this.b.removeEventListener(this.c,s,!1)}} +A.mw.prototype={ +$1(a){return this.a.$1(a)}, +$S:1} +A.mx.prototype={ +$1(a){return this.a.$1(a)}, +$S:1};(function aliases(){var s=J.bY.prototype +s.hS=s.i +s=A.cI.prototype +s.hU=s.bK +s=A.af.prototype +s.ds=s.aQ +s.eW=s.a7 +s.eX=s.bo +s=A.fo.prototype +s.hV=s.ee +s=A.w.prototype +s.eV=s.N +s=A.d.prototype +s.hR=s.hL +s=A.d4.prototype +s.hQ=s.n +s=A.cB.prototype +s.hT=s.n})();(function installTearOffs(){var s=hunkHelpers._static_2,r=hunkHelpers._static_1,q=hunkHelpers._static_0,p=hunkHelpers.installStaticTearOff,o=hunkHelpers._instance_0u,n=hunkHelpers.installInstanceTearOff,m=hunkHelpers._instance_2u,l=hunkHelpers._instance_1i,k=hunkHelpers._instance_1u +s(J,"w6","uf",101) +r(A,"wK","uX",9) +r(A,"wL","uY",9) +r(A,"wM","uZ",9) +r(A,"wN","wk",102) +q(A,"rx","wD",0) +r(A,"wO","wl",15) +s(A,"wP","wn",6) +q(A,"rw","wm",0) +p(A,"wV",5,null,["$5"],["ww"],103,0) +p(A,"x_",4,null,["$1$4","$4"],["nD",function(a,b,c,d){return A.nD(a,b,c,d,t.z)}],104,0) +p(A,"x1",5,null,["$2$5","$5"],["nF",function(a,b,c,d,e){var i=t.z +return A.nF(a,b,c,d,e,i,i)}],105,0) +p(A,"x0",6,null,["$3$6","$6"],["nE",function(a,b,c,d,e,f){var i=t.z +return A.nE(a,b,c,d,e,f,i,i,i)}],106,0) +p(A,"wY",4,null,["$1$4","$4"],["rn",function(a,b,c,d){return A.rn(a,b,c,d,t.z)}],107,0) +p(A,"wZ",4,null,["$2$4","$4"],["ro",function(a,b,c,d){var i=t.z +return A.ro(a,b,c,d,i,i)}],108,0) +p(A,"wX",4,null,["$3$4","$4"],["rm",function(a,b,c,d){var i=t.z +return A.rm(a,b,c,d,i,i,i)}],109,0) +p(A,"wT",5,null,["$5"],["wv"],110,0) +p(A,"x2",4,null,["$4"],["nG"],111,0) +p(A,"wS",5,null,["$5"],["wu"],112,0) +p(A,"wR",5,null,["$5"],["wt"],113,0) +p(A,"wW",4,null,["$4"],["wx"],114,0) +r(A,"wQ","wp",115) +p(A,"wU",5,null,["$5"],["rl"],116,0) +var j +o(j=A.cJ.prototype,"gbO","al",0) +o(j,"gbP","am",0) +n(A.dB.prototype,"gk6",0,1,null,["$2","$1"],["by","ag"],30,0,0) +m(A.n.prototype,"gdF","ig",6) +l(j=A.cS.prototype,"gjT","v",7) +n(j,"gfT",0,1,null,["$2","$1"],["a2","jU"],30,0,0) +o(j=A.cf.prototype,"gbO","al",0) +o(j,"gbP","am",0) +o(j=A.af.prototype,"gbO","al",0) +o(j,"gbP","am",0) +o(A.f0.prototype,"gfs","iZ",0) +k(j=A.dO.prototype,"giT","iU",7) +m(j,"giX","iY",6) +o(j,"giV","iW",0) +o(j=A.dE.prototype,"gbO","al",0) +o(j,"gbP","am",0) +k(j,"gdQ","dR",7) +m(j,"gdU","dV",78) +o(j,"gdS","dT",0) +o(j=A.dL.prototype,"gbO","al",0) +o(j,"gbP","am",0) +k(j,"gdQ","dR",7) +m(j,"gdU","dV",6) +o(j,"gdS","dT",0) +k(A.dM.prototype,"gjZ","ee","X<2>(e?)") +r(A,"x6","uT",8) +p(A,"xx",2,null,["$1$2","$2"],["rG",function(a,b){return A.rG(a,b,t.o)}],117,0) +r(A,"xz","xG",5) +r(A,"xy","xF",5) +r(A,"xw","x7",5) +r(A,"xA","xM",5) +r(A,"xt","wI",5) +r(A,"xu","wJ",5) +r(A,"xv","x3",5) +k(A.ej.prototype,"giH","iI",7) +k(A.h4.prototype,"gip","dI",16) +k(A.i7.prototype,"gjF","cK",16) +r(A,"yZ","rb",22) +r(A,"yX","r9",22) +r(A,"yY","ra",22) +r(A,"rI","wo",28) +r(A,"rJ","wr",120) +r(A,"rH","vX",121) +k(j=A.fZ.prototype,"gkS","kT",4) +m(j,"gkQ","kR",65) +n(j,"glH",0,5,null,["$5"],["lI"],66,0,0) +n(j,"glw",0,3,null,["$3"],["lx"],67,0,0) +n(j,"glo",0,4,null,["$4"],["lp"],32,0,0) +n(j,"glD",0,4,null,["$4"],["lE"],32,0,0) +n(j,"glJ",0,3,null,["$3"],["lK"],69,0,0) +m(j,"glO","lP",33) +m(j,"glu","lv",33) +k(j,"gls","lt",20) +n(j,"glL",0,4,null,["$4"],["lM"],34,0,0) +n(j,"glW",0,4,null,["$4"],["lX"],34,0,0) +m(j,"glS","lT",73) +m(j,"glQ","lR",12) +m(j,"glB","lC",12) +m(j,"glF","lG",12) +m(j,"glU","lV",12) +m(j,"glq","lr",12) +k(j,"gcq","ly",20) +n(j,"glz",0,3,null,["$3"],["lA"],75,0,0) +k(j,"gdj","lN",20) +k(j,"gkm","kn",9) +k(j,"gkh","ki",76) +n(j,"gkk",0,5,null,["$5"],["kl"],77,0,0) +n(j,"gks",0,4,null,["$4"],["kt"],21,0,0) +n(j,"gkw",0,4,null,["$4"],["kx"],21,0,0) +n(j,"gku",0,4,null,["$4"],["kv"],21,0,0) +m(j,"gky","kz",35) +m(j,"gkq","kr",35) +n(j,"gko",0,5,null,["$5"],["kp"],80,0,0) +m(j,"gkf","kg",81) +m(j,"gkd","ke",82) +n(j,"gkb",0,3,null,["$3"],["kc"],83,0,0) +o(A.dy.prototype,"gc4","n",0) +r(A,"bS","un",122) +r(A,"ba","uo",123) +r(A,"pi","up",124) +k(A.eQ.prototype,"gj8","j9",85) +o(A.d7.prototype,"gc4","n",10) +o(A.dq.prototype,"gc4","n",0) +r(A,"xf","u9",14) +r(A,"rA","u8",14) +r(A,"xd","u6",14) +r(A,"xe","u7",14) +r(A,"xQ","uM",36) +r(A,"xP","uL",36)})();(function inheritance(){var s=hunkHelpers.mixin,r=hunkHelpers.inherit,q=hunkHelpers.inheritMany +r(A.e,null) +q(A.e,[A.on,J.hi,A.eH,J.fL,A.d,A.fU,A.L,A.w,A.cq,A.kN,A.b4,A.dc,A.cG,A.ha,A.hR,A.hM,A.hN,A.h7,A.i8,A.eq,A.en,A.hV,A.hQ,A.fi,A.ef,A.ix,A.lt,A.hD,A.el,A.fm,A.S,A.kv,A.hr,A.db,A.hq,A.cx,A.dJ,A.m5,A.ds,A.ne,A.ml,A.iS,A.be,A.ir,A.nk,A.iP,A.ia,A.iN,A.U,A.X,A.af,A.cI,A.f7,A.dB,A.cg,A.n,A.ib,A.hP,A.cS,A.iO,A.ic,A.dP,A.il,A.mu,A.fh,A.f0,A.dO,A.f2,A.dF,A.av,A.iU,A.dV,A.fz,A.is,A.dp,A.n_,A.dI,A.iz,A.ay,A.iA,A.cr,A.cs,A.ns,A.fy,A.a8,A.iq,A.eh,A.bx,A.mv,A.hE,A.eK,A.ip,A.aF,A.hh,A.aP,A.N,A.dQ,A.aD,A.fv,A.hY,A.b7,A.hb,A.hC,A.mY,A.d4,A.h1,A.hs,A.hB,A.hW,A.ej,A.iC,A.fX,A.h5,A.h4,A.bZ,A.aQ,A.bW,A.c2,A.bp,A.c4,A.bV,A.c5,A.c3,A.bF,A.bI,A.kO,A.fj,A.i7,A.bK,A.bU,A.ec,A.aq,A.ea,A.d2,A.kG,A.ls,A.jM,A.dj,A.kH,A.eC,A.kF,A.bq,A.jN,A.lH,A.h6,A.dm,A.lF,A.kW,A.fY,A.li,A.kD,A.hF,A.c8,A.cn,A.h_,A.l6,A.d3,A.as,A.fS,A.ju,A.iJ,A.n3,A.cw,A.aJ,A.eJ,A.lP,A.lG,A.lR,A.lQ,A.cb,A.bN,A.fZ,A.bG,A.cK,A.lL,A.kL,A.br,A.bC,A.iF,A.eQ,A.dK,A.j6,A.f9,A.mz,A.iB,A.iu,A.n0,A.lA,A.bm,A.M,A.hp,A.a0,A.bt,A.eM,A.f8,A.hO,A.of,A.io]) +q(J.hi,[J.hk,J.et,J.eu,J.aN,J.d9,J.d8,J.bX]) +q(J.eu,[J.bY,J.u,A.de,A.ey]) +q(J.bY,[J.hG,J.cF,J.bz]) +r(J.hj,A.eH) +r(J.kr,J.u) +q(J.d8,[J.es,J.hl]) +q(A.d,[A.ce,A.q,A.aG,A.aK,A.em,A.cE,A.bJ,A.eI,A.eR,A.by,A.cP,A.i9,A.iM,A.dR,A.cy]) +q(A.ce,[A.cp,A.fA]) +r(A.f1,A.cp) +r(A.eW,A.fA) +r(A.ai,A.eW) +q(A.L,[A.da,A.bL,A.hn,A.hU,A.hK,A.im,A.eD,A.fO,A.bc,A.eP,A.hT,A.aI,A.fW]) +q(A.w,[A.du,A.i2,A.dx,A.dt]) +r(A.fV,A.du) +q(A.cq,[A.jg,A.kl,A.jh,A.lj,A.nS,A.nU,A.m7,A.m6,A.nu,A.nf,A.nh,A.ng,A.kd,A.kb,A.mD,A.mC,A.mO,A.lg,A.lf,A.ld,A.lb,A.nd,A.mt,A.ms,A.n8,A.n7,A.mQ,A.kz,A.mi,A.nn,A.nW,A.o0,A.o1,A.nM,A.jT,A.jU,A.jV,A.kT,A.kU,A.kV,A.kR,A.m_,A.lX,A.lY,A.lV,A.m0,A.lZ,A.kI,A.k1,A.nH,A.kt,A.ku,A.ky,A.lS,A.lT,A.jP,A.l1,A.nK,A.nZ,A.jW,A.kM,A.jm,A.jn,A.jo,A.l0,A.kX,A.l_,A.kY,A.kZ,A.js,A.jt,A.nI,A.m4,A.l7,A.o_,A.o3,A.o4,A.j5,A.mo,A.mp,A.jk,A.jl,A.jp,A.jq,A.jr,A.j9,A.j7,A.mS,A.mV,A.mW,A.kk,A.ki,A.mR,A.l4,A.lB,A.lC,A.lD,A.lE,A.ja,A.jf,A.je,A.jc,A.jd,A.jb,A.lp,A.ln,A.lm,A.lk,A.ll,A.lr,A.lq,A.mw,A.mx]) +q(A.jg,[A.nY,A.m8,A.m9,A.nj,A.ni,A.kc,A.mF,A.mK,A.mJ,A.mH,A.mG,A.mN,A.mM,A.mL,A.lh,A.le,A.lc,A.la,A.nc,A.nb,A.mk,A.mj,A.n1,A.nx,A.ny,A.mr,A.mq,A.n6,A.n5,A.nC,A.nr,A.nq,A.jS,A.kP,A.kQ,A.kS,A.m1,A.m2,A.lW,A.o2,A.ma,A.mf,A.md,A.me,A.mc,A.mb,A.n9,A.na,A.jR,A.jQ,A.my,A.kw,A.kx,A.lU,A.jO,A.k_,A.jX,A.jY,A.jZ,A.jK,A.o5,A.jy,A.jv,A.jA,A.jC,A.jE,A.jx,A.jD,A.jI,A.jG,A.jF,A.jz,A.jB,A.jH,A.jw,A.j3,A.j4,A.lM,A.j8,A.mT,A.mU,A.mA,A.kj,A.ka,A.k8,A.k5,A.k6,A.k7,A.lo,A.kg,A.kf]) +q(A.q,[A.Q,A.cv,A.bB,A.ew,A.ev,A.cO,A.fb]) +q(A.Q,[A.cD,A.D,A.eG]) +r(A.cu,A.aG) +r(A.ek,A.cE) +r(A.d5,A.bJ) +r(A.ct,A.by) +r(A.iD,A.fi) +q(A.iD,[A.ag,A.cR,A.iE]) +r(A.eg,A.ef) +r(A.er,A.kl) +r(A.eA,A.bL) +q(A.lj,[A.l9,A.eb]) +q(A.S,[A.bA,A.cN]) +q(A.jh,[A.ks,A.nT,A.nv,A.nJ,A.ke,A.mE,A.mP,A.nw,A.kh,A.kA,A.mh,A.ly,A.lK,A.lJ,A.lI,A.jL,A.mX,A.k9]) +r(A.dd,A.de) +q(A.ey,[A.cz,A.dg]) +q(A.dg,[A.fd,A.ff]) +r(A.fe,A.fd) +r(A.c_,A.fe) +r(A.fg,A.ff) +r(A.aX,A.fg) +q(A.c_,[A.hu,A.hv]) +q(A.aX,[A.hw,A.df,A.hx,A.hy,A.hz,A.ez,A.c0]) +r(A.fq,A.im) +q(A.X,[A.dN,A.f5,A.eU,A.e9,A.eY,A.f3]) +r(A.at,A.dN) +r(A.eV,A.at) +q(A.af,[A.cf,A.dE,A.dL]) +r(A.cJ,A.cf) +r(A.fp,A.cI) +q(A.dB,[A.a7,A.a1]) +q(A.cS,[A.dA,A.dS]) +q(A.il,[A.dD,A.eZ]) +r(A.fc,A.f5) +r(A.fo,A.hP) +r(A.dM,A.fo) +q(A.iU,[A.ij,A.iI]) +r(A.dG,A.cN) +r(A.fk,A.dp) +r(A.fa,A.fk) +q(A.cr,[A.h8,A.fQ]) +q(A.h8,[A.fM,A.i0]) +q(A.cs,[A.iR,A.fR,A.i1]) +r(A.fN,A.iR) +q(A.bc,[A.dk,A.ep]) +r(A.ik,A.fv) +q(A.bZ,[A.ar,A.bf,A.bo,A.bw]) +q(A.mv,[A.dh,A.cC,A.c1,A.dv,A.c7,A.cA,A.cc,A.bO,A.kC,A.ac,A.d6]) +r(A.jJ,A.kG) +r(A.kB,A.ls) +q(A.jM,[A.hA,A.k0]) +q(A.aq,[A.id,A.dH,A.ho]) +q(A.id,[A.iQ,A.h2,A.ie,A.f4]) +r(A.fn,A.iQ) +r(A.iw,A.dH) +r(A.cB,A.jJ) +r(A.fl,A.k0) +q(A.lH,[A.ji,A.dz,A.dn,A.dl,A.eL,A.h3]) +q(A.ji,[A.c6,A.ei]) +r(A.mn,A.kH) +r(A.i4,A.h2) +r(A.iT,A.cB) +r(A.kp,A.li) +q(A.kp,[A.kE,A.lz,A.m3]) +r(A.dr,A.d3) +r(A.fT,A.as) +q(A.fT,[A.he,A.dy,A.d7,A.dq]) +q(A.fS,[A.it,A.i5,A.iL]) +r(A.iG,A.ju) +r(A.iH,A.iG) +r(A.hJ,A.iH) +r(A.iK,A.iJ) +r(A.bs,A.iK) +q(A.ay,[A.cH,A.au]) +r(A.i6,A.l6) +q(A.bC,[A.b2,A.R]) +r(A.aW,A.R) +q(A.au,[A.f6,A.f_,A.dC,A.dU]) +q(A.eM,[A.ee,A.eo]) +r(A.eX,A.d4) +r(A.iv,A.dt) +r(A.bg,A.iv) +s(A.du,A.hV) +s(A.fA,A.w) +s(A.fd,A.w) +s(A.fe,A.en) +s(A.ff,A.w) +s(A.fg,A.en) +s(A.dA,A.ic) +s(A.dS,A.iO) +s(A.iG,A.w) +s(A.iH,A.hB) +s(A.iJ,A.hW) +s(A.iK,A.S)})() +var v={G:typeof self!="undefined"?self:globalThis,typeUniverse:{eC:new Map(),tR:{},eT:{},tPV:{},sEA:[]},mangledGlobalNames:{a:"int",F:"double",b0:"num",p:"String",I:"bool",N:"Null",o:"List",e:"Object",ap:"Map",z:"JSObject"},mangledNames:{},types:["~()","~(z)","I(p)","N()","~(a)","F(b0)","~(e,a_)","~(e?)","p(p)","~(~())","C<~>()","N(z)","a(aA,a)","M()","M(p)","~(@)","e?(e?)","C()","C<~>(f9)","~(z?,o?)","a(aA)","~(bG,a,a,a)","p(a)","@()","p(M)","I(~)","C()","N(@)","b0?(o)","N(e,a_)","~(e[a_?])","I()","a(as,a,a,a)","a(as,a)","a(aA,a,a,aN)","~(bG,a)","a0(p)","a(a)","a(M)","~(ae)","N(@,a_)","~(a,@)","C<~>(ar)","C()","ap(o)","a(o)","a?(a)","N(aq)","C(~)","N(~())","N(~)","bH?/(ar)","I(a)","z(u)","dm()","C()","C()","0&(p,a?)","~(I,I,I,o<+(bO,p)>)","@(p)","p(p?)","p(e?)","~(os,o)","a()","~(v,T,v,~())","~(aN,a)","aA?(as,a,a,a,a)","a(as,a,a)","C()","a(as?,a,a)","bU<@>?()","N(I)","ar()","a(aA,aN)","@(@)","a(aA,a,a)","a(a())","~(~(a,p,a),a,a,a,aN)","~(@,a_)","bf()","a(bG,a,a,a,a)","a(a(a),a)","a(ov,a)","a(ov,a,a)","bp()","~(dK)","~(@,@)","z(z?)","~(co)","C<~>(a,aY)","C<~>(a)","o(u)","C(p)","o(a0)","a(a0)","bK(e?)","p(a0)","C()","~(e?,e?)","M(p,p)","a0()","a(@,@)","I(e?)","~(v?,T?,v,e,a_)","0^(v?,T?,v,0^())","0^(v?,T?,v,0^(1^),1^)","0^(v?,T?,v,0^(1^,2^),1^,2^)","0^()(v,T,v,0^())","0^(1^)(v,T,v,0^(1^))","0^(1^,2^)(v,T,v,0^(1^,2^))","U?(v,T,v,e,a_?)","~(v?,T?,v,~())","eO(v,T,v,bx,~())","eO(v,T,v,bx,~(eO))","~(v,T,v,p)","~(p)","v(v?,T?,v,oE?,ap?)","0^(0^,0^)","@(@,p)","a(a,a)","I?(o)","I?(o<@>)","b2(br)","R(br)","aW(br)","aY()","z()"],interceptorsByTag:null,leafTags:null,arrayRti:Symbol("$ti"),rttc:{"2;":(a,b)=>c=>c instanceof A.ag&&a.b(c.a)&&b.b(c.b),"2;file,outFlags":(a,b)=>c=>c instanceof A.cR&&a.b(c.a)&&b.b(c.b),"2;result,resultCode":(a,b)=>c=>c instanceof A.iE&&a.b(c.a)&&b.b(c.b)}} +A.vp(v.typeUniverse,JSON.parse('{"hG":"bY","cF":"bY","bz":"bY","y2":"de","u":{"o":["1"],"q":["1"],"z":[],"d":["1"],"ax":["1"]},"hk":{"I":[],"K":[]},"et":{"N":[],"K":[]},"eu":{"z":[]},"bY":{"z":[]},"hj":{"eH":[]},"kr":{"u":["1"],"o":["1"],"q":["1"],"z":[],"d":["1"],"ax":["1"]},"d8":{"F":[],"b0":[]},"es":{"F":[],"a":[],"b0":[],"K":[]},"hl":{"F":[],"b0":[],"K":[]},"bX":{"p":[],"ax":["@"],"K":[]},"ce":{"d":["2"]},"cp":{"ce":["1","2"],"d":["2"],"d.E":"2"},"f1":{"cp":["1","2"],"ce":["1","2"],"q":["2"],"d":["2"],"d.E":"2"},"eW":{"w":["2"],"o":["2"],"ce":["1","2"],"q":["2"],"d":["2"]},"ai":{"eW":["1","2"],"w":["2"],"o":["2"],"ce":["1","2"],"q":["2"],"d":["2"],"w.E":"2","d.E":"2"},"da":{"L":[]},"fV":{"w":["a"],"o":["a"],"q":["a"],"d":["a"],"w.E":"a"},"q":{"d":["1"]},"Q":{"q":["1"],"d":["1"]},"cD":{"Q":["1"],"q":["1"],"d":["1"],"d.E":"1","Q.E":"1"},"aG":{"d":["2"],"d.E":"2"},"cu":{"aG":["1","2"],"q":["2"],"d":["2"],"d.E":"2"},"D":{"Q":["2"],"q":["2"],"d":["2"],"d.E":"2","Q.E":"2"},"aK":{"d":["1"],"d.E":"1"},"em":{"d":["2"],"d.E":"2"},"cE":{"d":["1"],"d.E":"1"},"ek":{"cE":["1"],"q":["1"],"d":["1"],"d.E":"1"},"bJ":{"d":["1"],"d.E":"1"},"d5":{"bJ":["1"],"q":["1"],"d":["1"],"d.E":"1"},"eI":{"d":["1"],"d.E":"1"},"cv":{"q":["1"],"d":["1"],"d.E":"1"},"eR":{"d":["1"],"d.E":"1"},"by":{"d":["+(a,1)"],"d.E":"+(a,1)"},"ct":{"by":["1"],"q":["+(a,1)"],"d":["+(a,1)"],"d.E":"+(a,1)"},"du":{"w":["1"],"o":["1"],"q":["1"],"d":["1"]},"eG":{"Q":["1"],"q":["1"],"d":["1"],"d.E":"1","Q.E":"1"},"ef":{"ap":["1","2"]},"eg":{"ef":["1","2"],"ap":["1","2"]},"cP":{"d":["1"],"d.E":"1"},"eA":{"bL":[],"L":[]},"hn":{"L":[]},"hU":{"L":[]},"hD":{"a6":[]},"fm":{"a_":[]},"hK":{"L":[]},"bA":{"S":["1","2"],"ap":["1","2"],"S.V":"2","S.K":"1"},"bB":{"q":["1"],"d":["1"],"d.E":"1"},"ew":{"q":["1"],"d":["1"],"d.E":"1"},"ev":{"q":["aP<1,2>"],"d":["aP<1,2>"],"d.E":"aP<1,2>"},"dJ":{"hI":[],"ex":[]},"i9":{"d":["hI"],"d.E":"hI"},"ds":{"ex":[]},"iM":{"d":["ex"],"d.E":"ex"},"dd":{"z":[],"co":[],"K":[]},"cz":{"oc":[],"z":[],"K":[]},"df":{"aX":[],"kn":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"c0":{"aX":[],"aY":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"de":{"z":[],"co":[],"K":[]},"ey":{"z":[]},"iS":{"co":[]},"dg":{"aV":["1"],"z":[],"ax":["1"]},"c_":{"w":["F"],"o":["F"],"aV":["F"],"q":["F"],"z":[],"ax":["F"],"d":["F"]},"aX":{"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"]},"hu":{"c_":[],"k3":[],"w":["F"],"o":["F"],"aV":["F"],"q":["F"],"z":[],"ax":["F"],"d":["F"],"K":[],"w.E":"F"},"hv":{"c_":[],"k4":[],"w":["F"],"o":["F"],"aV":["F"],"q":["F"],"z":[],"ax":["F"],"d":["F"],"K":[],"w.E":"F"},"hw":{"aX":[],"km":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"hx":{"aX":[],"ko":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"hy":{"aX":[],"lv":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"hz":{"aX":[],"lw":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"ez":{"aX":[],"lx":[],"w":["a"],"o":["a"],"aV":["a"],"q":["a"],"z":[],"ax":["a"],"d":["a"],"K":[],"w.E":"a"},"im":{"L":[]},"fq":{"bL":[],"L":[]},"U":{"L":[]},"af":{"af.T":"1"},"dF":{"ae":["1"]},"dR":{"d":["1"],"d.E":"1"},"eV":{"at":["1"],"dN":["1"],"X":["1"],"X.T":"1"},"cJ":{"cf":["1"],"af":["1"],"af.T":"1"},"cI":{"ae":["1"]},"fp":{"cI":["1"],"ae":["1"]},"eD":{"L":[]},"a7":{"dB":["1"]},"a1":{"dB":["1"]},"n":{"C":["1"]},"cS":{"ae":["1"]},"dA":{"cS":["1"],"ae":["1"]},"dS":{"cS":["1"],"ae":["1"]},"at":{"dN":["1"],"X":["1"],"X.T":"1"},"cf":{"af":["1"],"af.T":"1"},"dP":{"ae":["1"]},"dN":{"X":["1"]},"f5":{"X":["2"]},"dE":{"af":["2"],"af.T":"2"},"fc":{"f5":["1","2"],"X":["2"],"X.T":"2"},"f2":{"ae":["1"]},"dL":{"af":["2"],"af.T":"2"},"eU":{"X":["2"],"X.T":"2"},"dM":{"fo":["1","2"]},"iU":{"v":[]},"ij":{"v":[]},"iI":{"v":[]},"dV":{"T":[]},"fz":{"oE":[]},"cN":{"S":["1","2"],"ap":["1","2"],"S.V":"2","S.K":"1"},"dG":{"cN":["1","2"],"S":["1","2"],"ap":["1","2"],"S.V":"2","S.K":"1"},"cO":{"q":["1"],"d":["1"],"d.E":"1"},"fa":{"fk":["1"],"dp":["1"],"q":["1"],"d":["1"]},"cy":{"d":["1"],"d.E":"1"},"w":{"o":["1"],"q":["1"],"d":["1"]},"S":{"ap":["1","2"]},"fb":{"q":["2"],"d":["2"],"d.E":"2"},"dp":{"q":["1"],"d":["1"]},"fk":{"dp":["1"],"q":["1"],"d":["1"]},"fM":{"cr":["p","o"]},"iR":{"cs":["p","o"]},"fN":{"cs":["p","o"]},"fQ":{"cr":["o","p"]},"fR":{"cs":["o","p"]},"h8":{"cr":["p","o"]},"i0":{"cr":["p","o"]},"i1":{"cs":["p","o"]},"F":{"b0":[]},"a":{"b0":[]},"o":{"q":["1"],"d":["1"]},"hI":{"ex":[]},"fO":{"L":[]},"bL":{"L":[]},"bc":{"L":[]},"dk":{"L":[]},"ep":{"L":[]},"eP":{"L":[]},"hT":{"L":[]},"aI":{"L":[]},"fW":{"L":[]},"hE":{"L":[]},"eK":{"L":[]},"ip":{"a6":[]},"aF":{"a6":[]},"hh":{"a6":[],"L":[]},"dQ":{"a_":[]},"fv":{"hX":[]},"b7":{"hX":[]},"ik":{"hX":[]},"hC":{"a6":[]},"d4":{"ae":["1"]},"fX":{"a6":[]},"h5":{"a6":[]},"ar":{"bZ":[]},"bf":{"bZ":[]},"bp":{"az":[]},"bF":{"az":[]},"aQ":{"bH":[]},"bo":{"bZ":[]},"bw":{"bZ":[]},"dh":{"az":[]},"bW":{"az":[]},"c2":{"az":[]},"c4":{"az":[]},"bV":{"az":[]},"c5":{"az":[]},"c3":{"az":[]},"bI":{"bH":[]},"ec":{"a6":[]},"id":{"aq":[]},"iQ":{"hS":[],"aq":[]},"fn":{"hS":[],"aq":[]},"h2":{"aq":[]},"ie":{"aq":[]},"f4":{"aq":[]},"dH":{"aq":[]},"iw":{"hS":[],"aq":[]},"ho":{"aq":[]},"dz":{"a6":[]},"i4":{"aq":[]},"iT":{"cB":["od"],"cB.0":"od"},"hF":{"a6":[]},"c8":{"a6":[]},"h_":{"od":[]},"i2":{"w":["e?"],"o":["e?"],"q":["e?"],"d":["e?"],"w.E":"e?"},"dr":{"d3":[]},"he":{"as":[]},"it":{"dw":[],"aA":[]},"bs":{"S":["p","@"],"ap":["p","@"],"S.V":"@","S.K":"p"},"hJ":{"w":["bs"],"o":["bs"],"q":["bs"],"d":["bs"],"w.E":"bs"},"aJ":{"a6":[]},"fT":{"as":[]},"fS":{"dw":[],"aA":[]},"cH":{"ay":["cH"],"ay.E":"cH"},"bN":{"ot":[]},"cb":{"os":[]},"dx":{"w":["bN"],"o":["bN"],"q":["bN"],"d":["bN"],"w.E":"bN"},"e9":{"X":["1"],"X.T":"1"},"dy":{"as":[]},"i5":{"dw":[],"aA":[]},"b2":{"bC":[]},"R":{"bC":[]},"aW":{"R":[],"bC":[]},"d7":{"as":[]},"au":{"ay":["au"]},"iu":{"dw":[],"aA":[]},"f6":{"au":[],"ay":["au"],"ay.E":"au"},"f_":{"au":[],"ay":["au"],"ay.E":"au"},"dC":{"au":[],"ay":["au"],"ay.E":"au"},"dU":{"au":[],"ay":["au"],"ay.E":"au"},"dq":{"as":[]},"iL":{"dw":[],"aA":[]},"bm":{"a_":[]},"hp":{"a0":[],"a_":[]},"a0":{"a_":[]},"bt":{"M":[]},"ee":{"eM":["1"]},"eY":{"X":["1"],"X.T":"1"},"eX":{"ae":["1"]},"eo":{"eM":["1"]},"f8":{"ae":["1"]},"bg":{"dt":["a"],"w":["a"],"o":["a"],"q":["a"],"d":["a"],"w.E":"a"},"dt":{"w":["1"],"o":["1"],"q":["1"],"d":["1"]},"iv":{"dt":["a"],"w":["a"],"o":["a"],"q":["a"],"d":["a"]},"f3":{"X":["1"],"X.T":"1"},"ko":{"o":["a"],"q":["a"],"d":["a"]},"aY":{"o":["a"],"q":["a"],"d":["a"]},"lx":{"o":["a"],"q":["a"],"d":["a"]},"km":{"o":["a"],"q":["a"],"d":["a"]},"lv":{"o":["a"],"q":["a"],"d":["a"]},"kn":{"o":["a"],"q":["a"],"d":["a"]},"lw":{"o":["a"],"q":["a"],"d":["a"]},"k3":{"o":["F"],"q":["F"],"d":["F"]},"k4":{"o":["F"],"q":["F"],"d":["F"]}}')) +A.vo(v.typeUniverse,JSON.parse('{"cG":1,"hM":1,"hN":1,"h7":1,"eq":1,"en":1,"hV":1,"du":1,"fA":2,"hr":1,"db":1,"dg":1,"ae":1,"iN":1,"eD":2,"hP":2,"iO":1,"ic":1,"dP":1,"il":1,"dD":1,"fh":1,"f0":1,"dO":1,"f2":1,"av":1,"hb":1,"d4":1,"h1":1,"hs":1,"hB":1,"hW":2,"tL":1,"eX":1,"f8":1,"io":1}')) +var u={v:"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\u03f6\x00\u0404\u03f4 \u03f4\u03f6\u01f6\u01f6\u03f6\u03fc\u01f4\u03ff\u03ff\u0584\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u05d4\u01f4\x00\u01f4\x00\u0504\u05c4\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u0400\x00\u0400\u0200\u03f7\u0200\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u0200\u0200\u0200\u03f7\x00",q:"===== asynchronous gap ===========================\n",l:"Cannot extract a file path from a URI with a fragment component",y:"Cannot extract a file path from a URI with a query component",j:"Cannot extract a non-Windows file path from a file URI with an authority",o:"Cannot fire new event. Controller is already firing an event",c:"Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type",D:"Tried to operate on a released prepared statement"} +var t=(function rtii(){var s=A.aB +return{b9:s("tL"),cO:s("e9>"),w:s("co"),fd:s("oc"),g1:s("bU<@>"),eT:s("d3"),ed:s("ei"),gw:s("ej"),Q:s("q<@>"),p:s("b2"),C:s("L"),g8:s("a6"),G:s("R"),h4:s("k3"),gN:s("k4"),B:s("M"),b8:s("y_"),aQ:s("C"),bF:s("C"),cG:s("C"),eY:s("C"),bd:s("d7"),dQ:s("km"),an:s("kn"),gj:s("ko"),hf:s("d<@>"),b:s("u"),cf:s("u"),e:s("u"),fG:s("u>"),fk:s("u>"),W:s("u"),gP:s("u>"),gz:s("u>"),d:s("u>"),f:s("u"),L:s("u<+(bO,p)>"),bb:s("u"),s:s("u

"),j:s("o<@>"),I:s("o"),ee:s("o"),g6:s("ap"),eO:s("ap<@,@>"),M:s("aG"),fe:s("D"),do:s("D"),fJ:s("bZ"),cb:s("bC"),eN:s("aW"),u:s("dd"),gT:s("cz"),ha:s("df"),aV:s("c_"),eB:s("aX"),Z:s("c0"),bw:s("bF"),P:s("N"),K:s("e"),x:s("aq"),aj:s("dj"),fl:s("y4"),bQ:s("+()"),e1:s("+(z?,z)"),cV:s("+(e?,a)"),cz:s("hI"),al:s("ar"),cc:s("bH"),bJ:s("eG

"),fE:s("dm"),fL:s("c6"),gW:s("dq"),f_:s("c8"),l:s("a_"),a7:s("hO"),N:s("p"),aF:s("eO"),a:s("a0"),v:s("hS"),dm:s("K"),eK:s("bL"),h7:s("lv"),bv:s("lw"),fQ:s("bg"),go:s("lx"),E:s("aY"),ak:s("cF"),dD:s("hX"),ei:s("eQ"),gh:s("dw"),ab:s("i6"),aT:s("dy"),U:s("aK

"),eJ:s("eR

"),R:s("ac"),dx:s("ac"),b0:s("ac"),bi:s("a7"),co:s("a7"),fu:s("a7"),h:s("a7<~>"),V:s("cK"),fF:s("f3"),et:s("n"),a9:s("n"),k:s("n"),eI:s("n<@>"),gR:s("n"),fX:s("n"),D:s("n<~>"),hg:s("dG"),cT:s("dK"),aR:s("iC"),eg:s("iF"),dn:s("fp<~>"),eC:s("a1"),fa:s("a1"),F:s("a1<~>"),y:s("I"),i:s("F"),z:s("@"),bI:s("@(e)"),_:s("@(e,a_)"),S:s("a"),eH:s("C?"),A:s("z?"),dE:s("c0?"),X:s("e?"),ah:s("az?"),O:s("bH?"),dk:s("p?"),fN:s("bg?"),aD:s("aY?"),a6:s("I?"),cD:s("F?"),h6:s("a?"),cg:s("b0?"),o:s("b0"),H:s("~"),d5:s("~(e)"),da:s("~(e,a_)")}})();(function constants(){var s=hunkHelpers.makeConstList +B.as=J.hi.prototype +B.c=J.u.prototype +B.b=J.es.prototype +B.at=J.d8.prototype +B.a=J.bX.prototype +B.au=J.bz.prototype +B.av=J.eu.prototype +B.aF=A.cz.prototype +B.e=A.c0.prototype +B.T=J.hG.prototype +B.A=J.cF.prototype +B.ab=new A.cn(0) +B.k=new A.cn(1) +B.n=new A.cn(2) +B.D=new A.cn(3) +B.bv=new A.cn(-1) +B.ac=new A.fN(127) +B.u=new A.er(A.xx(),A.aB("er")) +B.ad=new A.fM() +B.bw=new A.fR() +B.ae=new A.fQ() +B.E=new A.ec() +B.af=new A.fX() +B.bx=new A.h1() +B.F=new A.h4() +B.G=new A.h7() +B.h=new A.b2() +B.ag=new A.hh() +B.H=function getTagFallback(o) { + var s = Object.prototype.toString.call(o); + return s.substring(8, s.length - 1); +} +B.ah=function() { + var toStringFunction = Object.prototype.toString; + function getTag(o) { + var s = toStringFunction.call(o); + return s.substring(8, s.length - 1); + } + function getUnknownTag(object, tag) { + if (/^HTML[A-Z].*Element$/.test(tag)) { + var name = toStringFunction.call(object); + if (name == "[object Object]") return null; + return "HTMLElement"; + } + } + function getUnknownTagGenericBrowser(object, tag) { + if (object instanceof HTMLElement) return "HTMLElement"; + return getUnknownTag(object, tag); + } + function prototypeForTag(tag) { + if (typeof window == "undefined") return null; + if (typeof window[tag] == "undefined") return null; + var constructor = window[tag]; + if (typeof constructor != "function") return null; + return constructor.prototype; + } + function discriminator(tag) { return null; } + var isBrowser = typeof HTMLElement == "function"; + return { + getTag: getTag, + getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag, + prototypeForTag: prototypeForTag, + discriminator: discriminator }; +} +B.am=function(getTagFallback) { + return function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("DumpRenderTree") >= 0) return hooks; + if (userAgent.indexOf("Chrome") >= 0) { + function confirm(p) { + return typeof window == "object" && window[p] && window[p].name == p; + } + if (confirm("Window") && confirm("HTMLElement")) return hooks; + } + hooks.getTag = getTagFallback; + }; +} +B.ai=function(hooks) { + if (typeof dartExperimentalFixupGetTag != "function") return hooks; + hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag); +} +B.al=function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("Firefox") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "GeoGeolocation": "Geolocation", + "Location": "!Location", + "WorkerMessageEvent": "MessageEvent", + "XMLDocument": "!Document"}; + function getTagFirefox(o) { + var tag = getTag(o); + return quickMap[tag] || tag; + } + hooks.getTag = getTagFirefox; +} +B.ak=function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("Trident/") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "HTMLDDElement": "HTMLElement", + "HTMLDTElement": "HTMLElement", + "HTMLPhraseElement": "HTMLElement", + "Position": "Geoposition" + }; + function getTagIE(o) { + var tag = getTag(o); + var newTag = quickMap[tag]; + if (newTag) return newTag; + if (tag == "Object") { + if (window.DataView && (o instanceof window.DataView)) return "DataView"; + } + return tag; + } + function prototypeForTagIE(tag) { + var constructor = window[tag]; + if (constructor == null) return null; + return constructor.prototype; + } + hooks.getTag = getTagIE; + hooks.prototypeForTag = prototypeForTagIE; +} +B.aj=function(hooks) { + var getTag = hooks.getTag; + var prototypeForTag = hooks.prototypeForTag; + function getTagFixed(o) { + var tag = getTag(o); + if (tag == "Document") { + if (!!o.xmlVersion) return "!Document"; + return "!HTMLDocument"; + } + return tag; + } + function prototypeForTagFixed(tag) { + if (tag == "Document") return null; + return prototypeForTag(tag); + } + hooks.getTag = getTagFixed; + hooks.prototypeForTag = prototypeForTagFixed; +} +B.I=function(hooks) { return hooks; } + +B.m=new A.hs() +B.an=new A.kB() +B.ao=new A.hA() +B.ap=new A.hE() +B.f=new A.kN() +B.j=new A.i0() +B.i=new A.i1() +B.v=new A.mu() +B.d=new A.iI() +B.J=new A.bx(0) +B.K=new A.d6("/database",0,"database") +B.L=new A.d6("/database-journal",1,"journal") +B.aq=new A.aF("Unknown tag",null,null) +B.ar=new A.aF("Cannot read message",null,null) +B.aw=s([11],t.t) +B.C=new A.bO(0,"opfs") +B.W=new A.cc(0,"opfsShared") +B.X=new A.cc(1,"opfsLocks") +B.Y=new A.bO(1,"indexedDb") +B.r=new A.cc(2,"sharedIndexedDb") +B.B=new A.cc(3,"unsafeIndexedDb") +B.bg=new A.cc(4,"inMemory") +B.ax=s([B.W,B.X,B.r,B.B,B.bg],A.aB("u")) +B.b6=new A.dv(0,"insert") +B.b7=new A.dv(1,"update") +B.b8=new A.dv(2,"delete") +B.M=s([B.b6,B.b7,B.b8],A.aB("u")) +B.ay=s([B.C,B.Y],A.aB("u")) +B.w=s([],t.W) +B.az=s([],t.gz) +B.aA=s([],t.f) +B.x=s([],t.s) +B.o=s([],t.c) +B.y=s([],t.L) +B.aC=s([B.K,B.L],A.aB("u")) +B.Z=new A.ac(A.pi(),A.ba(),0,"xAccess",t.b0) +B.a_=new A.ac(A.pi(),A.bS(),1,"xDelete",A.aB("ac")) +B.aa=new A.ac(A.pi(),A.ba(),2,"xOpen",t.b0) +B.a8=new A.ac(A.ba(),A.ba(),3,"xRead",t.dx) +B.a3=new A.ac(A.ba(),A.bS(),4,"xWrite",t.R) +B.a4=new A.ac(A.ba(),A.bS(),5,"xSleep",t.R) +B.a5=new A.ac(A.ba(),A.bS(),6,"xClose",t.R) +B.a9=new A.ac(A.ba(),A.ba(),7,"xFileSize",t.dx) +B.a6=new A.ac(A.ba(),A.bS(),8,"xSync",t.R) +B.a7=new A.ac(A.ba(),A.bS(),9,"xTruncate",t.R) +B.a1=new A.ac(A.ba(),A.bS(),10,"xLock",t.R) +B.a2=new A.ac(A.ba(),A.bS(),11,"xUnlock",t.R) +B.a0=new A.ac(A.bS(),A.bS(),12,"stopServer",A.aB("ac")) +B.aD=s([B.Z,B.a_,B.aa,B.a8,B.a3,B.a4,B.a5,B.a9,B.a6,B.a7,B.a1,B.a2,B.a0],A.aB("u>")) +B.l=new A.c7(0,"sqlite") +B.aN=new A.c7(1,"mysql") +B.aO=new A.c7(2,"postgres") +B.aP=new A.c7(3,"duckdb") +B.aQ=new A.c7(4,"mariadb") +B.N=s([B.l,B.aN,B.aO,B.aP,B.aQ],A.aB("u")) +B.aR=new A.cC(0,"custom") +B.aS=new A.cC(1,"deleteOrUpdate") +B.aT=new A.cC(2,"insert") +B.aU=new A.cC(3,"select") +B.O=s([B.aR,B.aS,B.aT,B.aU],A.aB("u")) +B.Q=new A.c1(0,"beginTransaction") +B.aG=new A.c1(1,"commit") +B.aH=new A.c1(2,"rollback") +B.R=new A.c1(3,"startExclusive") +B.S=new A.c1(4,"endExclusive") +B.P=s([B.Q,B.aG,B.aH,B.R,B.S],A.aB("u")) +B.aI={} +B.aE=new A.eg(B.aI,[],A.aB("eg")) +B.z=new A.dh(0,"terminateAll") +B.by=new A.kC(2,"readWriteCreate") +B.p=new A.cA(0,0,"legacy") +B.aJ=new A.cA(1,1,"v1") +B.aK=new A.cA(2,2,"v2") +B.aL=new A.cA(3,3,"v3") +B.q=new A.cA(4,4,"v4") +B.aB=s([],t.d) +B.aM=new A.bI(B.aB) +B.U=new A.hQ("drift.runtime.cancellation") +B.aV=A.bl("co") +B.aW=A.bl("oc") +B.aX=A.bl("k3") +B.aY=A.bl("k4") +B.aZ=A.bl("km") +B.b_=A.bl("kn") +B.b0=A.bl("ko") +B.b1=A.bl("e") +B.b2=A.bl("lv") +B.b3=A.bl("lw") +B.b4=A.bl("lx") +B.b5=A.bl("aY") +B.b9=new A.aJ(10) +B.ba=new A.aJ(12) +B.bb=new A.aJ(14) +B.bc=new A.aJ(2570) +B.bd=new A.aJ(3850) +B.be=new A.aJ(522) +B.V=new A.aJ(778) +B.bf=new A.aJ(8) +B.t=new A.dQ("") +B.bh=new A.av(B.d,A.wV()) +B.bi=new A.av(B.d,A.wR()) +B.bj=new A.av(B.d,A.wZ()) +B.bk=new A.av(B.d,A.wS()) +B.bl=new A.av(B.d,A.wT()) +B.bm=new A.av(B.d,A.wU()) +B.bn=new A.av(B.d,A.wW()) +B.bo=new A.av(B.d,A.wY()) +B.bp=new A.av(B.d,A.x_()) +B.bq=new A.av(B.d,A.x0()) +B.br=new A.av(B.d,A.x1()) +B.bs=new A.av(B.d,A.x2()) +B.bt=new A.av(B.d,A.wX()) +B.bu=new A.fz(null,null,null,null,null,null,null,null,null,null,null,null,null)})();(function staticFields(){$.mZ=null +$.cU=A.f([],t.f) +$.rk=null +$.pZ=null +$.py=null +$.px=null +$.rD=null +$.rv=null +$.rL=null +$.nO=null +$.nV=null +$.p8=null +$.n2=A.f([],A.aB("u?>")) +$.dZ=null +$.fD=null +$.fE=null +$.oZ=!1 +$.m=B.d +$.n4=null +$.qy=null +$.qz=null +$.qA=null +$.qB=null +$.oF=A.mm("_lastQuoRemDigits") +$.oG=A.mm("_lastQuoRemUsed") +$.eT=A.mm("_lastRemUsed") +$.oH=A.mm("_lastRem_nsh") +$.qr="" +$.qs=null +$.r8=null +$.nz=null})();(function lazyInitializers(){var s=hunkHelpers.lazyFinal,r=hunkHelpers.lazy +s($,"xW","rR",()=>A.rC("_$dart_dartClosure")) +s($,"xV","cZ",()=>A.rC("_$dart_dartClosure_dartJSInterop")) +s($,"z_","tA",()=>B.d.bd(new A.nY(),A.aB("C<~>"))) +s($,"yM","tr",()=>A.f([new J.hj()],A.aB("u"))) +s($,"ya","rX",()=>A.bM(A.lu({ +toString:function(){return"$receiver$"}}))) +s($,"yb","rY",()=>A.bM(A.lu({$method$:null, +toString:function(){return"$receiver$"}}))) +s($,"yc","rZ",()=>A.bM(A.lu(null))) +s($,"yd","t_",()=>A.bM(function(){var $argumentsExpr$="$arguments$" +try{null.$method$($argumentsExpr$)}catch(q){return q.message}}())) +s($,"yg","t2",()=>A.bM(A.lu(void 0))) +s($,"yh","t3",()=>A.bM(function(){var $argumentsExpr$="$arguments$" +try{(void 0).$method$($argumentsExpr$)}catch(q){return q.message}}())) +s($,"yf","t1",()=>A.bM(A.qn(null))) +s($,"ye","t0",()=>A.bM(function(){try{null.$method$}catch(q){return q.message}}())) +s($,"yj","t5",()=>A.bM(A.qn(void 0))) +s($,"yi","t4",()=>A.bM(function(){try{(void 0).$method$}catch(q){return q.message}}())) +s($,"ym","pm",()=>A.uW()) +s($,"y1","cm",()=>$.tA()) +s($,"y0","rU",()=>A.v7(!1,B.d,t.y)) +s($,"yw","tc",()=>{var q=t.z +return A.pN(q,q)}) +s($,"yA","tg",()=>A.pW(4096)) +s($,"yy","te",()=>new A.nr().$0()) +s($,"yz","tf",()=>new A.nq().$0()) +s($,"yn","t7",()=>A.uq(A.fC(A.f([-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1,-2,-2,-2,-2,-2,62,-2,62,-2,63,52,53,54,55,56,57,58,59,60,61,-2,-2,-2,-1,-2,-2,-2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-2,-2,-2,-2,63,-2,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-2,-2,-2,-2,-2],t.t)))) +s($,"yu","bb",()=>A.eS(0)) +s($,"ys","d_",()=>A.eS(1)) +s($,"yt","ta",()=>A.eS(2)) +s($,"yq","po",()=>$.d_().aj(0)) +s($,"yo","pn",()=>A.eS(1e4)) +r($,"yr","t9",()=>A.G("^\\s*([+-]?)((0x[a-f0-9]+)|(\\d+)|([a-z0-9]+))\\s*$",!1,!1,!1,!1)) +s($,"yp","t8",()=>A.pW(8)) +s($,"yv","tb",()=>typeof FinalizationRegistry=="function"?FinalizationRegistry:null) +s($,"yx","td",()=>A.G("^[\\-\\.0-9A-Z_a-z~]*$",!0,!1,!1,!1)) +s($,"yJ","o7",()=>A.pb(B.b1)) +s($,"y3","rV",()=>{var q=new A.mY(new DataView(new ArrayBuffer(A.vW(8)))) +q.i1() +return q}) +s($,"yl","pl",()=>A.u_(B.ay,A.aB("bO"))) +s($,"z1","tB",()=>A.pB($.fK())) +s($,"yV","pp",()=>new A.fY($.pk(),null)) +s($,"y7","rW",()=>new A.kE(A.G("/",!0,!1,!1,!1),A.G("[^/]$",!0,!1,!1,!1),A.G("^/",!0,!1,!1,!1))) +s($,"y9","fK",()=>new A.m3(A.G("[/\\\\]",!0,!1,!1,!1),A.G("[^/\\\\]$",!0,!1,!1,!1),A.G("^(\\\\\\\\[^\\\\]+\\\\[^\\\\/]+|[a-zA-Z]:[/\\\\])",!0,!1,!1,!1),A.G("^[/\\\\](?![/\\\\])",!0,!1,!1,!1))) +s($,"y8","fJ",()=>new A.lz(A.G("/",!0,!1,!1,!1),A.G("(^[a-zA-Z][-+.a-zA-Z\\d]*://|[^/])$",!0,!1,!1,!1),A.G("[a-zA-Z][-+.a-zA-Z\\d]*://[^/]*",!0,!1,!1,!1),A.G("^/",!0,!1,!1,!1))) +s($,"y6","pk",()=>A.uG()) +s($,"xU","rQ",()=>$.d_().aE(0,63).aj(0)) +s($,"xT","rP",()=>{var q=$.d_() +return q.aE(0,63).cw(0,q)}) +s($,"xS","fI",()=>$.rV()) +s($,"yk","t6",()=>new A.hb(new WeakMap())) +s($,"yN","ts",()=>A.ul(A.f([A.qf("files"),A.qf("blocks")],t.s))) +s($,"xX","o6",()=>{var q,p,o=A.ao(t.N,A.aB("d6")) +for(q=0;q<2;++q){p=B.aC[q] +o.t(0,p.c,p)}return o}) +s($,"yU","tz",()=>A.G("^#\\d+\\s+(\\S.*) \\((.+?)((?::\\d+){0,2})\\)$",!0,!1,!1,!1)) +s($,"yP","tu",()=>A.G("^\\s*at (?:(\\S.*?)(?: \\[as [^\\]]+\\])? \\((.*)\\)|(.*))$",!0,!1,!1,!1)) +s($,"yQ","tv",()=>A.G("^(.*?):(\\d+)(?::(\\d+))?$|native$",!0,!1,!1,!1)) +s($,"yT","ty",()=>A.G("^\\s*at (?:(?.+) )?(?:\\(?(?:(?\\S+):wasm-function\\[(?\\d+)\\]\\:0x(?[0-9a-fA-F]+))\\)?)$",!0,!1,!1,!1)) +s($,"yO","tt",()=>A.G("^eval at (?:\\S.*?) \\((.*)\\)(?:, .*?:\\d+:\\d+)?$",!0,!1,!1,!1)) +s($,"yC","ti",()=>A.G("(\\S+)@(\\S+) line (\\d+) >.* (Function|eval):\\d+:\\d+",!0,!1,!1,!1)) +s($,"yE","tk",()=>A.G("^(?:([^@(/]*)(?:\\(.*\\))?((?:/[^/]*)*)(?:\\(.*\\))?@)?(.*?):(\\d*)(?::(\\d*))?$",!0,!1,!1,!1)) +s($,"yG","tm",()=>A.G("^(?.*?)@(?:(?\\S+).*?:wasm-function\\[(?\\d+)\\]:0x(?[0-9a-fA-F]+))$",!0,!1,!1,!1)) +s($,"yL","tq",()=>A.G("^.*?wasm-function\\[(?.*)\\]@\\[wasm code\\]$",!0,!1,!1,!1)) +s($,"yH","tn",()=>A.G("^(\\S+)(?: (\\d+)(?::(\\d+))?)?\\s+([^\\d].*)$",!0,!1,!1,!1)) +s($,"yB","th",()=>A.G("<(|[^>]+)_async_body>",!0,!1,!1,!1)) +s($,"yK","tp",()=>A.G("^\\.",!0,!1,!1,!1)) +s($,"xY","rS",()=>A.G("^[a-zA-Z][-+.a-zA-Z\\d]*://",!0,!1,!1,!1)) +s($,"xZ","rT",()=>A.G("^([a-zA-Z]:[\\\\/]|\\\\\\\\)",!0,!1,!1,!1)) +s($,"yR","tw",()=>A.G("\\n ?at ",!0,!1,!1,!1)) +s($,"yS","tx",()=>A.G(" ?at ",!0,!1,!1,!1)) +s($,"yD","tj",()=>A.G("@\\S+ line \\d+ >.* (Function|eval):\\d+:\\d+",!0,!1,!1,!1)) +s($,"yF","tl",()=>A.G("^(([.0-9A-Za-z_$/<]|\\(.*\\))*@)?[^\\s]*:\\d*$",!0,!1,!0,!1)) +s($,"yI","to",()=>A.G("^[^\\s<][^\\s]*( \\d+(:\\d+)?)?[ \\t]+[^\\s]+$",!0,!1,!0,!1)) +s($,"z0","pq",()=>A.G("^\\n?$",!0,!1,!0,!1))})();(function nativeSupport(){!function(){var s=function(a){var m={} +m[a]=1 +return Object.keys(hunkHelpers.convertToFastObject(m))[0]} +v.getIsolateTag=function(a){return s("___dart_"+a+v.isolateTag)} +var r="___dart_isolate_tags_" +var q=Object[r]||(Object[r]=Object.create(null)) +var p="_ZxYxX" +for(var o=0;;o++){var n=s(p+"_"+o+"_") +if(!(n in q)){q[n]=1 +v.isolateTag=n +break}}v.dispatchPropertyName=v.getIsolateTag("dispatch_record")}() +hunkHelpers.setOrUpdateInterceptorsByTag({SharedArrayBuffer:A.de,ArrayBuffer:A.dd,ArrayBufferView:A.ey,DataView:A.cz,Float32Array:A.hu,Float64Array:A.hv,Int16Array:A.hw,Int32Array:A.df,Int8Array:A.hx,Uint16Array:A.hy,Uint32Array:A.hz,Uint8ClampedArray:A.ez,CanvasPixelArray:A.ez,Uint8Array:A.c0}) +hunkHelpers.setOrUpdateLeafTags({SharedArrayBuffer:true,ArrayBuffer:true,ArrayBufferView:false,DataView:true,Float32Array:true,Float64Array:true,Int16Array:true,Int32Array:true,Int8Array:true,Uint16Array:true,Uint32Array:true,Uint8ClampedArray:true,CanvasPixelArray:true,Uint8Array:false}) +A.dg.$nativeSuperclassTag="ArrayBufferView" +A.fd.$nativeSuperclassTag="ArrayBufferView" +A.fe.$nativeSuperclassTag="ArrayBufferView" +A.c_.$nativeSuperclassTag="ArrayBufferView" +A.ff.$nativeSuperclassTag="ArrayBufferView" +A.fg.$nativeSuperclassTag="ArrayBufferView" +A.aX.$nativeSuperclassTag="ArrayBufferView"})() +Function.prototype.$0=function(){return this()} +Function.prototype.$1=function(a){return this(a)} +Function.prototype.$2=function(a,b){return this(a,b)} +Function.prototype.$1$1=function(a){return this(a)} +Function.prototype.$3=function(a,b,c){return this(a,b,c)} +Function.prototype.$4=function(a,b,c,d){return this(a,b,c,d)} +Function.prototype.$3$1=function(a){return this(a)} +Function.prototype.$2$1=function(a){return this(a)} +Function.prototype.$3$3=function(a,b,c){return this(a,b,c)} +Function.prototype.$2$2=function(a,b){return this(a,b)} +Function.prototype.$2$3=function(a,b,c){return this(a,b,c)} +Function.prototype.$1$2=function(a,b){return this(a,b)} +Function.prototype.$5=function(a,b,c,d,e){return this(a,b,c,d,e)} +Function.prototype.$6=function(a,b,c,d,e,f){return this(a,b,c,d,e,f)} +Function.prototype.$1$0=function(){return this()} +convertAllToFastObject(w) +convertToFastObject($);(function(a){if(typeof document==="undefined"){a(null) +return}if(typeof document.currentScript!="undefined"){a(document.currentScript) +return}var s=document.scripts +function onLoad(b){for(var q=0;q runApp(const _ProbeApp()); + +class _ProbeApp extends StatelessWidget { + const _ProbeApp(); + + @override + Widget build(BuildContext context) => const MaterialApp( + home: Scaffold(body: Center(child: _Probe())), + ); +} + +class _Probe extends StatefulWidget { + const _Probe(); + + @override + State<_Probe> createState() => _ProbeState(); +} + +class _ProbeState extends State<_Probe> { + String _status = 'running…'; + + @override + void initState() { + super.initState(); + _run(); + } + + Future _run() async { + final lines = []; + OffbandDatabase? db; + try { + db = OffbandDatabase(); + + final ok = await db.verifyReadWrite(); + final r1 = ok + ? 'PROBE-OK open+write+read' + : 'PROBE-FAIL readback mismatch'; + lines.add(r1); + // ignore: avoid_print + print(r1); + + // The point of the migration: exceed the 5 MiB localStorage ceiling. + final big = 'x' * (6 * 1024 * 1024); + await db + .into(db.storedBlobs) + .insertOnConflictUpdate( + StoredBlobsCompanion.insert(key: 'big', value: big), + ); + final row = await (db.select( + db.storedBlobs, + )..where((t) => t.key.equals('big'))).getSingle(); + final r2 = row.value.length == big.length + ? 'PROBE-OK 6MiB round-trip (localStorage cap is 5MiB)' + : 'PROBE-FAIL 6MiB truncated to ${row.value.length}'; + lines.add(r2); + // ignore: avoid_print + print(r2); + + await (db.delete(db.storedBlobs)..where((t) => t.key.equals('big'))).go(); + } catch (e) { + lines.add('PROBE-FAIL $e'); + // ignore: avoid_print + print('PROBE-FAIL $e'); + } finally { + await db?.close(); + } + if (mounted) setState(() => _status = lines.join('\n')); + } + + @override + Widget build(BuildContext context) => Text( + _status, + key: const Key('probe-result'), + textAlign: TextAlign.center, + ); +} diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index f3c57d9..0000000 --- a/wrangler.toml +++ /dev/null @@ -1,7 +0,0 @@ -#:schema node_modules/wrangler/config-schema.json -name = "meshcore" -compatibility_date = "2025-10-08" - -[assets] -directory = "build/web" -

"),be:s("u"),J:s("u"),gQ:s("u"),n:s("u"),gn:s("u<@>"),t:s("u"),dL:s("u"),c:s("u"),d4:s("u"),r:s("u"),Y:s("u"),bT:s("u<~()>"),aP:s("ax<@>"),T:s("et"),m:s("z"),g:s("bz"),aU:s("aV<@>"),bN:s("cy"),au:s("cy"),e9:s("o>"),cl:s("o"),aS:s("o>"),q:s("o