diff --git a/CHANGELOG.md b/CHANGELOG.md index 28cdea24a..3c60aea36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b1] - Unreleased ### Fixed +- **VP archive/queue names with `&` no longer render as `&amp;` + tooltip corrected for BambuStudio 2.7.x reality (#1658 follow-up, reported by @IndividualGhost1905)** — Two bugs surfaced on the same screenshot set: (A) Metadata-mode archive and queue names showed `PCB Vise &amp; Solder Station` where the 3MF's Title metadata is `PCB Vise & Solder Station`. **Root cause:** `ThreeMFParser._parse_3dmodel` (`backend/app/services/archive.py:495-538`) parsed the XML `…` payload via regex and stripped whitespace but never called `html.unescape()`. The raw `&` landed in the DB; React then auto-escaped the `&` again on render, producing `&amp;`. The sibling parser `ProjectPageParser` (line 754) already had a loop-until-stable unescape and a comment explaining why ("content is often triple-encoded" — observed BambuStudio behavior), the makerworld-fields path just didn't share it. **Fix:** module-level `import html` and the same loop-until-stable unescape pattern in `_parse_3dmodel`, applied uniformly to all `` values so `Title`, `Designer`, and any future fields all get peeled the same way. The loop terminates as soon as `html.unescape()` stops changing the string, so single-, double-, and triple-encoded payloads all converge to the correct value; plain ASCII passes through untouched. (B) Filename-mode showed the slugified project title (`PCB_Vise_&_Solder_Station`) instead of the user-typed Send-dialog text ("Main Parts"). **This is NOT a Bambuddy bug** — BambuStudio source confirms it. `PrintJob.cpp:314-325` (`src/slic3r/GUI/Jobs/PrintJob.cpp`) reads `BBL_DESIGNER_MODEL_TITLE_TAG` (defined as `"Title"` in `bbs_3mf.hpp`) from the 3MF, slugifies it (space → `_`, unusable chars `<>[]:/\|?*"` → `_`, collapse runs of `_`, truncate to 100 chars), and **unconditionally overwrites** the user-typed `m_project_name` with it before sending. `params.project_name` becomes both the FTP filename and the MQTT `subtask_name`. The user-typed string never leaves BambuStudio when a Title metadata exists — there is no MQTT field carrying it, so Bambuddy has no recovery path. The previous tooltip ("handy if you renamed the job in the 'send to printer' dialog") promised something BambuStudio strips, and the previous reply to the reporter dismissed this as "OrcaSlicer-style upload, working as designed" which was wrong on BambuStudio 2.7.1.57. **Fix:** tooltip rewritten in all 11 locales (de / en / es / fr / it / ja / ko / pt-BR / tr / zh-CN / zh-TW) to spell out the BambuStudio behavior — both modes often produce the same string because BS overwrites the Send-dialog name with the 3MF Title field when present. **Tests**: 3 new in `test_archive_service.py::TestThreeMFMetadataHTMLUnescape` — `Title` with `&` unescapes to `&` (the reporter's exact case), `Title` with triple-encoded `&amp;amp;` peels all three layers (the BambuStudio worst-case ProjectPageParser already documents), plain `Title=Benchy` passes through unchanged (regression guard against accidentally munging non-encoded payloads). Full 104-test archive suite green; ruff clean; i18n parity holds (5065 leaves × 11 locales); frontend build clean. - **FTP passive-port pool now sliced per-VP (10 ports each) so bridge-mode Docker drops from ~3.5 GB to ~210 MB host RAM (#1646, reported by @TheFou — followed up with corrections we acted on)** — Reporter on a Linux Docker VM (`network_mode: host` not viable because other containers already bind the same ports) measured 2002 `docker-proxy` host processes spawned from the previously-exposed `50000-51000:50000-51000` range — one process per port per address family, ~3.5 MB RSS each, ~3.5 GB total that doesn't show up in `docker stats` because it's host-level not container-level. **Root cause: shared port pool, treated as symptom not cause.** `VirtualPrinterFTPServer` exposed `PASSIVE_PORT_MIN/MAX` as **class constants** (`backend/app/services/virtual_printer/ftp_server.py:573-574`), so every VP's FTP session passed the same `(50000, 51000)` range into `_bind_passive_port` and competed on the same 0.0.0.0 binds. The widening from 100 → 1001 ports in an earlier round had been collision-avoidance headroom for multi-VP-on-shared-bind, but the cost was paid by every install — including the reporter's single-VP install that only ever needed ~10 ports of headroom. **Fix: per-VP non-overlapping slices, allocated by VP id.** New module-level `compute_passive_port_slice(vp_id) → (port_min, port_max)` returns a 10-port window: VP id 1 → 50000-50009, VP id 2 → 50010-50019, …, VP id 100 → 50990-50999. Class constants are gone; `VirtualPrinterFTPServer.__init__` now takes `passive_port_min` / `passive_port_max` instance args. `manager.py` computes the slice at server-construction time from `self.id` and passes it in. Result for the reporter (single VP): 10 exposed ports → 20 docker-proxy processes → ~70 MB instead of ~3.5 GB. Three VPs → 30 ports → ~210 MB. **Wrap-around behaviour pinned**: VP ids beyond `PASSIVE_MAX_SLOTS = 100` wrap modulo 100 (an install that's churned through many VPs over time still produces a valid in-range slice). A same-slot collision (vp_id 101 lands on the same slice as vp_id 1) falls back to the per-session 10-attempt random retry that pre-#1646 code already had — same recovery, no regression. **Compose default narrowed**: `docker-compose.yml` now exposes `50000-50029:50000-50029` by default (covers 3 VPs out of the box) instead of the 1001-port range. The comment explains how to widen for more VPs (`50000-500N9` for `N = vp_count - 1`) and that proxy-mode VPs still need `50000-50100:50000-50100` because proxy mode forwards the real printer's full range — that codepath uses a separate `TCPProxy.FTP_DATA_PORT_MIN/MAX` and isn't sliced (the real printer owns that range, not Bambuddy). **Doc corrections in the same drop**: the previous warning over-stated `userland-proxy: false` as "confirmed by the reporter" — TheFou had flagged it as theoretical, not tested; the new comment doesn't push it as a recommendation at all (it's a global daemon flag, too blunt for a per-container problem). The new comment also explicitly names Linux multi-service hosts (NAS, dedicated Docker VMs, Unraid, Synology DSM) as a primary bridge-mode audience instead of leaving the warning under a "macOS/Windows" framing that TheFou pointed out missed his use case. Acknowledges that host-mode default is a deliberate trade-off for SSDP discovery, not a security-blind default. **Tests**: 10 new in `test_vp_ftp_port_slicing.py` — `compute_passive_port_slice` pins: vp_id=1 starts at base, consecutive vp_ids get adjacent non-overlapping slices, no two distinct vp_ids within MAX_SLOTS share a port (exhaustive across all 100 slots), wraps modulo MAX_SLOTS, top slot stays within the documented pool, non-positive vp_ids clamp to slot 0 (defensive — never produce a negative port that would crash `asyncio.start_server`). Two `VirtualPrinterFTPServer` instance tests pin: two instances constructed with different slices stay independent (regression guard against re-introducing class-level state), default-arg construction yields a valid one-slice window. Existing proxy-mode test at `test_virtual_printer.py:2269` (101 ports for `_ftp_data_proxies`) stays green — that path is unchanged. Full 130-test VP suite green. - **Print Log "User" column now shows the user for prints started from the Queue (#1670, reported by @JmanB52D)** — Reporter on a P2S with auth enabled, Virtual Printer in Queue mode and Auto-dispatch off: a user uploads a `.3mf` to the VP (FTP, anonymous), then logs into Bambuddy and clicks ▶ on the staged queue item to start it; the print finishes and the PrintLogEntry's User column is blank. Same setup with the VP in Archive (slicer-initiated) mode correctly attributes the user. **Root cause: two-link gap on the Queue→manual-start dispatch path.** (a) `POST /queue/{id}/start` (`print_queue.py:1039`) auth-protected, but the route's user dep was bound to `_` and discarded — the clicker was never recorded. (b) `PrintScheduler._start_print` (`print_scheduler.py:1886`) dispatches the queue item directly and never calls `printer_manager.set_current_print_user(...)`. The print-complete callback (`main.py:3513`) reads `_print_user_info = printer_manager.get_current_print_user(printer_id)` — which is only ever populated by `background_dispatch.py:747/943` (the Archive→Print and Library→Print flows). Queue dispatch had no equivalent hop, so `_print_user_info` was always `None` and the PrintLogEntry's `created_by_username` landed `NULL`. **Fix (two-sided):** (1) `print_queue.py /start` now binds the auth dep to `user: User | None` and writes `item.created_by_id = user.id` when `user is not None AND item.created_by_id is None` — credits the clicker on VP-uploaded (unattributed) items without overwriting existing attribution from UI-added queue items (matches the standard "first claim wins" ownership rule in `auth.py::require_ownership_permission`). (2) `print_scheduler.py` gains a small `_propagate_owner_to_printer_manager` helper, called from `_start_print` immediately after `register_expected_print`: when `item.created_by_id` resolves to a real User row, it forwards `(printer_id, owner.id, owner.username)` into `printer_manager.set_current_print_user`. No-ops cleanly when the item has no owner (auto-dispatched VP items intrinsically) or when the user row is missing (e.g. user deleted between queue-add and dispatch — the print log row falls back to un-credited rather than crashing the dispatch). **Tests:** 6 new in `test_queue_start_user_attribution.py` — three route tests pin (a) authenticated `/start` writes `created_by_id` on an unattributed item, (b) an existing owner is preserved when a different user clicks `/start`, (c) auth-disabled leaves `created_by_id=NULL` (no synthetic placeholder user invented); three helper tests pin (d) the propagation forwards the resolved username into `set_current_print_user`, (e) a `None` owner is silently skipped, (f) a missing User row is silently skipped instead of raising. Full 63-test `test_print_queue_api.py` suite stays green. Backend ruff clean. diff --git a/backend/app/services/archive.py b/backend/app/services/archive.py index 886f1c0e0..de69afe01 100644 --- a/backend/app/services/archive.py +++ b/backend/app/services/archive.py @@ -1,4 +1,5 @@ import hashlib +import html import json import logging import os @@ -506,9 +507,20 @@ class ThreeMFParser: metadata_pattern = r']*>([^<]*)' matches = re.findall(metadata_pattern, content) + # 3MF metadata values are XML-encoded — `&` becomes `&`, etc. + # ProjectPageParser learned this the hard way: BambuStudio sometimes + # writes triple-encoded payloads (`&amp;amp;`), so we unescape + # in a loop until the string stabilises. Without this, a Title like + # "Foo & Bar" lands in the DB as raw "Foo & Bar" and React then + # double-escapes it on render to "Foo &amp; Bar" (#1658). makerworld_fields = {} for name, value in matches: - makerworld_fields[name] = value.strip() + decoded = value.strip() + prev = None + while prev != decoded: + prev = decoded + decoded = html.unescape(decoded) + makerworld_fields[name] = decoded # Check for direct MakerWorld URL in content url_pattern = r'https?://makerworld\.com/[^\s<>"\']+/models/(\d+)' diff --git a/backend/tests/unit/services/test_archive_service.py b/backend/tests/unit/services/test_archive_service.py index 844328119..324c54259 100644 --- a/backend/tests/unit/services/test_archive_service.py +++ b/backend/tests/unit/services/test_archive_service.py @@ -255,6 +255,69 @@ class TestArchiveThumbnails: assert parsed.get("_thumbnail_data") == b"PLATE1" +class TestThreeMFMetadataHTMLUnescape: + """3MF `…` values are XML-encoded. + BambuStudio sometimes writes triple-encoded payloads (the + ProjectPageParser comment documents this). Without an unescape loop, + a Title like ``Foo & Bar`` lands in the DB as raw ``Foo & Bar`` and + React then escapes the `&` on render to ``Foo &amp; Bar`` — the + user-visible symptom reported on #1658.""" + + def test_title_with_ampersand_is_unescaped(self, tmp_path): + import zipfile + + from backend.app.services.archive import ThreeMFParser + + threemf_path = tmp_path / "ampersand.3mf" + with zipfile.ZipFile(threemf_path, "w") as zf: + zf.writestr( + "3D/3dmodel.model", + '\n' + 'PCB Vise & Solder Station' + 'Chefkoch', + ) + + parsed = ThreeMFParser(str(threemf_path)).parse() + assert parsed.get("print_name") == "PCB Vise & Solder Station" + assert parsed.get("designer") == "Chefkoch" + + def test_title_with_triple_encoded_ampersand_is_fully_unescaped(self, tmp_path): + """BambuStudio has been observed writing triple-encoded payloads + (`&amp;amp;`). The decoder loops until the string stops changing + so all layers get peeled in one pass.""" + import zipfile + + from backend.app.services.archive import ThreeMFParser + + threemf_path = tmp_path / "triple.3mf" + with zipfile.ZipFile(threemf_path, "w") as zf: + zf.writestr( + "3D/3dmodel.model", + '\n' + 'Foo &amp;amp; Bar', + ) + + parsed = ThreeMFParser(str(threemf_path)).parse() + assert parsed.get("print_name") == "Foo & Bar" + + def test_title_without_entities_passes_through_unchanged(self, tmp_path): + """The unescape loop must be a no-op when there's nothing to unescape — + regression guard against accidentally munging plain ASCII titles.""" + import zipfile + + from backend.app.services.archive import ThreeMFParser + + threemf_path = tmp_path / "plain.3mf" + with zipfile.ZipFile(threemf_path, "w") as zf: + zf.writestr( + "3D/3dmodel.model", + '\nBenchy', + ) + + parsed = ThreeMFParser(str(threemf_path)).parse() + assert parsed.get("print_name") == "Benchy" + + class TestPrintableObjectsExtraction: """Tests for extracting printable objects count from 3MF files.""" diff --git a/docs/onboarding-tour-plan.md b/docs/onboarding-tour-plan.md new file mode 100644 index 000000000..a839fa3e2 --- /dev/null +++ b/docs/onboarding-tour-plan.md @@ -0,0 +1,469 @@ +# Bambuddy Onboarding Tour - Detailed Plan + +**Status:** Draft for review +**Owner:** Onboarding admin +**Audience:** Designer + frontend dev implementing the tour overlay +**Scope:** Welcome modal + step-by-step in-app tour for fresh installs +**Mascot:** BB (poses + expressions reference in the character sheet shipped 2026-06-07) + +--- + +## Goals + +1. Eliminate the most common cause of closed-as-`invalid` issues: add-printer setup confusion (access code / LAN mode / Developer mode / discovery). +2. Walk a brand-new user from zero to "first print sent via Bambuddy" in under 10 minutes. +3. Surface all major Bambuddy features at the right depth (overview, not full docs) so users discover what's available without reading the wiki cover-to-cover. +4. Hand off cleanly to existing self-service surfaces (Connection Diagnostic, Log Health Scanner, System page, wiki, Discord) — the tour points; the diagnostic surfaces do the work. + +## Out of scope + +- Per-feature settings panels (Auth providers, Postgres migration, Tailscale wiring) — covered by feature-specific docs. +- AI/ML feature deep dives (Obico detection, failure response) — covered by per-printer opt-in flows that already exist. +- The detection heuristic (per-user DB flag + backfill migration) — decided separately. + +--- + +## Phase 0 - First contact + +### Step 0.1 - Welcome modal +**Anchor:** centered modal, no DOM anchor +**Conditions to show:** `users.onboarding_status IS NULL` +**Content:** +- BB mascot, "Let's get started" pose (1) +- Headline: "Hallo! Welcome to Bambuddy." +- Body: "Bambuddy replaces the Bambu Lab cloud with a local-first dashboard. Your data, prints, spools, and timelapses stay on your hardware. Want a 5-minute tour?" +- Three buttons: + - `Tour starten` (primary, green) + - `Ich bin erfahren` (secondary, ghost) + - `Später erinnern` (text link, 7-day snooze) +**Execute on action:** +- Tour → continue to 0.2 +- Experienced → `PATCH /api/users/me/onboarding {status: "dismissed"}`, close +- Snooze → write `onboarding_snoozed_until = now + 7d`, close + +### Step 0.2 - What Bambuddy is (and isn't) +**Anchor:** modal, BB "Let me walk you through it" pose (2) +**Content:** +- Two-column comparison: + - **What Bambuddy does:** local cloud replacement, AMS + inventory + RFID, print queue, archives, slicer integration via Virtual Printer, multi-user, HomeAssistant, optional Tailscale. + - **What Bambuddy isn't (today):** not a slicer (uses BambuStudio/OrcaSlicer), not a cloud service, not a printer firmware tool, not a Klipper UI. +- One-line privacy note: "No telemetry. No accounts. bambuddy.cool only serves the docs." +**Links:** wiki home, GitHub repo, Discord, sponsor portal +**Buttons:** `Weiter` / `Überspringen` + +--- + +## Phase 1 - Critical setup (everyone needs these) + +### Step 1.1 - Authentication setup +**Anchor:** Settings → Auth tab (`/settings?tab=auth`, `[data-tour="auth-card"]`) +**Conditions to show:** `auth_enabled === false` AND user is first admin +**Content:** +- BB "Thinking" expression +- Headline: "Lock the front door first" +- Body: "Bambuddy can run with or without authentication. If anyone else on your network (or your tailnet, or your reverse proxy) can reach this URL, turn auth on now — passwords, OIDC, SAML, and MFA are all built in." +- Inline severity callout (yellow): "Bambuddy can also control your printers, manage files, and read your camera feeds. Treat the URL like an admin panel." +**Buttons:** +- `Enable auth now` → navigates to `/settings?tab=auth`, tour pauses, resumes on success +- `Later (I'm on a private network)` → continue +**Links:** wiki/security/authentication + +### Step 1.2 - Add your first printer (LOAD-BEARING) +**Anchor:** Printers page (`/`), `[data-tour="add-printer-button"]` +**Conditions to show:** `printers.count === 0` +**Content:** +- BB "Almost there!" pose (3) +- Headline: "Add your first printer" +- Body: "You'll need three things: model, IP address, and access code." +- Inline checklist with info popovers: + 1. **Model** — auto-detected on discovery, or pick manually (A1, A1 Mini, P1S, P1P, X1C, X1E, H2D, H2C, P2S). + 2. **IP address** — found on the printer LCD under Settings → WLAN. Tip: assign a DHCP reservation in your router so it doesn't change. + 3. **Access code** — printer LCD, model-specific path (see access-code popover below). +- **Embedded "Where's my access code?" popover** — one model-specific image + path per model: + - A1 / A1 Mini: Settings → WLAN → info icon + - P1S / P1P: Settings → General → LAN-only Mode → access code displayed + - X1C / X1E: Settings → Network → LAN-only Mode → access code + - H2D / H2C: same as X1 family + - P2S: Settings → Network → LAN Mode +- **Critical pre-flight warnings (red border):** + - "Enable **LAN-only mode** on the printer (X1 / H2 / P2S family) — without this, MQTT/FTP are blocked." + - "Enable **Developer Mode** on the printer LCD — required for MQTT control on most models." + - "Docker bridge mode users: discovery may not find the printer. Use **Add manually by IP**." +**Buttons:** +- `Add via discovery` → opens Add Printer modal with SSDP scan running +- `Add manually by IP` → opens Add Printer modal in manual mode +**On success (printer row appears in DB):** advance to 1.3 +**Links:** wiki/getting-started/add-printer, wiki/troubleshooting/discovery +**Issue evidence this step prevents:** #1641, #1453, #1411, #1487, #1524, #1405 + +### Step 1.3 - Verify the connection +**Anchor:** newly-added printer card, `[data-tour="printer-status-pill"]` +**Conditions to show:** at least one printer just added in this session +**Content:** +- BB "Focused" expression +- Headline: "Let's make sure Bambuddy can talk to it" +- Live status pills animate as the connection establishes: + - MQTT connect (port 8883) + - Camera stream (RTSPS 322, X1 / H2 / P2S only) + - File transfer (FTP 990) +- "All green within 30 seconds" → next button enables +- If yellow / red after 30s: surface a `Run full diagnostic` button +**Execute on action:** +- `POST /api/printers/{id}/diagnostic` → opens the existing Connection Diagnostic modal (the layer-1-through-8 triage feature shipped 2026-05-21). +- Tour parses the diagnostic verdict and either shows "All green — let's continue" or "Found {N} issues — open diagnostic for details" with a deep link. +**Links:** Connection Diagnostic, Log Health Scanner, wiki/troubleshooting + +### Step 1.4 - Tour the printer card +**Anchor:** printer card, sequential highlights of each region +**Content — 5 sub-highlights:** +1. **Status row** — printer state, ETA, current stage. "Your at-a-glance status." +2. **AMS row** — slots, RFID auto-detection, drying button. "Bambuddy reads your AMS slot config live — colors and types come from RFID, the rest from your inventory." +3. **Camera tile** — live feed via RTSPS proxy. "Same stream BambuStudio uses, but local — no cloud round-trip." +4. **Controls** — pause / resume / cancel, lights, fans. "Same controls as the printer LCD." +5. **Customization** — "Right-click the card to rearrange tiles or hide what you don't need" (Printer Card Customization, see wiki/features/printer-card). +**Buttons:** `Weiter` / `Überspringen Rest des Tours` + +--- + +## Phase 2 - Core workflows (everyone benefits) + +### Step 2.1 - Inventory mode pick (irreversible) +**Anchor:** Inventory page (`/inventory`) +**Conditions to show:** `inventory_mode IS NULL` (not yet chosen) +**Content:** +- BB "Need help?" pose (4) +- Headline: "Track your filament" +- Body: "Bambuddy can keep tabs on your spools. Pick a mode now — switching later loses data." +- Three large radio cards: + - **Internal (recommended)** — built-in inventory, mirrors AMS, reads RFID, auto-decrements weight as you print. Best for most users. + - **Spoolman** — point at an existing Spoolman instance, Bambuddy syncs from it. Best if you already run Spoolman. + - **None** — skip filament tracking entirely. You can change this later, but historical data won't backfill. +- Inline note (yellow): "#1556 footgun — switching modes later does not migrate data." +**Execute on action:** `PATCH /api/settings/inventory_mode {mode: "internal" | "spoolman" | "none"}` +**Links:** wiki/features/inventory, wiki/features/spoolman +**Issue evidence:** #1556, #1644, #1517, #1607, #1456 + +### Step 2.2 - Add your first spool +**Anchor:** Inventory page, `[data-tour="add-spool-button"]` +**Conditions to show:** `inventory_mode === "internal"` AND `spools.count === 0` +**Content:** +- "Add a spool the way that suits you:" + - **RFID scan** (Bambu spools) — load it in the AMS, Bambuddy detects automatically. No manual entry needed. + - **SpoolBuddy kiosk** — if you have a SpoolBuddy box, scan RFID write tag for non-Bambu spools. + - **Manual entry** — brand, material, color, weight. +- One-line note: "Bambuddy ships with a color catalog covering the major brands — names autocomplete as you type." +**Buttons:** `Add manually` → opens Add Spool modal / `Use RFID` → goes to printer card highlighting AMS row / `Skip` → continue +**Links:** wiki/features/inventory, wiki/features/spoolbuddy + +### Step 2.2b - Spoolman sync setup +**Anchor:** Settings → Spoolman card (`#card-spoolman`) +**Conditions to show:** `inventory_mode === "spoolman"` +**Content:** +- "Tell Bambuddy where Spoolman lives." +- Inline form: Spoolman URL + sync direction (Spoolman → Bambuddy, or bi-directional). +- "Bambuddy will pull your existing spool library and keep it in sync. RFID scans still work — they create new spools in Spoolman." +**Execute on action:** `POST /api/settings/spoolman/test` → green = continue, red = stay on step with error. + +### Step 2.3 - Profile management (Bambu cloud sync) +**Anchor:** Profiles page (`/profiles`), `[data-tour="bambu-cloud-sync"]` +**Content:** +- BB "Helpful" pose +- Headline: "Sync your filament + print profiles from Bambu Lab" +- Body: "If you've created custom filament or print profiles in BambuStudio + the Bambu cloud, Bambuddy can pull them down so they're available everywhere — assigned via the web UI, sent via Virtual Printer, used by the queue." +- Inline form: Bambu Lab account email + password (or "Sign in later") +- One-line warning: "Bambuddy stores credentials encrypted at rest and only uses them against the official Bambu API. Source is open." +**Buttons:** `Sign in to Bambu` / `Skip (use built-in defaults)` +**Links:** wiki/features/profiles, wiki/security/credential-storage + +### Step 2.4 - The print queue +**Anchor:** Queue page (`/queue`), `[data-tour="add-to-queue-button"]` +**Content:** +- "Queue prints across all your printers." +- Three things the queue can do, with a one-line example each: + 1. **Manual queue** — drag-and-drop files, pick which printer runs them. + 2. **Auto-dispatch** — Bambuddy assigns queued jobs to idle printers automatically based on AMS / build-plate / capacity. + 3. **Auto-drying** — queued PETG / PA jobs trigger AMS pre-drying so the spool is ready when dispatch fires (Queue Auto-Drying, see wiki/features/queue-drying). +- "Power features for later: dependencies (`require_previous_success`), scheduled prints, batch jobs." +**Buttons:** `Weiter` / `Show me how to add my first job` → opens Add to Queue modal +**Links:** wiki/features/queue, wiki/features/queue-drying + +### Step 2.5 - Archives + statistics +**Anchor:** Archives page (`/archives`), then Stats (`/stats`) +**Content:** +- "Every finished print is archived automatically." +- Two-screen mini-tour: + - **Archives** — thumbnail, timelapse video, finish photo, gcode, sliced 3MF, runtime, weight, filaments used per slot. "Re-print directly from any archive." + - **Statistics** — print hours, filament used (by brand / material / color), energy cost, time-saved, success rate. +- "Cost tracking pulls electricity price from settings — set it once and stats compute energy spend per print." +**Buttons:** `Weiter` +**Links:** wiki/features/archives, wiki/features/statistics + +### Step 2.6 - Maintenance tracking +**Anchor:** Maintenance page (`/maintenance`), `[data-tour="add-maintenance-task"]` +**Content:** +- BB "Helpful" pose +- "Bambuddy tracks consumables and maintenance per printer." +- Examples: nozzle wear (by print hours), belt tension (by month), hotend swap (by filament weight), grease (by print count). +- "Built-in tasks cover the standard intervals — add your own for custom maintenance." +- Notifications fire via the same channel as print events (see Step 3.8). +**Buttons:** `Weiter` / `Show me the defaults` → highlights default-task list +**Links:** wiki/features/maintenance + +### Step 2.7 - File library + projects +**Anchor:** File Manager (`/files`) +**Content:** +- "Your library lives here — upload 3MF, gcode, STL; group into projects; send to any printer." +- Two sub-highlights: + - **Files page** — flat browser, upload, tag, search, send-to. + - **Projects page** — group files into a logical project (multi-plate models, multi-part assemblies). Track which plates are printed; mark project complete when done. +- "External library folders (see Phase 3) let you mount a NAS share if your files don't live inside the container." +**Buttons:** `Weiter` +**Links:** wiki/features/library, wiki/features/projects + +--- + +## Phase 3 - Power features (offer, don't push) + +Each Phase 3 step starts with an "Interested?" gate — if the user clicks `Skip`, they jump to the next step without seeing the detail. The mascot uses the "Curious" expression for these. + +### Step 3.1 - Virtual Printer (intro only) +**Anchor:** Settings → Virtual Printer card +**Content:** +- "Want BambuStudio / OrcaSlicer to send prints to Bambuddy instead of the cloud?" +- Four-mode decision tree (one sentence each): + - **Bridge** — drop-in cloud replacement; slicer sends, Bambuddy forwards to the real printer. + - **Queue** — slicer sends to a virtual collector; Bambuddy queues for dispatch. + - **Proxy** — slicer points at Bambuddy, Bambuddy passes through with full MQTT/FTP/RTSP rewrite (best for multi-slicer setups). + - **Archive / Review** — slicer sends, Bambuddy stores but doesn't print. Audit / approval workflows. +- "VP picks a free IP on your bind interface so it looks like a real printer to the slicer." +- One-line warning: "Docker bridge mode needs port exposure — see the Docker wiki page for the FTP passive port slicing (#1646)." +**Buttons:** `Set up a Virtual Printer` → opens VP wizard / `Show me later` +**Links:** wiki/features/virtual-printer +**Issue evidence:** #1652, #1604, #1594, #1612, #1527 + +### Step 3.2 - Slicer API sidecar +**Anchor:** Settings → Slicer API card +**Conditions to show:** sidecar URL not configured +**Content:** +- "Slice directly inside Bambuddy from MakerWorld URLs or your library — no BambuStudio needed." +- "Requires the orca-slicer-api sidecar container (separate docker-compose, link below). Bambuddy talks to it over HTTP." +- One-line note: "Status: still maturing upstream (segfault on multi-filament 3MF being patched). Solid for single-filament / single-plate jobs today." +**Buttons:** `Configure sidecar` → opens slicer URL field / `Skip` +**Links:** github.com/maziggy/orca-slicer-api, wiki/features/slicer-api + +### Step 3.3 - External library folders +**Anchor:** Settings → Library / external roots +**Content:** +- "Mount a NAS share, an external SSD, or a project drive — Bambuddy reads files in-place." +- "Set `BAMBUDDY_EXTERNAL_ROOTS` in `docker-compose.yml`, bind-mount the host path. Bambuddy auto-shows folders in the File Manager." +- One-line warning (red): "Use `:ro` (read-only) unless you specifically want users uploading back to the share." +**Buttons:** `Weiter` +**Links:** wiki/getting-started/docker, wiki/features/library-external + +### Step 3.4 - MakerWorld integration +**Anchor:** MakerWorld page (`/makerworld`) +**Conditions to show:** `permissions.has("makerworld:view")` +**Content:** +- "Paste any MakerWorld URL — Bambuddy downloads the 3MF, adds it to your library." +- One-line note: "Direct search inside the UI was cut for this release — paste the URL from the MakerWorld site." +- "Imports respect your external-folder layout — pick where the file lands." +**Buttons:** `Try it now` → opens MakerWorld page / `Skip` +**Links:** wiki/features/makerworld + +### Step 3.5 - Obico ML failure detection (opt-in per printer) +**Anchor:** printer card → settings → Obico section +**Content:** +- "Self-hosted ML print-failure detection — no Obico cloud account, no telemetry." +- "Bambuddy talks directly to your self-hosted Obico ML server. Opt-in per printer; off by default." +- One-line note: "Smoothing / dead-zone tuning lives on the printer's Obico panel." +**Buttons:** `Weiter` +**Links:** wiki/features/obico, github.com/TheSpaghettiDetective/obico-server + +### Step 3.6 - HomeAssistant + webhooks +**Anchor:** Settings → Integrations (`#card-integrations`) +**Content:** +- "Bambuddy ships first-class HomeAssistant integration — sensors for every printer (state, temp, ETA, AMS slots), services to start / pause / cancel." +- "Webhooks fire on print events, queue events, archive events — useful for Discord bots, NodeRED, custom dashboards." +- One-line note: "Webhook signing secret in Settings → Integrations." +**Buttons:** `Weiter` +**Links:** wiki/features/homeassistant, wiki/features/webhooks + +### Step 3.7 - Tailscale / remote access +**Anchor:** Settings → Tailscale card +**Conditions to show:** `/var/run/tailscale/tailscaled.sock` mounted OR `tailscale` binary detected on host +**Content:** +- "Access Bambuddy from anywhere via your tailnet — no port forwarding, no public exposure." +- "MagicDNS HTTPS via Let's Encrypt (Bambuddy requests certs via `tailscale cert`)." +- One-line note: "Read the Tailscale blog post about Bambuddy at [link] for the full setup walkthrough." +**Buttons:** `Open Tailscale settings` / `Skip` +**Links:** wiki/features/tailscale, tailscale blog post + +### Step 3.8 - Notifications +**Anchor:** Notifications page (`/notifications`) and Settings → Notifications +**Content:** +- "Get told when prints finish, fail, or need attention." +- Channels: in-app, browser push, Discord, Telegram, Pushover, Gotify, ntfy, email (SMTP), webhook. +- "Per-event filters — only ping me on failures, route AMS humidity warnings to Discord, send finish photos via Telegram, etc." +**Buttons:** `Configure now` / `Skip` +**Links:** wiki/features/notifications + +--- + +## Phase 4 - Multi-user setup (conditional) + +### Step 4.1 - Invite users +**Anchor:** Settings → Users tab (`/settings?tab=users`) +**Conditions to show:** `auth_enabled === true` AND `users.count === 1` +**Content:** +- "Add accounts for the rest of your household / team." +- "Each user has their own permissions, print history, and notification settings. Print log shows who started which job (#1670 fix)." +**Buttons:** `Add user` / `Skip` +**Links:** wiki/features/multi-user + +### Step 4.2 - Groups & permissions +**Anchor:** Settings → Users tab → Groups section +**Content:** +- "Group users by role. Bambuddy ships with default groups: Admin, Operator, Viewer." +- Quick permission matrix: who can add printers / send prints / view archives / change settings. +- "Build your own groups for custom roles (read-only kid account, full-access partner, etc.)." +**Buttons:** `Weiter` +**Links:** wiki/features/permissions + +### Step 4.3 - SSO (OIDC / SAML) and MFA +**Anchor:** Settings → Auth tab +**Conditions to show:** more than 3 users OR admin opens this section explicitly +**Content:** +- "Bambuddy supports OIDC (Authentik, Authelia, Keycloak, Google, GitHub) and SAML 2.0 for org SSO." +- "Per-user MFA (TOTP). Encryption key auto-generates on first start (see #1219), override via env var for secret-manager workflows." +**Buttons:** `Configure OIDC` / `Configure SAML` / `Enable MFA on my account` / `Skip` +**Links:** wiki/security/authentication, wiki/security/oidc, wiki/security/saml, wiki/security/mfa + +--- + +## Phase 5 - Outro + +### Step 5.1 - Where help lives +**Anchor:** modal, BB "All set!" pose (5) +**Content:** +- Headline: "You're all set! Here's where to go when something's off." +- Quick reference card (icon + one line each): + - **System page** (`/system`) — version, logs, debug bundle, support export. + - **Connection Diagnostic** — printer won't connect / camera black / FTP fails — open from the printer card menu. + - **Log Health Scanner** — recurring runtime issues with known-fix suggestions (shipped 2026-05-22). + - **Wiki** — wiki.bambuddy.cool, full feature docs. + - **Discord** — community help, faster than GitHub for usage questions. + - **GitHub Issues** — actual bugs and feature requests. +**Buttons:** `Done` + +### Step 5.2 - Dismiss + sidebar re-entry +**Anchor:** sidebar bottom, `[data-tour="help-icon"]` +**Content:** +- "Need to see this tour again? It lives here at the bottom of the sidebar (BB icon)." +- Single highlight on the BB icon for 3 seconds, then close. +**Execute on close:** `PATCH /api/users/me/onboarding {status: "completed_tour"}` + +--- + +## Appendix A - Anchor selector strategy + +All anchors use stable `data-tour=""` attributes added to the underlying components, NOT text matching against translated strings. + +Required selectors (full list — track in code review): +- `[data-tour="add-printer-button"]` — PrintersPage +- `[data-tour="printer-status-pill"]` — PrinterCard +- `[data-tour="auth-card"]` — SettingsPage auth tab +- `[data-tour="add-spool-button"]` — InventoryPage +- `[data-tour="bambu-cloud-sync"]` — ProfilesPage +- `[data-tour="add-to-queue-button"]` — QueuePage +- `[data-tour="add-maintenance-task"]` — MaintenancePage +- `[data-tour="help-icon"]` — Sidebar bottom (NEW, to be added) + +A vitest test walks the tour against the rendered DOM and asserts every anchor resolves. PRs that change a component carrying a tour anchor have to either keep the anchor or update the tour script. + +--- + +## Appendix B - Tour state model + +```typescript +type OnboardingStatus = + | null // never seen the modal + | "dismissed" // skipped at welcome modal + | "snoozed" // 7-day defer (see onboarding_snoozed_until) + | "completed_tour" // finished phase 5 + | "tour_in_progress:" // resume from here on next session + | "dismissed_at_migration"; // backfilled for existing installs + +// PATCH /api/users/me/onboarding +// body: { status: OnboardingStatus, snoozed_until?: ISO8601 } +``` + +- Mid-tour close = same as dismissed (`completed_tour`). No partial resume in v1. +- Sidebar re-entry ignores the flag and restarts from Step 0.2 (skipping the welcome modal — they've already seen it). +- Step 1.2 (Add Printer) and Step 2.1 (Inventory mode) skip themselves if the underlying state is already set (printer exists / inventory mode chosen) — useful when re-entering the tour after partial setup. + +--- + +## Appendix C - i18n + +All step text, button labels, and tooltip strings live in `frontend/src/i18n/locales/*.ts` under a new `onboarding.*` namespace. + +Locales required at ship: de, en, es, fr, it, ja, ko, pt-BR, tr, zh-CN, zh-TW. + +CI gate: `check-i18n-parity.mjs` Check 4 fails on any English leak. NO `IDENTICAL_TO_EN_ALLOWED` entries for onboarding strings — the tour body is exactly the surface where users notice missing translations. + +--- + +## Appendix D - Backend additions needed + +1. `users.onboarding_status` column (TEXT NULL) + Alembic migration that backfills `'dismissed_at_migration'` for all existing rows. +2. `users.onboarding_snoozed_until` column (TIMESTAMP NULL). +3. `PATCH /api/users/me/onboarding` route — body: `{status, snoozed_until?}`. +4. `GET /api/users/me/onboarding` route — returns current status (frontend polls on app boot). +5. Branch on `is_sqlite()` for the column types — `TEXT` and `TIMESTAMP` differ Postgres vs SQLite (`feedback_postgres_migration_types`, `feedback_sqlite_and_postgres_upfront`). + +--- + +## Appendix E - Mascot asset inventory + +Poses needed (from the character sheet): +1. Let's get started — Step 0.1, 5.1 +2. Let me walk you through it — Step 0.2 +3. Almost there — Step 1.2 +4. Need help? — Step 2.1 +5. All set — Step 5.1 + +Expressions needed: +- Happy — Phase 0, Phase 5 +- Thinking — Step 1.1 +- Focused — Step 1.3 +- Excited — Step 2.2 ("first spool added!" celebration) +- Helpful — Steps 2.3, 2.6, 3.5 +- Curious — Phase 3 gates + +Branding elements: BB logo, leaf, filament spool, guidance arrow, setup checklist, foundation block — all already on the sheet. + +--- + +## Appendix F - Issue evidence trail + +GitHub `invalid`-tagged issues this tour explicitly addresses: + +| Cluster | Tour step | Closed issues | +|---------|-----------|---------------| +| Access code / LAN mode / Dev mode | 1.2 | #1641, #1453, #1411, #1487, #1524, #1405 | +| Connection failure not triaged | 1.3 → Diagnostic | #1527, #1612, #1604 | +| VP mode confusion | 3.1 | #1652, #1604, #1594, #1612, #1527 | +| Inventory mode switch destructive | 2.1 | #1556 | +| Inventory not reflecting reality | 2.1 + 2.2 | #1644, #1517, #1607, #1456 | +| Slicer-side mistaken as Bambuddy | 5.1 → "is this Bambuddy?" | #1597, #1525, #1582, #1579, #1578 | +| Docker volume / data-loss | 1.2 inline warning | #1524, #1517, #1409 | + +--- + +## Open questions + +1. Should Step 1.2 include an interactive "test the access code without saving" button (calls a one-shot MQTT connect with the entered creds), so users get instant feedback before committing the printer row? +2. Should Phase 3 be entirely opt-in (the user clicks "Show me power features" from Phase 5) instead of inline at the end of Phase 2? +3. Should the SpoolBuddy steps (kiosk-related) appear in the main tour, or only after SpoolBuddy hardware is detected on the network? +4. What's the right balance between "tour the page" (Phase 2.4 - 2.7) and "tooltips on the page itself"? Some of these could be inline help instead of tour steps. +5. Does the mascot character set need a "wrong" / "warning" expression for the inline red callouts in Step 1.2, or do plain icons work? diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 5207cab7c..f4cf28445 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -4466,7 +4466,7 @@ export default { }, archiveNameSource: { title: 'Quelle des Archivnamens', - description: 'Lege fest, wie neue Archive benannt werden, wenn Dateien über den virtuellen Drucker eintreffen. "Metadaten" verwendet den im 3MF eingebetteten Titel des Slicers (Standard). "Dateiname" nutzt den Dateinamen, den Bambu Studio per FTP gesendet hat — praktisch, wenn der Job im Dialog "Zum Drucker senden" umbenannt wurde.', + description: 'Lege fest, wie neue Archive benannt werden, wenn Dateien über den virtuellen Drucker eintreffen. "Metadaten" verwendet den im 3MF eingebetteten Titel des Slicers (Standard). "Dateiname" nutzt den Dateinamen, den Bambu Studio per FTP gesendet hat. Hinweis: Bambu Studio überschreibt den im Dialog "Zum Drucker senden" eingegebenen Namen mit dem Titelfeld der 3MF, sofern eines vorhanden ist — beide Modi liefern daher oft denselben Wert.', metadata: 'Metadaten', filename: 'Dateiname', }, diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 6f1af6525..2fdc6c6f7 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4479,7 +4479,7 @@ export default { }, archiveNameSource: { title: 'Archive name source', - description: 'Choose how new archives are named when files arrive via the virtual printer. "Metadata" uses the slicer-embedded title from the 3MF (default). "Filename" uses the filename Bambu Studio sent over FTP — handy if you renamed the job in the "send to printer" dialog.', + description: 'Choose how new archives are named when files arrive via the virtual printer. "Metadata" uses the slicer-embedded Title from the 3MF (default). "Filename" uses the filename Bambu Studio sent over FTP. Note: Bambu Studio overwrites the name you type in the "send to printer" dialog with the 3MF\'s Title field whenever one is present, so both modes often produce the same string.', metadata: 'Metadata', filename: 'Filename', }, diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 116694d8b..d6c3f5eae 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -4475,7 +4475,7 @@ export default { }, archiveNameSource: { title: 'Origen del nombre del archivo', - description: 'Elija cómo se nombran los archivos nuevos cuando llegan a través de la impresora virtual. «Metadatos» usa el título incrustado por el laminador del 3MF (predeterminado). «Nombre de archivo» usa el nombre de archivo que Bambu Studio envió por FTP — útil si renombró el trabajo en el diálogo de «enviar a la impresora».', + description: 'Elija cómo se nombran los archivos nuevos cuando llegan a través de la impresora virtual. «Metadatos» usa el título incrustado por el laminador del 3MF (predeterminado). «Nombre de archivo» usa el nombre de archivo que Bambu Studio envió por FTP. Nota: Bambu Studio sobrescribe el nombre que escribe en el diálogo «enviar a la impresora» con el campo Título del 3MF cuando existe, por lo que ambos modos suelen producir la misma cadena.', metadata: 'Metadatos', filename: 'Nombre de archivo', }, diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 53c679b8d..2fc7f7d52 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -4514,7 +4514,7 @@ export default { }, archiveNameSource: { title: 'Source du nom d\'archive', - description: 'Choisissez comment les nouvelles archives sont nommées lorsque les fichiers arrivent via l\'imprimante virtuelle. "Métadonnées" utilise le titre intégré par le slicer dans le 3MF (par défaut). "Nom de fichier" utilise le nom envoyé par Bambu Studio en FTP — pratique si vous avez renommé le job dans la boîte "envoyer à l\'imprimante".', + description: 'Choisissez comment les nouvelles archives sont nommées lorsque les fichiers arrivent via l\'imprimante virtuelle. "Métadonnées" utilise le titre intégré par le slicer dans le 3MF (par défaut). "Nom de fichier" utilise le nom envoyé par Bambu Studio en FTP. Note : Bambu Studio écrase le nom que vous saisissez dans la boîte "envoyer à l\'imprimante" avec le champ Titre du 3MF lorsque celui-ci est présent, les deux modes produisent donc souvent la même chaîne.', metadata: 'Métadonnées', filename: 'Nom de fichier', }, diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index d3d3cade1..0d8aae026 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -4513,7 +4513,7 @@ export default { }, archiveNameSource: { title: 'Origine nome archivio', - description: 'Scegli come vengono nominati i nuovi archivi quando i file arrivano tramite la stampante virtuale. "Metadati" utilizza il titolo incorporato nello slicer dal 3MF (default). "Nome file" utilizza il nome che Bambu Studio ha inviato via FTP — utile se hai rinominato il job nella finestra "invia a stampante".', + description: 'Scegli come vengono nominati i nuovi archivi quando i file arrivano tramite la stampante virtuale. "Metadati" utilizza il titolo incorporato nello slicer dal 3MF (default). "Nome file" utilizza il nome che Bambu Studio ha inviato via FTP. Nota: Bambu Studio sovrascrive il nome digitato nella finestra "invia a stampante" con il campo Titolo del 3MF quando presente, quindi entrambe le modalità producono spesso la stessa stringa.', metadata: 'Metadati', filename: 'Nome file', }, diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 80e81cfda..33589218e 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -4525,7 +4525,7 @@ export default { }, archiveNameSource: { title: 'アーカイブ名のソース', - description: '仮想プリンター経由でファイルが到着したときに、新しいアーカイブをどのように命名するかを選択します。「メタデータ」は3MFに埋め込まれたスライサータイトル(デフォルト)を使用します。「ファイル名」はBambu StudioがFTPで送信したファイル名を使用します — 「プリンターに送信」ダイアログでジョブ名を変更した場合に便利です。', + description: '仮想プリンター経由でファイルが到着したときに、新しいアーカイブをどのように命名するかを選択します。「メタデータ」は3MFに埋め込まれたスライサータイトル(デフォルト)を使用します。「ファイル名」はBambu StudioがFTPで送信したファイル名を使用します。注: Bambu Studioは、3MFにタイトルフィールドが存在する場合、「プリンターに送信」ダイアログで入力した名前をその値で上書きするため、両方のモードで同じ文字列になることが多くあります。', metadata: 'メタデータ', filename: 'ファイル名', }, diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index fd03cabed..ccee3cf26 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -4206,7 +4206,7 @@ export default { }, archiveNameSource: { title: '아카이브 이름 출처', - description: '가상 프린터를 통해 파일이 도착할 때 새 아카이브의 이름 지정 방법을 선택합니다. "메타데이터"는 3MF의 슬라이서 내장 제목을 사용합니다(기본값). "파일 이름"은 Bambu Studio가 FTP를 통해 전송한 파일 이름을 사용합니다.', + description: '가상 프린터를 통해 파일이 도착할 때 새 아카이브의 이름 지정 방법을 선택합니다. "메타데이터"는 3MF의 슬라이서 내장 제목을 사용합니다(기본값). "파일 이름"은 Bambu Studio가 FTP를 통해 전송한 파일 이름을 사용합니다. 참고: Bambu Studio는 3MF에 제목 필드가 있을 경우 "프린터로 보내기" 대화 상자에 입력한 이름을 해당 값으로 덮어쓰므로, 두 모드 모두 동일한 문자열이 표시되는 경우가 많습니다.', metadata: '메타데이터', filename: '파일 이름' }, diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 111ef24f9..14a880f0d 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -4513,7 +4513,7 @@ export default { }, archiveNameSource: { title: 'Origem do nome do arquivo', - description: 'Escolha como os novos arquivos são nomeados quando chegam pela impressora virtual. "Metadados" usa o título embutido pelo slicer no 3MF (padrão). "Nome do arquivo" usa o nome que o Bambu Studio enviou via FTP — útil se você renomeou o job no diálogo "enviar para impressora".', + description: 'Escolha como os novos arquivos são nomeados quando chegam pela impressora virtual. "Metadados" usa o título embutido pelo slicer no 3MF (padrão). "Nome do arquivo" usa o nome que o Bambu Studio enviou via FTP. Nota: o Bambu Studio sobrescreve o nome digitado no diálogo "enviar para impressora" com o campo Título do 3MF quando presente, portanto ambos os modos costumam produzir a mesma string.', metadata: 'Metadados', filename: 'Nome do arquivo', }, diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index b1532d0e7..e60575bed 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -4430,7 +4430,7 @@ export default { }, archiveNameSource: { title: 'Arşiv ad kaynağı', - description: 'Dosyalar sanal yazıcı aracılığıyla geldiğinde yeni arşivlerin nasıl adlandırılacağını seçin. "Meta veri" 3MF\'den dilimleyici-gömülü başlığı kullanır (varsayılan). "Dosya adı" Bambu Studio\'nun FTP üzerinden gönderdiği dosya adını kullanır — "yazıcıya gönder" iletişim kutusunda işi yeniden adlandırdıysanız kullanışlıdır.', + description: 'Dosyalar sanal yazıcı aracılığıyla geldiğinde yeni arşivlerin nasıl adlandırılacağını seçin. "Meta veri" 3MF\'den dilimleyici-gömülü başlığı kullanır (varsayılan). "Dosya adı" Bambu Studio\'nun FTP üzerinden gönderdiği dosya adını kullanır. Not: Bambu Studio, 3MF\'de bir Başlık alanı varsa "yazıcıya gönder" iletişim kutusunda yazdığınız adı bu alanla geçersiz kılar, bu nedenle her iki mod da genellikle aynı dizeyi üretir.', metadata: 'Meta veri', filename: 'Dosya adı', }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 745035ffe..4b8bd3b45 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -4455,7 +4455,7 @@ export default { }, archiveNameSource: { title: '存档名称来源', - description: '选择通过虚拟打印机接收文件时新存档的命名方式。"元数据"使用 3MF 中嵌入的切片标题(默认)。"文件名"使用 Bambu Studio 通过 FTP 发送的文件名 — 当您在"发送到打印机"对话框中重命名作业时非常有用。', + description: '选择通过虚拟打印机接收文件时新存档的命名方式。"元数据"使用 3MF 中嵌入的切片标题(默认)。"文件名"使用 Bambu Studio 通过 FTP 发送的文件名。注意:当 3MF 包含标题字段时,Bambu Studio 会用该字段覆盖您在"发送到打印机"对话框中输入的名称,因此两种模式通常会产生相同的字符串。', metadata: '元数据', filename: '文件名', }, diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index bfab32ac1..ba05b33cf 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -4455,7 +4455,7 @@ export default { }, archiveNameSource: { title: '存檔名稱來源', - description: '選擇透過虛擬印表機接收檔案時新存檔的命名方式。"元資料"使用 3MF 中嵌入的切片標題(預設)。"檔名"使用 Bambu Studio 透過 FTP 傳送的檔案名稱 — 當您在"傳送到印表機"對話方塊中重新命名工作時非常有用。', + description: '選擇透過虛擬印表機接收檔案時新存檔的命名方式。"元資料"使用 3MF 中嵌入的切片標題(預設)。"檔名"使用 Bambu Studio 透過 FTP 傳送的檔案名稱。注意:當 3MF 包含標題欄位時,Bambu Studio 會以該欄位覆寫您在"傳送到印表機"對話方塊中輸入的名稱,因此兩種模式通常會產生相同的字串。', metadata: '元資料', filename: '檔名', }, diff --git a/static/assets/index-SS1wDI23.js b/static/assets/index-B-fcW9HO.js similarity index 98% rename from static/assets/index-SS1wDI23.js rename to static/assets/index-B-fcW9HO.js index ffe316175..a9389bb1d 100644 --- a/static/assets/index-SS1wDI23.js +++ b/static/assets/index-B-fcW9HO.js @@ -6,37 +6,37 @@ function dme(t,e){for(var a=0;a")&&(qt=qt.replace("",h.displayName)),qt}while(1<=M&&0<=W);break}}}finally{q=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?B(_):""}function X(h,w){switch(h.tag){case 26:case 27:case 5:return B(h.type);case 16:return B("Lazy");case 13:return h.child!==w&&w!==null?B("Suspense Fallback"):B("Suspense");case 19:return B("SuspenseList");case 0:case 15:return K(h.type,!1);case 11:return K(h.type.render,!1);case 1:return K(h.type,!0);case 31:return B("Activity");default:return""}}function ie(h){try{var w="",_=null;do w+=X(h,_),_=h,h=h.return;while(h);return w}catch(M){return` Error generating stack: `+M.message+` `+M.stack}}var se=Object.prototype.hasOwnProperty,he=t.unstable_scheduleCallback,pe=t.unstable_cancelCallback,fe=t.unstable_shouldYield,$=t.unstable_requestPaint,L=t.unstable_now,ae=t.unstable_getCurrentPriorityLevel,ge=t.unstable_ImmediatePriority,be=t.unstable_UserBlockingPriority,ve=t.unstable_NormalPriority,Me=t.unstable_LowPriority,Ee=t.unstable_IdlePriority,Pe=t.log,Ue=t.unstable_setDisableYieldValue,Ce=null,Ie=null;function Re(h){if(typeof Pe=="function"&&Ue(h),Ie&&typeof Ie.setStrictMode=="function")try{Ie.setStrictMode(Ce,h)}catch{}}var qe=Math.clz32?Math.clz32:ze,Le=Math.log,st=Math.LN2;function ze(h){return h>>>=0,h===0?32:31-(Le(h)/st|0)|0}var at=256,je=262144,Qe=4194304;function $e(h){var w=h&42;if(w!==0)return w;switch(h&-h){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return h&261888;case 262144:case 524288:case 1048576:case 2097152:return h&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return h&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return h}}function Ne(h,w,_){var M=h.pendingLanes;if(M===0)return 0;var W=0,ee=h.suspendedLanes,Se=h.pingedLanes;h=h.warmLanes;var He=M&134217727;return He!==0?(M=He&~ee,M!==0?W=$e(M):(Se&=He,Se!==0?W=$e(Se):_||(_=He&~h,_!==0&&(W=$e(_))))):(He=M&~ee,He!==0?W=$e(He):Se!==0?W=$e(Se):_||(_=M&~h,_!==0&&(W=$e(_)))),W===0?0:w!==0&&w!==W&&(w&ee)===0&&(ee=W&-W,_=w&-w,ee>=_||ee===32&&(_&4194048)!==0)?w:W}function Oe(h,w){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&w)===0}function Ze(h,w){switch(h){case 1:case 2:case 4:case 8:case 64:return w+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return w+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function We(){var h=Qe;return Qe<<=1,(Qe&62914560)===0&&(Qe=4194304),h}function Ae(h){for(var w=[],_=0;31>_;_++)w.push(h);return w}function rt(h,w){h.pendingLanes|=w,w!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function et(h,w,_,M,W,ee){var Se=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var He=h.entanglements,pt=h.expirationTimes,Et=h.hiddenUpdates;for(_=Se&~_;0<_;){var qt=31-qe(_),Wt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Ir=/[\n"\\]/g;function Oa(h){return h.replace(Ir,function(w){return"\\"+w.charCodeAt(0).toString(16)+" "})}function Un(h,w,_,M,W,ee,Se,He){h.name="",Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"?h.type=Se:h.removeAttribute("type"),w!=null?Se==="number"?(w===0&&h.value===""||h.value!=w)&&(h.value=""+da(w)):h.value!==""+da(w)&&(h.value=""+da(w)):Se!=="submit"&&Se!=="reset"||h.removeAttribute("value"),w!=null?un(h,Se,da(w)):_!=null?un(h,Se,da(_)):M!=null&&h.removeAttribute("value"),W==null&&ee!=null&&(h.defaultChecked=!!ee),W!=null&&(h.checked=W&&typeof W!="function"&&typeof W!="symbol"),He!=null&&typeof He!="function"&&typeof He!="symbol"&&typeof He!="boolean"?h.name=""+da(He):h.removeAttribute("name")}function Ua(h,w,_,M,W,ee,Se,He){if(ee!=null&&typeof ee!="function"&&typeof ee!="symbol"&&typeof ee!="boolean"&&(h.type=ee),w!=null||_!=null){if(!(ee!=="submit"&&ee!=="reset"||w!=null)){ma(h);return}_=_!=null?""+da(_):"",w=w!=null?""+da(w):_,He||w===h.value||(h.value=w),h.defaultValue=w}M=M??W,M=typeof M!="function"&&typeof M!="symbol"&&!!M,h.checked=He?h.checked:!!M,h.defaultChecked=!!M,Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"&&(h.name=Se),ma(h)}function un(h,w,_){w==="number"&&Za(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function Mr(h,w,_,M){if(h=h.options,w){w={};for(var W=0;W<_.length;W++)w["$"+_[W]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),bp=!1;if(Ja)try{var Cd={};Object.defineProperty(Cd,"passive",{get:function(){bp=!0}}),window.addEventListener("test",Cd,Cd),window.removeEventListener("test",Cd,Cd)}catch{bp=!1}var li=null,Eu=null,wi=null;function wf(){if(wi)return wi;var h,w=Eu,_=w.length,M,W="value"in li?li.value:li.textContent,ee=W.length;for(h=0;h<_&&w[h]===W[h];h++);var Se=_-h;for(M=1;M<=Se&&w[_-M]===W[ee-M];M++);return wi=W.slice(h,1=di),Sp=" ",Nb=!1;function ye(h,w){switch(h){case"keyup":return Cv.indexOf(w.keyCode)!==-1;case"keydown":return w.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ot(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var la=!1;function Ta(h,w){switch(h){case"compositionend":return Ot(w);case"keypress":return w.which!==32?null:(Nb=!0,Sp);case"textInput":return h=w.data,h===Sp&&Nb?null:h;default:return null}}function Ha(h,w){if(la)return h==="compositionend"||!sl&&ye(h,w)?(h=wf(),wi=Eu=li=null,la=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(w.ctrlKey||w.altKey||w.metaKey)||w.ctrlKey&&w.altKey){if(w.char&&1=w)return{node:_,offset:w-h};h=M}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=Eo(_)}}function Do(h,w){return h&&w?h===w?!0:h&&h.nodeType===3?!1:w&&w.nodeType===3?Do(h,w.parentNode):"contains"in h?h.contains(w):h.compareDocumentPosition?!!(h.compareDocumentPosition(w)&16):!1:!1}function uo(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var w=Za(h.document);w instanceof h.HTMLIFrameElement;){try{var _=typeof w.contentWindow.location.href=="string"}catch{_=!1}if(_)h=w.contentWindow;else break;w=Za(h.document)}return w}function Si(h){var w=h&&h.nodeName&&h.nodeName.toLowerCase();return w&&(w==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||w==="textarea"||h.contentEditable==="true")}var Md=Ja&&"documentMode"in document&&11>=document.documentMode,mi=null,fr=null,Er=null,Qn=!1;function kp(h,w,_){var M=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;Qn||mi==null||mi!==Za(M)||(M=mi,"selectionStart"in M&&Si(M)?M={start:M.selectionStart,end:M.selectionEnd}:(M=(M.ownerDocument&&M.ownerDocument.defaultView||window).getSelection(),M={anchorNode:M.anchorNode,anchorOffset:M.anchorOffset,focusNode:M.focusNode,focusOffset:M.focusOffset}),Er&&vs(Er,M)||(Er=M,M=g_(fr,"onSelect"),0>=Se,W-=Se,Dd=1<<32-qe(w)+W|_<tr?(_r=Ca,Ca=null):_r=Ca.sibling;var Lr=Lt(kt,Ca,Mt[tr],$t);if(Lr===null){Ca===null&&(Ca=_r);break}h&&Ca&&Lr.alternate===null&&w(kt,Ca),xt=ee(Lr,xt,tr),Fr===null?Da=Lr:Fr.sibling=Lr,Fr=Lr,Ca=_r}if(tr===Mt.length)return _(kt,Ca),Cr&&Bu(kt,tr),Da;if(Ca===null){for(;trtr?(_r=Ca,Ca=null):_r=Ca.sibling;var $p=Lt(kt,Ca,Lr.value,$t);if($p===null){Ca===null&&(Ca=_r);break}h&&Ca&&$p.alternate===null&&w(kt,Ca),xt=ee($p,xt,tr),Fr===null?Da=$p:Fr.sibling=$p,Fr=$p,Ca=_r}if(Lr.done)return _(kt,Ca),Cr&&Bu(kt,tr),Da;if(Ca===null){for(;!Lr.done;tr++,Lr=Mt.next())Lr=Wt(kt,Lr.value,$t),Lr!==null&&(xt=ee(Lr,xt,tr),Fr===null?Da=Lr:Fr.sibling=Lr,Fr=Lr);return Cr&&Bu(kt,tr),Da}for(Ca=M(Ca);!Lr.done;tr++,Lr=Mt.next())Lr=Bt(Ca,kt,tr,Lr.value,$t),Lr!==null&&(h&&Lr.alternate!==null&&Ca.delete(Lr.key===null?tr:Lr.key),xt=ee(Lr,xt,tr),Fr===null?Da=Lr:Fr.sibling=Lr,Fr=Lr);return h&&Ca.forEach(function(cme){return w(kt,cme)}),Cr&&Bu(kt,tr),Da}function an(kt,xt,Mt,$t){if(typeof Mt=="object"&&Mt!==null&&Mt.type===v&&Mt.key===null&&(Mt=Mt.props.children),typeof Mt=="object"&&Mt!==null){switch(Mt.$$typeof){case f:e:{for(var Da=Mt.key;xt!==null;){if(xt.key===Da){if(Da=Mt.type,Da===v){if(xt.tag===7){_(kt,xt.sibling),$t=W(xt,Mt.props.children),$t.return=kt,kt=$t;break e}}else if(xt.elementType===Da||typeof Da=="object"&&Da!==null&&Da.$$typeof===D&&Mf(Da)===xt.type){_(kt,xt.sibling),$t=W(xt,Mt.props),Ev($t,Mt),$t.return=kt,kt=$t;break e}_(kt,xt);break}else w(kt,xt);xt=xt.sibling}Mt.type===v?($t=Cf(Mt.props.children,kt.mode,$t,Mt.key),$t.return=kt,kt=$t):($t=Dk(Mt.type,Mt.key,Mt.props,null,kt.mode,$t),Ev($t,Mt),$t.return=kt,kt=$t)}return Se(kt);case g:e:{for(Da=Mt.key;xt!==null;){if(xt.key===Da)if(xt.tag===4&&xt.stateNode.containerInfo===Mt.containerInfo&&xt.stateNode.implementation===Mt.implementation){_(kt,xt.sibling),$t=W(xt,Mt.children||[]),$t.return=kt,kt=$t;break e}else{_(kt,xt);break}else w(kt,xt);xt=xt.sibling}$t=Sj(Mt,kt.mode,$t),$t.return=kt,kt=$t}return Se(kt);case D:return Mt=Mf(Mt),an(kt,xt,Mt,$t)}if(Y(Mt))return wa(kt,xt,Mt,$t);if(O(Mt)){if(Da=O(Mt),typeof Da!="function")throw Error(n(150));return Mt=Da.call(Mt),za(kt,xt,Mt,$t)}if(typeof Mt.then=="function")return an(kt,xt,Bk(Mt),$t);if(Mt.$$typeof===k)return an(kt,xt,Rk(kt,Mt),$t);Uk(kt,Mt)}return typeof Mt=="string"&&Mt!==""||typeof Mt=="number"||typeof Mt=="bigint"?(Mt=""+Mt,xt!==null&&xt.tag===6?(_(kt,xt.sibling),$t=W(xt,Mt),$t.return=kt,kt=$t):(_(kt,xt),$t=wj(Mt,kt.mode,$t),$t.return=kt,kt=$t),Se(kt)):_(kt,xt)}return function(kt,xt,Mt,$t){try{Mv=0;var Da=an(kt,xt,Mt,$t);return zb=null,Da}catch(Ca){if(Ca===Rb||Ca===Ik)throw Ca;var Fr=ol(29,Ca,null,kt.mode);return Fr.lanes=$t,Fr.return=kt,Fr}}}var Df=$5(!0),V5=$5(!1),Tp=!1;function Fj(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Lj(h,w){h=h.updateQueue,w.updateQueue===h&&(w.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function Ap(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function jp(h,w,_){var M=h.updateQueue;if(M===null)return null;if(M=M.shared,(Br&2)!==0){var W=M.pending;return W===null?w.next=w:(w.next=W.next,W.next=w),M.pending=w,w=Ek(h),A5(h,null,_),w}return Mk(h,M,w,_),Ek(h)}function Dv(h,w,_){if(w=w.updateQueue,w!==null&&(w=w.shared,(_&4194048)!==0)){var M=w.lanes;M&=h.pendingLanes,_|=M,w.lanes=_,Ut(h,_)}}function Rj(h,w){var _=h.updateQueue,M=h.alternate;if(M!==null&&(M=M.updateQueue,_===M)){var W=null,ee=null;if(_=_.firstBaseUpdate,_!==null){do{var Se={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};ee===null?W=ee=Se:ee=ee.next=Se,_=_.next}while(_!==null);ee===null?W=ee=w:ee=ee.next=w}else W=ee=w;_={baseState:M.baseState,firstBaseUpdate:W,lastBaseUpdate:ee,shared:M.shared,callbacks:M.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=w:h.next=w,_.lastBaseUpdate=w}var zj=!1;function Fv(){if(zj){var h=Lb;if(h!==null)throw h}}function Lv(h,w,_,M){zj=!1;var W=h.updateQueue;Tp=!1;var ee=W.firstBaseUpdate,Se=W.lastBaseUpdate,He=W.shared.pending;if(He!==null){W.shared.pending=null;var pt=He,Et=pt.next;pt.next=null,Se===null?ee=Et:Se.next=Et,Se=pt;var qt=h.alternate;qt!==null&&(qt=qt.updateQueue,He=qt.lastBaseUpdate,He!==Se&&(He===null?qt.firstBaseUpdate=Et:He.next=Et,qt.lastBaseUpdate=pt))}if(ee!==null){var Wt=W.baseState;Se=0,qt=Et=pt=null,He=ee;do{var Lt=He.lane&-536870913,Bt=Lt!==He.lane;if(Bt?(kr&Lt)===Lt:(M&Lt)===Lt){Lt!==0&&Lt===Fb&&(zj=!0),qt!==null&&(qt=qt.next={lane:0,tag:He.tag,payload:He.payload,callback:null,next:null});e:{var wa=h,za=He;Lt=w;var an=_;switch(za.tag){case 1:if(wa=za.payload,typeof wa=="function"){Wt=wa.call(an,Wt,Lt);break e}Wt=wa;break e;case 3:wa.flags=wa.flags&-65537|128;case 0:if(wa=za.payload,Lt=typeof wa=="function"?wa.call(an,Wt,Lt):wa,Lt==null)break e;Wt=m({},Wt,Lt);break e;case 2:Tp=!0}}Lt=He.callback,Lt!==null&&(h.flags|=64,Bt&&(h.flags|=8192),Bt=W.callbacks,Bt===null?W.callbacks=[Lt]:Bt.push(Lt))}else Bt={lane:Lt,tag:He.tag,payload:He.payload,callback:He.callback,next:null},qt===null?(Et=qt=Bt,pt=Wt):qt=qt.next=Bt,Se|=Lt;if(He=He.next,He===null){if(He=W.shared.pending,He===null)break;Bt=He,He=Bt.next,Bt.next=null,W.lastBaseUpdate=Bt,W.shared.pending=null}}while(!0);qt===null&&(pt=Wt),W.baseState=pt,W.firstBaseUpdate=Et,W.lastBaseUpdate=qt,ee===null&&(W.shared.lanes=0),Lp|=Se,h.lanes=Se,h.memoizedState=Wt}}function W5(h,w){if(typeof h!="function")throw Error(n(191,h));h.call(w)}function K5(h,w){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)W5(_[h],w)}var Ib=Q(null),Hk=Q(0);function Y5(h,w){h=Xu,ue(Hk,h),ue(Ib,w),Xu=h|w.baseLanes}function Ij(){ue(Hk,Xu),ue(Ib,Ib.current)}function Oj(){Xu=Hk.current,re(Ib),re(Hk)}var ll=Q(null),Yl=null;function Mp(h){var w=h.alternate;ue(pi,pi.current&1),ue(ll,h),Yl===null&&(w===null||Ib.current!==null||w.memoizedState!==null)&&(Yl=h)}function Bj(h){ue(pi,pi.current),ue(ll,h),Yl===null&&(Yl=h)}function Q5(h){h.tag===22?(ue(pi,pi.current),ue(ll,h),Yl===null&&(Yl=h)):Ep()}function Ep(){ue(pi,pi.current),ue(ll,ll.current)}function cl(h){re(ll),Yl===h&&(Yl=null),re(pi)}var pi=Q(0);function qk(h){for(var w=h;w!==null;){if(w.tag===13){var _=w.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||VM(_)||WM(_)))return w}else if(w.tag===19&&(w.memoizedProps.revealOrder==="forwards"||w.memoizedProps.revealOrder==="backwards"||w.memoizedProps.revealOrder==="unstable_legacy-backwards"||w.memoizedProps.revealOrder==="together")){if((w.flags&128)!==0)return w}else if(w.child!==null){w.child.return=w,w=w.child;continue}if(w===h)break;for(;w.sibling===null;){if(w.return===null||w.return===h)return null;w=w.return}w.sibling.return=w.return,w=w.sibling}return null}var qu=0,er=null,en=null,_i=null,Gk=!1,Ob=!1,Ff=!1,$k=0,Rv=0,Bb=null,Jde=0;function Xn(){throw Error(n(321))}function Uj(h,w){if(w===null)return!1;for(var _=0;_ee?ee:8;var Se=te.T,He={};te.T=He,aM(h,!1,w,_);try{var pt=W(),Et=te.S;if(Et!==null&&Et(He,pt),pt!==null&&typeof pt=="object"&&typeof pt.then=="function"){var qt=Zde(pt,M);Iv(h,w,qt,ml(h))}else Iv(h,w,M,ml(h))}catch(Wt){Iv(h,w,{then:function(){},status:"rejected",reason:Wt},ml())}finally{J.p=ee,Se!==null&&He.types!==null&&(Se.types=He.types),te.T=Se}}function nue(){}function eM(h,w,_,M){if(h.tag!==5)throw Error(n(476));var W=CU(h).queue;PU(h,W,w,ce,_===null?nue:function(){return NU(h),_(M)})}function CU(h){var w=h.memoizedState;if(w!==null)return w;w={memoizedState:ce,baseState:ce,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Gu,lastRenderedState:ce},next:null};var _={};return w.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Gu,lastRenderedState:_},next:null},h.memoizedState=w,h=h.alternate,h!==null&&(h.memoizedState=w),w}function NU(h){var w=CU(h);w.next===null&&(w=h.alternate.memoizedState),Iv(h,w.next.queue,{},ml())}function tM(){return Ss(t0)}function TU(){return hi().memoizedState}function AU(){return hi().memoizedState}function iue(h){for(var w=h.return;w!==null;){switch(w.tag){case 24:case 3:var _=ml();h=Ap(_);var M=jp(w,h,_);M!==null&&(Io(M,w,_),Dv(M,w,_)),w={cache:jj()},h.payload=w;return}w=w.return}}function sue(h,w,_){var M=ml();_={lane:M,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Xk(h)?MU(w,_):(_=yj(h,w,_,M),_!==null&&(Io(_,h,M),EU(_,w,M)))}function jU(h,w,_){var M=ml();Iv(h,w,_,M)}function Iv(h,w,_,M){var W={lane:M,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Xk(h))MU(w,W);else{var ee=h.alternate;if(h.lanes===0&&(ee===null||ee.lanes===0)&&(ee=w.lastRenderedReducer,ee!==null))try{var Se=w.lastRenderedState,He=ee(Se,_);if(W.hasEagerState=!0,W.eagerState=He,En(He,Se))return Mk(h,w,W,0),hn===null&&jk(),!1}catch{}if(_=yj(h,w,W,M),_!==null)return Io(_,h,M),EU(_,w,M),!0}return!1}function aM(h,w,_,M){if(M={lane:2,revertLane:FM(),gesture:null,action:M,hasEagerState:!1,eagerState:null,next:null},Xk(h)){if(w)throw Error(n(479))}else w=yj(h,_,M,2),w!==null&&Io(w,h,2)}function Xk(h){var w=h.alternate;return h===er||w!==null&&w===er}function MU(h,w){Ob=Gk=!0;var _=h.pending;_===null?w.next=w:(w.next=_.next,_.next=w),h.pending=w}function EU(h,w,_){if((_&4194048)!==0){var M=w.lanes;M&=h.pendingLanes,_|=M,w.lanes=_,Ut(h,_)}}var Ov={readContext:Ss,use:Wk,useCallback:Xn,useContext:Xn,useEffect:Xn,useImperativeHandle:Xn,useLayoutEffect:Xn,useInsertionEffect:Xn,useMemo:Xn,useReducer:Xn,useRef:Xn,useState:Xn,useDebugValue:Xn,useDeferredValue:Xn,useTransition:Xn,useSyncExternalStore:Xn,useId:Xn,useHostTransitionStatus:Xn,useFormState:Xn,useActionState:Xn,useOptimistic:Xn,useMemoCache:Xn,useCacheRefresh:Xn};Ov.useEffectEvent=Xn;var DU={readContext:Ss,use:Wk,useCallback:function(h,w){return mo().memoizedState=[h,w===void 0?null:w],h},useContext:Ss,useEffect:gU,useImperativeHandle:function(h,w,_){_=_!=null?_.concat([h]):null,Yk(4194308,4,vU.bind(null,w,h),_)},useLayoutEffect:function(h,w){return Yk(4194308,4,h,w)},useInsertionEffect:function(h,w){Yk(4,2,h,w)},useMemo:function(h,w){var _=mo();w=w===void 0?null:w;var M=h();if(Ff){Re(!0);try{h()}finally{Re(!1)}}return _.memoizedState=[M,w],M},useReducer:function(h,w,_){var M=mo();if(_!==void 0){var W=_(w);if(Ff){Re(!0);try{_(w)}finally{Re(!1)}}}else W=w;return M.memoizedState=M.baseState=W,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:W},M.queue=h,h=h.dispatch=sue.bind(null,er,h),[M.memoizedState,h]},useRef:function(h){var w=mo();return h={current:h},w.memoizedState=h},useState:function(h){h=Yj(h);var w=h.queue,_=jU.bind(null,er,w);return w.dispatch=_,[h.memoizedState,_]},useDebugValue:Zj,useDeferredValue:function(h,w){var _=mo();return Jj(_,h,w)},useTransition:function(){var h=Yj(!1);return h=PU.bind(null,er,h.queue,!0,!1),mo().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,w,_){var M=er,W=mo();if(Cr){if(_===void 0)throw Error(n(407));_=_()}else{if(_=w(),hn===null)throw Error(n(349));(kr&127)!==0||eU(M,w,_)}W.memoizedState=_;var ee={value:_,getSnapshot:w};return W.queue=ee,gU(aU.bind(null,M,ee,h),[h]),M.flags|=2048,Ub(9,{destroy:void 0},tU.bind(null,M,ee,_,w),null),_},useId:function(){var h=mo(),w=hn.identifierPrefix;if(Cr){var _=Fd,M=Dd;_=(M&~(1<<32-qe(M)-1)).toString(32)+_,w="_"+w+"R_"+_,_=$k++,0<_&&(w+="H"+_.toString(32)),w+="_"}else _=Jde++,w="_"+w+"r_"+_.toString(32)+"_";return h.memoizedState=w},useHostTransitionStatus:tM,useFormState:uU,useActionState:uU,useOptimistic:function(h){var w=mo();w.memoizedState=w.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return w.queue=_,w=aM.bind(null,er,!0,_),_.dispatch=w,[h,w]},useMemoCache:Vj,useCacheRefresh:function(){return mo().memoizedState=iue.bind(null,er)},useEffectEvent:function(h){var w=mo(),_={impl:h};return w.memoizedState=_,function(){if((Br&2)!==0)throw Error(n(440));return _.impl.apply(void 0,arguments)}}},rM={readContext:Ss,use:Wk,useCallback:SU,useContext:Ss,useEffect:Xj,useImperativeHandle:wU,useInsertionEffect:xU,useLayoutEffect:yU,useMemo:kU,useReducer:Kk,useRef:fU,useState:function(){return Kk(Gu)},useDebugValue:Zj,useDeferredValue:function(h,w){var _=hi();return _U(_,en.memoizedState,h,w)},useTransition:function(){var h=Kk(Gu)[0],w=hi().memoizedState;return[typeof h=="boolean"?h:zv(h),w]},useSyncExternalStore:J5,useId:TU,useHostTransitionStatus:tM,useFormState:mU,useActionState:mU,useOptimistic:function(h,w){var _=hi();return iU(_,en,h,w)},useMemoCache:Vj,useCacheRefresh:AU};rM.useEffectEvent=bU;var FU={readContext:Ss,use:Wk,useCallback:SU,useContext:Ss,useEffect:Xj,useImperativeHandle:wU,useInsertionEffect:xU,useLayoutEffect:yU,useMemo:kU,useReducer:Kj,useRef:fU,useState:function(){return Kj(Gu)},useDebugValue:Zj,useDeferredValue:function(h,w){var _=hi();return en===null?Jj(_,h,w):_U(_,en.memoizedState,h,w)},useTransition:function(){var h=Kj(Gu)[0],w=hi().memoizedState;return[typeof h=="boolean"?h:zv(h),w]},useSyncExternalStore:J5,useId:TU,useHostTransitionStatus:tM,useFormState:hU,useActionState:hU,useOptimistic:function(h,w){var _=hi();return en!==null?iU(_,en,h,w):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:Vj,useCacheRefresh:AU};FU.useEffectEvent=bU;function nM(h,w,_,M){w=h.memoizedState,_=_(M,w),_=_==null?w:m({},w,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var iM={enqueueSetState:function(h,w,_){h=h._reactInternals;var M=ml(),W=Ap(M);W.payload=w,_!=null&&(W.callback=_),w=jp(h,W,M),w!==null&&(Io(w,h,M),Dv(w,h,M))},enqueueReplaceState:function(h,w,_){h=h._reactInternals;var M=ml(),W=Ap(M);W.tag=1,W.payload=w,_!=null&&(W.callback=_),w=jp(h,W,M),w!==null&&(Io(w,h,M),Dv(w,h,M))},enqueueForceUpdate:function(h,w){h=h._reactInternals;var _=ml(),M=Ap(_);M.tag=2,w!=null&&(M.callback=w),w=jp(h,M,_),w!==null&&(Io(w,h,_),Dv(w,h,_))}};function LU(h,w,_,M,W,ee,Se){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(M,ee,Se):w.prototype&&w.prototype.isPureReactComponent?!vs(_,M)||!vs(W,ee):!0}function RU(h,w,_,M){h=w.state,typeof w.componentWillReceiveProps=="function"&&w.componentWillReceiveProps(_,M),typeof w.UNSAFE_componentWillReceiveProps=="function"&&w.UNSAFE_componentWillReceiveProps(_,M),w.state!==h&&iM.enqueueReplaceState(w,w.state,null)}function Lf(h,w){var _=w;if("ref"in w){_={};for(var M in w)M!=="ref"&&(_[M]=w[M])}if(h=h.defaultProps){_===w&&(_=m({},_));for(var W in h)_[W]===void 0&&(_[W]=h[W])}return _}function zU(h){Ak(h)}function IU(h){console.error(h)}function OU(h){Ak(h)}function Zk(h,w){try{var _=h.onUncaughtError;_(w.value,{componentStack:w.stack})}catch(M){setTimeout(function(){throw M})}}function BU(h,w,_){try{var M=h.onCaughtError;M(_.value,{componentStack:_.stack,errorBoundary:w.tag===1?w.stateNode:null})}catch(W){setTimeout(function(){throw W})}}function sM(h,w,_){return _=Ap(_),_.tag=3,_.payload={element:null},_.callback=function(){Zk(h,w)},_}function UU(h){return h=Ap(h),h.tag=3,h}function HU(h,w,_,M){var W=_.type.getDerivedStateFromError;if(typeof W=="function"){var ee=M.value;h.payload=function(){return W(ee)},h.callback=function(){BU(w,_,M)}}var Se=_.stateNode;Se!==null&&typeof Se.componentDidCatch=="function"&&(h.callback=function(){BU(w,_,M),typeof W!="function"&&(Rp===null?Rp=new Set([this]):Rp.add(this));var He=M.stack;this.componentDidCatch(M.value,{componentStack:He!==null?He:""})})}function oue(h,w,_,M,W){if(_.flags|=32768,M!==null&&typeof M=="object"&&typeof M.then=="function"){if(w=_.alternate,w!==null&&Db(w,_,W,!0),_=ll.current,_!==null){switch(_.tag){case 31:case 13:return Yl===null?d_():_.alternate===null&&Zn===0&&(Zn=3),_.flags&=-257,_.flags|=65536,_.lanes=W,M===Ok?_.flags|=16384:(w=_.updateQueue,w===null?_.updateQueue=new Set([M]):w.add(M),MM(h,M,W)),!1;case 22:return _.flags|=65536,M===Ok?_.flags|=16384:(w=_.updateQueue,w===null?(w={transitions:null,markerInstances:null,retryQueue:new Set([M])},_.updateQueue=w):(_=w.retryQueue,_===null?w.retryQueue=new Set([M]):_.add(M)),MM(h,M,W)),!1}throw Error(n(435,_.tag))}return MM(h,M,W),d_(),!1}if(Cr)return w=ll.current,w!==null?((w.flags&65536)===0&&(w.flags|=256),w.flags|=65536,w.lanes=W,M!==Pj&&(h=Error(n(422),{cause:M}),Tv($l(h,_)))):(M!==Pj&&(w=Error(n(423),{cause:M}),Tv($l(w,_))),h=h.current.alternate,h.flags|=65536,W&=-W,h.lanes|=W,M=$l(M,_),W=sM(h.stateNode,M,W),Rj(h,W),Zn!==4&&(Zn=2)),!1;var ee=Error(n(520),{cause:M});if(ee=$l(ee,_),Wv===null?Wv=[ee]:Wv.push(ee),Zn!==4&&(Zn=2),w===null)return!0;M=$l(M,_),_=w;do{switch(_.tag){case 3:return _.flags|=65536,h=W&-W,_.lanes|=h,h=sM(_.stateNode,M,h),Rj(_,h),!1;case 1:if(w=_.type,ee=_.stateNode,(_.flags&128)===0&&(typeof w.getDerivedStateFromError=="function"||ee!==null&&typeof ee.componentDidCatch=="function"&&(Rp===null||!Rp.has(ee))))return _.flags|=65536,W&=-W,_.lanes|=W,W=UU(W),HU(W,h,_,M),Rj(_,W),!1}_=_.return}while(_!==null);return!1}var oM=Error(n(461)),Pi=!1;function ks(h,w,_,M){w.child=h===null?V5(w,null,_,M):Df(w,h.child,_,M)}function qU(h,w,_,M,W){_=_.render;var ee=w.ref;if("ref"in M){var Se={};for(var He in M)He!=="ref"&&(Se[He]=M[He])}else Se=M;return Af(w),M=Hj(h,w,_,Se,ee,W),He=qj(),h!==null&&!Pi?(Gj(h,w,W),$u(h,w,W)):(Cr&&He&&kj(w),w.flags|=1,ks(h,w,M,W),w.child)}function GU(h,w,_,M,W){if(h===null){var ee=_.type;return typeof ee=="function"&&!vj(ee)&&ee.defaultProps===void 0&&_.compare===null?(w.tag=15,w.type=ee,$U(h,w,ee,M,W)):(h=Dk(_.type,null,M,w,w.mode,W),h.ref=w.ref,h.return=w,w.child=h)}if(ee=h.child,!fM(h,W)){var Se=ee.memoizedProps;if(_=_.compare,_=_!==null?_:vs,_(Se,M)&&h.ref===w.ref)return $u(h,w,W)}return w.flags|=1,h=Ou(ee,M),h.ref=w.ref,h.return=w,w.child=h}function $U(h,w,_,M,W){if(h!==null){var ee=h.memoizedProps;if(vs(ee,M)&&h.ref===w.ref)if(Pi=!1,w.pendingProps=M=ee,fM(h,W))(h.flags&131072)!==0&&(Pi=!0);else return w.lanes=h.lanes,$u(h,w,W)}return lM(h,w,_,M,W)}function VU(h,w,_,M){var W=M.children,ee=h!==null?h.memoizedState:null;if(h===null&&w.stateNode===null&&(w.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),M.mode==="hidden"){if((w.flags&128)!==0){if(ee=ee!==null?ee.baseLanes|_:_,h!==null){for(M=w.child=h.child,W=0;M!==null;)W=W|M.lanes|M.childLanes,M=M.sibling;M=W&~ee}else M=0,w.child=null;return WU(h,w,ee,_,M)}if((_&536870912)!==0)w.memoizedState={baseLanes:0,cachePool:null},h!==null&&zk(w,ee!==null?ee.cachePool:null),ee!==null?Y5(w,ee):Ij(),Q5(w);else return M=w.lanes=536870912,WU(h,w,ee!==null?ee.baseLanes|_:_,_,M)}else ee!==null?(zk(w,ee.cachePool),Y5(w,ee),Ep(),w.memoizedState=null):(h!==null&&zk(w,null),Ij(),Ep());return ks(h,w,W,_),w.child}function Bv(h,w){return h!==null&&h.tag===22||w.stateNode!==null||(w.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),w.sibling}function WU(h,w,_,M,W){var ee=Ej();return ee=ee===null?null:{parent:ki._currentValue,pool:ee},w.memoizedState={baseLanes:_,cachePool:ee},h!==null&&zk(w,null),Ij(),Q5(w),h!==null&&Db(h,w,M,!0),w.childLanes=W,null}function Jk(h,w){return w=t_({mode:w.mode,children:w.children},h.mode),w.ref=h.ref,h.child=w,w.return=h,w}function KU(h,w,_){return Df(w,h.child,null,_),h=Jk(w,w.pendingProps),h.flags|=2,cl(w),w.memoizedState=null,h}function lue(h,w,_){var M=w.pendingProps,W=(w.flags&128)!==0;if(w.flags&=-129,h===null){if(Cr){if(M.mode==="hidden")return h=Jk(w,M),w.lanes=536870912,Bv(null,h);if(Bj(w),(h=Pn)?(h=sq(h,Kl),h=h!==null&&h.data==="&"?h:null,h!==null&&(w.memoizedState={dehydrated:h,treeContext:_p!==null?{id:Dd,overflow:Fd}:null,retryLane:536870912,hydrationErrors:null},_=M5(h),_.return=w,w.child=_,ws=w,Pn=null)):h=null,h===null)throw Cp(w);return w.lanes=536870912,null}return Jk(w,M)}var ee=h.memoizedState;if(ee!==null){var Se=ee.dehydrated;if(Bj(w),W)if(w.flags&256)w.flags&=-257,w=KU(h,w,_);else if(w.memoizedState!==null)w.child=h.child,w.flags|=128,w=null;else throw Error(n(558));else if(Pi||Db(h,w,_,!1),W=(_&h.childLanes)!==0,Pi||W){if(M=hn,M!==null&&(Se=gt(M,_),Se!==0&&Se!==ee.retryLane))throw ee.retryLane=Se,Pf(h,Se),Io(M,h,Se),oM;d_(),w=KU(h,w,_)}else h=ee.treeContext,Pn=Ql(Se.nextSibling),ws=w,Cr=!0,Pp=null,Kl=!1,h!==null&&F5(w,h),w=Jk(w,M),w.flags|=4096;return w}return h=Ou(h.child,{mode:M.mode,children:M.children}),h.ref=w.ref,w.child=h,h.return=w,h}function e_(h,w){var _=w.ref;if(_===null)h!==null&&h.ref!==null&&(w.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(n(284));(h===null||h.ref!==_)&&(w.flags|=4194816)}}function lM(h,w,_,M,W){return Af(w),_=Hj(h,w,_,M,void 0,W),M=qj(),h!==null&&!Pi?(Gj(h,w,W),$u(h,w,W)):(Cr&&M&&kj(w),w.flags|=1,ks(h,w,_,W),w.child)}function YU(h,w,_,M,W,ee){return Af(w),w.updateQueue=null,_=Z5(w,M,_,W),X5(h),M=qj(),h!==null&&!Pi?(Gj(h,w,ee),$u(h,w,ee)):(Cr&&M&&kj(w),w.flags|=1,ks(h,w,_,ee),w.child)}function QU(h,w,_,M,W){if(Af(w),w.stateNode===null){var ee=Ab,Se=_.contextType;typeof Se=="object"&&Se!==null&&(ee=Ss(Se)),ee=new _(M,ee),w.memoizedState=ee.state!==null&&ee.state!==void 0?ee.state:null,ee.updater=iM,w.stateNode=ee,ee._reactInternals=w,ee=w.stateNode,ee.props=M,ee.state=w.memoizedState,ee.refs={},Fj(w),Se=_.contextType,ee.context=typeof Se=="object"&&Se!==null?Ss(Se):Ab,ee.state=w.memoizedState,Se=_.getDerivedStateFromProps,typeof Se=="function"&&(nM(w,_,Se,M),ee.state=w.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof ee.getSnapshotBeforeUpdate=="function"||typeof ee.UNSAFE_componentWillMount!="function"&&typeof ee.componentWillMount!="function"||(Se=ee.state,typeof ee.componentWillMount=="function"&&ee.componentWillMount(),typeof ee.UNSAFE_componentWillMount=="function"&&ee.UNSAFE_componentWillMount(),Se!==ee.state&&iM.enqueueReplaceState(ee,ee.state,null),Lv(w,M,ee,W),Fv(),ee.state=w.memoizedState),typeof ee.componentDidMount=="function"&&(w.flags|=4194308),M=!0}else if(h===null){ee=w.stateNode;var He=w.memoizedProps,pt=Lf(_,He);ee.props=pt;var Et=ee.context,qt=_.contextType;Se=Ab,typeof qt=="object"&&qt!==null&&(Se=Ss(qt));var Wt=_.getDerivedStateFromProps;qt=typeof Wt=="function"||typeof ee.getSnapshotBeforeUpdate=="function",He=w.pendingProps!==He,qt||typeof ee.UNSAFE_componentWillReceiveProps!="function"&&typeof ee.componentWillReceiveProps!="function"||(He||Et!==Se)&&RU(w,ee,M,Se),Tp=!1;var Lt=w.memoizedState;ee.state=Lt,Lv(w,M,ee,W),Fv(),Et=w.memoizedState,He||Lt!==Et||Tp?(typeof Wt=="function"&&(nM(w,_,Wt,M),Et=w.memoizedState),(pt=Tp||LU(w,_,pt,M,Lt,Et,Se))?(qt||typeof ee.UNSAFE_componentWillMount!="function"&&typeof ee.componentWillMount!="function"||(typeof ee.componentWillMount=="function"&&ee.componentWillMount(),typeof ee.UNSAFE_componentWillMount=="function"&&ee.UNSAFE_componentWillMount()),typeof ee.componentDidMount=="function"&&(w.flags|=4194308)):(typeof ee.componentDidMount=="function"&&(w.flags|=4194308),w.memoizedProps=M,w.memoizedState=Et),ee.props=M,ee.state=Et,ee.context=Se,M=pt):(typeof ee.componentDidMount=="function"&&(w.flags|=4194308),M=!1)}else{ee=w.stateNode,Lj(h,w),Se=w.memoizedProps,qt=Lf(_,Se),ee.props=qt,Wt=w.pendingProps,Lt=ee.context,Et=_.contextType,pt=Ab,typeof Et=="object"&&Et!==null&&(pt=Ss(Et)),He=_.getDerivedStateFromProps,(Et=typeof He=="function"||typeof ee.getSnapshotBeforeUpdate=="function")||typeof ee.UNSAFE_componentWillReceiveProps!="function"&&typeof ee.componentWillReceiveProps!="function"||(Se!==Wt||Lt!==pt)&&RU(w,ee,M,pt),Tp=!1,Lt=w.memoizedState,ee.state=Lt,Lv(w,M,ee,W),Fv();var Bt=w.memoizedState;Se!==Wt||Lt!==Bt||Tp||h!==null&&h.dependencies!==null&&Lk(h.dependencies)?(typeof He=="function"&&(nM(w,_,He,M),Bt=w.memoizedState),(qt=Tp||LU(w,_,qt,M,Lt,Bt,pt)||h!==null&&h.dependencies!==null&&Lk(h.dependencies))?(Et||typeof ee.UNSAFE_componentWillUpdate!="function"&&typeof ee.componentWillUpdate!="function"||(typeof ee.componentWillUpdate=="function"&&ee.componentWillUpdate(M,Bt,pt),typeof ee.UNSAFE_componentWillUpdate=="function"&&ee.UNSAFE_componentWillUpdate(M,Bt,pt)),typeof ee.componentDidUpdate=="function"&&(w.flags|=4),typeof ee.getSnapshotBeforeUpdate=="function"&&(w.flags|=1024)):(typeof ee.componentDidUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(w.flags|=4),typeof ee.getSnapshotBeforeUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(w.flags|=1024),w.memoizedProps=M,w.memoizedState=Bt),ee.props=M,ee.state=Bt,ee.context=pt,M=qt):(typeof ee.componentDidUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(w.flags|=4),typeof ee.getSnapshotBeforeUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(w.flags|=1024),M=!1)}return ee=M,e_(h,w),M=(w.flags&128)!==0,ee||M?(ee=w.stateNode,_=M&&typeof _.getDerivedStateFromError!="function"?null:ee.render(),w.flags|=1,h!==null&&M?(w.child=Df(w,h.child,null,W),w.child=Df(w,null,_,W)):ks(h,w,_,W),w.memoizedState=ee.state,h=w.child):h=$u(h,w,W),h}function XU(h,w,_,M){return Nf(),w.flags|=256,ks(h,w,_,M),w.child}var cM={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function dM(h){return{baseLanes:h,cachePool:B5()}}function uM(h,w,_){return h=h!==null?h.childLanes&~_:0,w&&(h|=ul),h}function ZU(h,w,_){var M=w.pendingProps,W=!1,ee=(w.flags&128)!==0,Se;if((Se=ee)||(Se=h!==null&&h.memoizedState===null?!1:(pi.current&2)!==0),Se&&(W=!0,w.flags&=-129),Se=(w.flags&32)!==0,w.flags&=-33,h===null){if(Cr){if(W?Mp(w):Ep(),(h=Pn)?(h=sq(h,Kl),h=h!==null&&h.data!=="&"?h:null,h!==null&&(w.memoizedState={dehydrated:h,treeContext:_p!==null?{id:Dd,overflow:Fd}:null,retryLane:536870912,hydrationErrors:null},_=M5(h),_.return=w,w.child=_,ws=w,Pn=null)):h=null,h===null)throw Cp(w);return WM(h)?w.lanes=32:w.lanes=536870912,null}var He=M.children;return M=M.fallback,W?(Ep(),W=w.mode,He=t_({mode:"hidden",children:He},W),M=Cf(M,W,_,null),He.return=w,M.return=w,He.sibling=M,w.child=He,M=w.child,M.memoizedState=dM(_),M.childLanes=uM(h,Se,_),w.memoizedState=cM,Bv(null,M)):(Mp(w),mM(w,He))}var pt=h.memoizedState;if(pt!==null&&(He=pt.dehydrated,He!==null)){if(ee)w.flags&256?(Mp(w),w.flags&=-257,w=pM(h,w,_)):w.memoizedState!==null?(Ep(),w.child=h.child,w.flags|=128,w=null):(Ep(),He=M.fallback,W=w.mode,M=t_({mode:"visible",children:M.children},W),He=Cf(He,W,_,null),He.flags|=2,M.return=w,He.return=w,M.sibling=He,w.child=M,Df(w,h.child,null,_),M=w.child,M.memoizedState=dM(_),M.childLanes=uM(h,Se,_),w.memoizedState=cM,w=Bv(null,M));else if(Mp(w),WM(He)){if(Se=He.nextSibling&&He.nextSibling.dataset,Se)var Et=Se.dgst;Se=Et,M=Error(n(419)),M.stack="",M.digest=Se,Tv({value:M,source:null,stack:null}),w=pM(h,w,_)}else if(Pi||Db(h,w,_,!1),Se=(_&h.childLanes)!==0,Pi||Se){if(Se=hn,Se!==null&&(M=gt(Se,_),M!==0&&M!==pt.retryLane))throw pt.retryLane=M,Pf(h,M),Io(Se,h,M),oM;VM(He)||d_(),w=pM(h,w,_)}else VM(He)?(w.flags|=192,w.child=h.child,w=null):(h=pt.treeContext,Pn=Ql(He.nextSibling),ws=w,Cr=!0,Pp=null,Kl=!1,h!==null&&F5(w,h),w=mM(w,M.children),w.flags|=4096);return w}return W?(Ep(),He=M.fallback,W=w.mode,pt=h.child,Et=pt.sibling,M=Ou(pt,{mode:"hidden",children:M.children}),M.subtreeFlags=pt.subtreeFlags&65011712,Et!==null?He=Ou(Et,He):(He=Cf(He,W,_,null),He.flags|=2),He.return=w,M.return=w,M.sibling=He,w.child=M,Bv(null,M),M=w.child,He=h.child.memoizedState,He===null?He=dM(_):(W=He.cachePool,W!==null?(pt=ki._currentValue,W=W.parent!==pt?{parent:pt,pool:pt}:W):W=B5(),He={baseLanes:He.baseLanes|_,cachePool:W}),M.memoizedState=He,M.childLanes=uM(h,Se,_),w.memoizedState=cM,Bv(h.child,M)):(Mp(w),_=h.child,h=_.sibling,_=Ou(_,{mode:"visible",children:M.children}),_.return=w,_.sibling=null,h!==null&&(Se=w.deletions,Se===null?(w.deletions=[h],w.flags|=16):Se.push(h)),w.child=_,w.memoizedState=null,_)}function mM(h,w){return w=t_({mode:"visible",children:w},h.mode),w.return=h,h.child=w}function t_(h,w){return h=ol(22,h,null,w),h.lanes=0,h}function pM(h,w,_){return Df(w,h.child,null,_),h=mM(w,w.pendingProps.children),h.flags|=2,w.memoizedState=null,h}function JU(h,w,_){h.lanes|=w;var M=h.alternate;M!==null&&(M.lanes|=w),Tj(h.return,w,_)}function hM(h,w,_,M,W,ee){var Se=h.memoizedState;Se===null?h.memoizedState={isBackwards:w,rendering:null,renderingStartTime:0,last:M,tail:_,tailMode:W,treeForkCount:ee}:(Se.isBackwards=w,Se.rendering=null,Se.renderingStartTime=0,Se.last=M,Se.tail=_,Se.tailMode=W,Se.treeForkCount=ee)}function eH(h,w,_){var M=w.pendingProps,W=M.revealOrder,ee=M.tail;M=M.children;var Se=pi.current,He=(Se&2)!==0;if(He?(Se=Se&1|2,w.flags|=128):Se&=1,ue(pi,Se),ks(h,w,M,_),M=Cr?Nv:0,!He&&h!==null&&(h.flags&128)!==0)e:for(h=w.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&JU(h,_,w);else if(h.tag===19)JU(h,_,w);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===w)break e;for(;h.sibling===null;){if(h.return===null||h.return===w)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(W){case"forwards":for(_=w.child,W=null;_!==null;)h=_.alternate,h!==null&&qk(h)===null&&(W=_),_=_.sibling;_=W,_===null?(W=w.child,w.child=null):(W=_.sibling,_.sibling=null),hM(w,!1,W,_,ee,M);break;case"backwards":case"unstable_legacy-backwards":for(_=null,W=w.child,w.child=null;W!==null;){if(h=W.alternate,h!==null&&qk(h)===null){w.child=W;break}h=W.sibling,W.sibling=_,_=W,W=h}hM(w,!0,_,null,ee,M);break;case"together":hM(w,!1,null,null,void 0,M);break;default:w.memoizedState=null}return w.child}function $u(h,w,_){if(h!==null&&(w.dependencies=h.dependencies),Lp|=w.lanes,(_&w.childLanes)===0)if(h!==null){if(Db(h,w,_,!1),(_&w.childLanes)===0)return null}else return null;if(h!==null&&w.child!==h.child)throw Error(n(153));if(w.child!==null){for(h=w.child,_=Ou(h,h.pendingProps),w.child=_,_.return=w;h.sibling!==null;)h=h.sibling,_=_.sibling=Ou(h,h.pendingProps),_.return=w;_.sibling=null}return w.child}function fM(h,w){return(h.lanes&w)!==0?!0:(h=h.dependencies,!!(h!==null&&Lk(h)))}function cue(h,w,_){switch(w.tag){case 3:A(w,w.stateNode.containerInfo),Np(w,ki,h.memoizedState.cache),Nf();break;case 27:case 5:U(w);break;case 4:A(w,w.stateNode.containerInfo);break;case 10:Np(w,w.type,w.memoizedProps.value);break;case 31:if(w.memoizedState!==null)return w.flags|=128,Bj(w),null;break;case 13:var M=w.memoizedState;if(M!==null)return M.dehydrated!==null?(Mp(w),w.flags|=128,null):(_&w.child.childLanes)!==0?ZU(h,w,_):(Mp(w),h=$u(h,w,_),h!==null?h.sibling:null);Mp(w);break;case 19:var W=(h.flags&128)!==0;if(M=(_&w.childLanes)!==0,M||(Db(h,w,_,!1),M=(_&w.childLanes)!==0),W){if(M)return eH(h,w,_);w.flags|=128}if(W=w.memoizedState,W!==null&&(W.rendering=null,W.tail=null,W.lastEffect=null),ue(pi,pi.current),M)break;return null;case 22:return w.lanes=0,VU(h,w,_,w.pendingProps);case 24:Np(w,ki,h.memoizedState.cache)}return $u(h,w,_)}function tH(h,w,_){if(h!==null)if(h.memoizedProps!==w.pendingProps)Pi=!0;else{if(!fM(h,_)&&(w.flags&128)===0)return Pi=!1,cue(h,w,_);Pi=(h.flags&131072)!==0}else Pi=!1,Cr&&(w.flags&1048576)!==0&&D5(w,Nv,w.index);switch(w.lanes=0,w.tag){case 16:e:{var M=w.pendingProps;if(h=Mf(w.elementType),w.type=h,typeof h=="function")vj(h)?(M=Lf(h,M),w.tag=1,w=QU(null,w,h,M,_)):(w.tag=0,w=lM(null,w,h,M,_));else{if(h!=null){var W=h.$$typeof;if(W===C){w.tag=11,w=qU(null,w,h,M,_);break e}else if(W===j){w.tag=14,w=GU(null,w,h,M,_);break e}}throw w=R(h)||h,Error(n(306,w,""))}}return w;case 0:return lM(h,w,w.type,w.pendingProps,_);case 1:return M=w.type,W=Lf(M,w.pendingProps),QU(h,w,M,W,_);case 3:e:{if(A(w,w.stateNode.containerInfo),h===null)throw Error(n(387));M=w.pendingProps;var ee=w.memoizedState;W=ee.element,Lj(h,w),Lv(w,M,null,_);var Se=w.memoizedState;if(M=Se.cache,Np(w,ki,M),M!==ee.cache&&Aj(w,[ki],_,!0),Fv(),M=Se.element,ee.isDehydrated)if(ee={element:M,isDehydrated:!1,cache:Se.cache},w.updateQueue.baseState=ee,w.memoizedState=ee,w.flags&256){w=XU(h,w,M,_);break e}else if(M!==W){W=$l(Error(n(424)),w),Tv(W),w=XU(h,w,M,_);break e}else for(h=w.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,Pn=Ql(h.firstChild),ws=w,Cr=!0,Pp=null,Kl=!0,_=V5(w,null,M,_),w.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Nf(),M===W){w=$u(h,w,_);break e}ks(h,w,M,_)}w=w.child}return w;case 26:return e_(h,w),h===null?(_=mq(w.type,null,w.pendingProps,null))?w.memoizedState=_:Cr||(_=w.type,h=w.pendingProps,M=b_(G.current).createElement(_),M[vt]=w,M[Pt]=h,_s(M,_,h),nt(M),w.stateNode=M):w.memoizedState=mq(w.type,h.memoizedProps,w.pendingProps,h.memoizedState),null;case 27:return U(w),h===null&&Cr&&(M=w.stateNode=cq(w.type,w.pendingProps,G.current),ws=w,Kl=!0,W=Pn,Bp(w.type)?(KM=W,Pn=Ql(M.firstChild)):Pn=W),ks(h,w,w.pendingProps.children,_),e_(h,w),h===null&&(w.flags|=4194304),w.child;case 5:return h===null&&Cr&&((W=M=Pn)&&(M=Oue(M,w.type,w.pendingProps,Kl),M!==null?(w.stateNode=M,ws=w,Pn=Ql(M.firstChild),Kl=!1,W=!0):W=!1),W||Cp(w)),U(w),W=w.type,ee=w.pendingProps,Se=h!==null?h.memoizedProps:null,M=ee.children,qM(W,ee)?M=null:Se!==null&&qM(W,Se)&&(w.flags|=32),w.memoizedState!==null&&(W=Hj(h,w,eue,null,null,_),t0._currentValue=W),e_(h,w),ks(h,w,M,_),w.child;case 6:return h===null&&Cr&&((h=_=Pn)&&(_=Bue(_,w.pendingProps,Kl),_!==null?(w.stateNode=_,ws=w,Pn=null,h=!0):h=!1),h||Cp(w)),null;case 13:return ZU(h,w,_);case 4:return A(w,w.stateNode.containerInfo),M=w.pendingProps,h===null?w.child=Df(w,null,M,_):ks(h,w,M,_),w.child;case 11:return qU(h,w,w.type,w.pendingProps,_);case 7:return ks(h,w,w.pendingProps,_),w.child;case 8:return ks(h,w,w.pendingProps.children,_),w.child;case 12:return ks(h,w,w.pendingProps.children,_),w.child;case 10:return M=w.pendingProps,Np(w,w.type,M.value),ks(h,w,M.children,_),w.child;case 9:return W=w.type._context,M=w.pendingProps.children,Af(w),W=Ss(W),M=M(W),w.flags|=1,ks(h,w,M,_),w.child;case 14:return GU(h,w,w.type,w.pendingProps,_);case 15:return $U(h,w,w.type,w.pendingProps,_);case 19:return eH(h,w,_);case 31:return lue(h,w,_);case 22:return VU(h,w,_,w.pendingProps);case 24:return Af(w),M=Ss(ki),h===null?(W=Ej(),W===null&&(W=hn,ee=jj(),W.pooledCache=ee,ee.refCount++,ee!==null&&(W.pooledCacheLanes|=_),W=ee),w.memoizedState={parent:M,cache:W},Fj(w),Np(w,ki,W)):((h.lanes&_)!==0&&(Lj(h,w),Lv(w,null,null,_),Fv()),W=h.memoizedState,ee=w.memoizedState,W.parent!==M?(W={parent:M,cache:M},w.memoizedState=W,w.lanes===0&&(w.memoizedState=w.updateQueue.baseState=W),Np(w,ki,M)):(M=ee.cache,Np(w,ki,M),M!==W.cache&&Aj(w,[ki],_,!0))),ks(h,w,w.pendingProps.children,_),w.child;case 29:throw w.pendingProps}throw Error(n(156,w.tag))}function Vu(h){h.flags|=4}function gM(h,w,_,M,W){if((w=(h.mode&32)!==0)&&(w=!1),w){if(h.flags|=16777216,(W&335544128)===W)if(h.stateNode.complete)h.flags|=8192;else if(TH())h.flags|=8192;else throw Ef=Ok,Dj}else h.flags&=-16777217}function aH(h,w){if(w.type!=="stylesheet"||(w.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!bq(w))if(TH())h.flags|=8192;else throw Ef=Ok,Dj}function a_(h,w){w!==null&&(h.flags|=4),h.flags&16384&&(w=h.tag!==22?We():536870912,h.lanes|=w,$b|=w)}function Uv(h,w){if(!Cr)switch(h.tailMode){case"hidden":w=h.tail;for(var _=null;w!==null;)w.alternate!==null&&(_=w),w=w.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var M=null;_!==null;)_.alternate!==null&&(M=_),_=_.sibling;M===null?w||h.tail===null?h.tail=null:h.tail.sibling=null:M.sibling=null}}function Cn(h){var w=h.alternate!==null&&h.alternate.child===h.child,_=0,M=0;if(w)for(var W=h.child;W!==null;)_|=W.lanes|W.childLanes,M|=W.subtreeFlags&65011712,M|=W.flags&65011712,W.return=h,W=W.sibling;else for(W=h.child;W!==null;)_|=W.lanes|W.childLanes,M|=W.subtreeFlags,M|=W.flags,W.return=h,W=W.sibling;return h.subtreeFlags|=M,h.childLanes=_,w}function due(h,w,_){var M=w.pendingProps;switch(_j(w),w.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Cn(w),null;case 1:return Cn(w),null;case 3:return _=w.stateNode,M=null,h!==null&&(M=h.memoizedState.cache),w.memoizedState.cache!==M&&(w.flags|=2048),Hu(ki),F(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(Eb(w)?Vu(w):h===null||h.memoizedState.isDehydrated&&(w.flags&256)===0||(w.flags|=1024,Cj())),Cn(w),null;case 26:var W=w.type,ee=w.memoizedState;return h===null?(Vu(w),ee!==null?(Cn(w),aH(w,ee)):(Cn(w),gM(w,W,null,M,_))):ee?ee!==h.memoizedState?(Vu(w),Cn(w),aH(w,ee)):(Cn(w),w.flags&=-16777217):(h=h.memoizedProps,h!==M&&Vu(w),Cn(w),gM(w,W,h,M,_)),null;case 27:if(V(w),_=G.current,W=w.type,h!==null&&w.stateNode!=null)h.memoizedProps!==M&&Vu(w);else{if(!M){if(w.stateNode===null)throw Error(n(166));return Cn(w),null}h=me.current,Eb(w)?L5(w):(h=cq(W,M,_),w.stateNode=h,Vu(w))}return Cn(w),null;case 5:if(V(w),W=w.type,h!==null&&w.stateNode!=null)h.memoizedProps!==M&&Vu(w);else{if(!M){if(w.stateNode===null)throw Error(n(166));return Cn(w),null}if(ee=me.current,Eb(w))L5(w);else{var Se=b_(G.current);switch(ee){case 1:ee=Se.createElementNS("http://www.w3.org/2000/svg",W);break;case 2:ee=Se.createElementNS("http://www.w3.org/1998/Math/MathML",W);break;default:switch(W){case"svg":ee=Se.createElementNS("http://www.w3.org/2000/svg",W);break;case"math":ee=Se.createElementNS("http://www.w3.org/1998/Math/MathML",W);break;case"script":ee=Se.createElement("div"),ee.innerHTML=" +