fix(inventory): a used weight entered for a new spool is kept; Spoolman last_used means print use; the slot K picker accounts for nozzle flow

- Create, Add copies and CSV import lock a used weight above 0 (an
  explicit weight_locked wins); a negative used weight is refused on create
  and edit, more than the label is still taken.
- update_spool no longer stamps last_used: syncs, location changes and
  weighing are not use. Print usage goes through Spoolman's /use, which sets it.
- ConfigureAmsSlotModal matches K-profiles to the fitted nozzle's flow
  (others under Other) and labels each [HF] / [S]; the spool form uses the
  same flowApplies helper.
This commit is contained in:
maziggy
2026-10-18 11:35:56 +02:00
parent 346b58ed8f
commit f7bd06641a
64 changed files with 270 additions and 75 deletions
+3
View File
@@ -164,6 +164,9 @@ All notable changes to Bambuddy will be documented in this file.
- **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
### Fixed
- **A used weight entered for a new spool is kept** — Adding a spool (one, several with **Add copies**, or from a CSV) left its weight unlocked even when a used weight was entered, so the automatic AMS sync could overwrite a part-used spool's weight on its first pass. A used weight entered at creation now locks the spool, as editing the weight already did. A negative used weight is refused; more than the label weight is still accepted, since a mis-labelled spool really can give more.
- **Spoolman's "last used" means a print used the spool** — Every AMS sync stamped *last used* on every loaded Spoolman spool, as did clearing a removed spool's location and weighing on the SpoolBuddy scale. Only print usage sets it now (through Spoolman's own usage endpoint), as in built-in inventory.
- **The AMS slot K-profile picker accounts for the nozzle's flow** — On printers that report a high-flow or standard nozzle (H2-series), the picker offered and pre-selected K-profiles measured on the other kind, which are not applied when the slot is configured. They now sit under **Other**, and every profile shows its flow ([HF] / [S]), as in the spool form.
- **Printer temperature history retention can be set, and no retention setting can wipe history** — How long the nozzle, bed and chamber temperature history is kept had no setting in the interface; it now sits next to the AMS history under **Settings → Filament → History Retention**. Both settings take at least one day: 0 or less was accepted through the API and made the daily cleanup delete all history, and a value stored that way is now read as one day.
- **Bug reports no longer keep the reporter's email** — Every report sent from Bambuddy was also stored locally with the email it was sent with, and nothing ever read or removed it. The email still goes with the report; the local record keeps only the outcome, and emails stored before are cleared at startup.
- **Unused API pieces removed and the rest documented** — The firmware upload no longer broadcasts a WebSocket progress message nothing listened to (the page polls the upload status, as before); the old route that deleted every plate-detection calibration of a printer at once is gone (references are deleted one at a time from the plate-detection dialog); `/spoolbuddy/calibration` now opens the SpoolBuddy settings page, where calibration lives. The wiki now documents `POST /archives/upload-source` for slicer post-processing scripts and `PUT /local-presets/{id}`.
+13 -1
View File
@@ -1480,7 +1480,8 @@ async def import_spools_csv(
# the other create paths: the file is authoritative. A CSV that
# leaves the column blank is stating "no number", not asking for
# one to be guessed from whatever else is in the inventory.
spool = Spool(**row.spool)
# The file has no lock column: a used weight in it is locked.
spool = Spool(**_lock_entered_weight(dict(row.spool), set()))
db.add(spool)
# Supplier assignments resolved by name during parsing (#2988).
# Flush first so the spool has an id to hang the links on.
@@ -1565,6 +1566,15 @@ async def get_spool(
return spool
def _lock_entered_weight(payload: dict, fields_set: set[str]) -> dict:
"""A used weight entered for a new spool is locked, as an edited one is:
otherwise the AMS remain% sync can overwrite it on its first pass. An
explicit ``weight_locked`` from the client wins."""
if (payload.get("weight_used") or 0) > 0 and "weight_locked" not in fields_set:
payload["weight_locked"] = True
return payload
@router.post("/spools", response_model=SpoolResponse)
async def create_spool(
spool_data: SpoolCreate,
@@ -1576,6 +1586,7 @@ async def create_spool(
payload = await prepare_internal_spool_payload(db, spool_data.model_dump(), set(spool_data.model_fields_set))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
payload = _lock_entered_weight(payload, set(spool_data.model_fields_set))
# A new spool of an already-numbered product inherits its material number (#2870).
payload = await apply_material_number_inheritance(db, payload)
spool = Spool(**payload)
@@ -1604,6 +1615,7 @@ async def bulk_create_spools(
payload = await prepare_internal_spool_payload(db, data.spool.model_dump(), fields_set)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
payload = _lock_entered_weight(payload, fields_set)
# A new spool of an already-numbered product inherits its material number (#2870).
payload = await apply_material_number_inheritance(db, payload)
for _ in range(data.quantity):
+5 -2
View File
@@ -167,7 +167,10 @@ class SpoolBase(BaseModel):
class SpoolCreate(SpoolBase):
pass
# Never negative from a client; more than the label is possible (print
# tracking of a mis-labelled spool). Here, not on SpoolBase: a response
# must still serialise a value stored before there was a bound.
weight_used: float = Field(default=0, ge=0)
class SpoolBulkCreate(BaseModel):
@@ -198,7 +201,7 @@ class SpoolUpdate(IgnoresNullForRequiredColumns):
label_weight: int | None = None
core_weight: int | None = None
core_weight_catalog_id: int | None = None
weight_used: float | None = None
weight_used: float | None = Field(default=None, ge=0)
slicer_filament: str | None = None
slicer_filament_name: str | None = None
nozzle_temp_min: int | None = None
+7 -5
View File
@@ -7,7 +7,6 @@ import math
import time
import weakref
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Literal
import httpx
@@ -458,7 +457,12 @@ class SpoolmanClient:
clear_location: bool = False,
extra: dict | None = None,
) -> dict:
"""Update an existing spool in Spoolman, always setting last_used."""
"""Update an existing spool in Spoolman.
Leaves ``last_used`` alone: syncs, location changes and weighing are not
use. Print usage goes through ``use_spool`` (Spoolman's /use endpoint),
which sets it.
"""
data: dict = {}
if remaining_weight is not None:
data["remaining_weight"] = remaining_weight
@@ -469,8 +473,6 @@ class SpoolmanClient:
if extra:
data["extra"] = extra
await self._ensure_extra_fields(extra)
data["last_used"] = datetime.now(timezone.utc).isoformat()
response = await self._request_spool("PATCH", spool_id, json_body=data, operation="update")
return response.json()
@@ -741,7 +743,7 @@ class SpoolmanClient:
clear_spool_weight: bool = False,
initial_weight: float | None = None,
) -> dict:
"""Update a spool with full field support; unlike update_spool, does not auto-set last_used."""
"""Update a spool with full field support; like update_spool, leaves last_used alone."""
data: dict = {}
if filament_id is not None:
data["filament_id"] = filament_id
@@ -0,0 +1,85 @@
"""A weight entered for a new spool is kept, and a negative one is refused.
Creating a spool (one, several, or from a CSV) left ``weight_locked`` off even
when a used weight was entered, so the AMS remain% sync could overwrite a
part-used spool's weight on its first pass; editing a spool's weight locks it.
A weight above 0 entered at creation is now locked too. And ``weight_used`` had
no lower bound in the API: a negative value gave a spool more than its label.
Print tracking may still take a spool past its label (mis-labelled spools).
"""
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from backend.app.models.spool import Spool
from backend.tests.integration.test_inventory_csv import _csv_upload
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
BASE = {"material": "PLA", "brand": "Bambu", "color_name": "White", "rgba": "FFFFFFFF", "label_weight": 1000}
async def _spools(db_session):
db_session.expire_all()
return (await db_session.execute(select(Spool).order_by(Spool.id))).scalars().all()
@pytest.mark.parametrize(("weight_used", "locked"), [(250, True), (0, False)])
async def test_a_new_spool_keeps_the_weight_entered(async_client: AsyncClient, weight_used, locked):
response = await async_client.post("/api/v1/inventory/spools", json={**BASE, "weight_used": weight_used})
assert response.status_code == 200, response.text
assert response.json()["weight_locked"] is locked
async def test_an_explicit_unlock_is_respected(async_client: AsyncClient):
response = await async_client.post(
"/api/v1/inventory/spools", json={**BASE, "weight_used": 250, "weight_locked": False}
)
assert response.json()["weight_locked"] is False
async def test_copies_keep_the_weight_entered(async_client: AsyncClient, db_session):
response = await async_client.post(
"/api/v1/inventory/spools/bulk", json={"spool": {**BASE, "weight_used": 400}, "quantity": 2}
)
assert response.status_code == 200, response.text
assert [s.weight_locked for s in await _spools(db_session)] == [True, True]
async def test_an_imported_weight_is_kept(async_client: AsyncClient, db_session):
csv_text = "material,brand,color_name,weight_used\nPLA,Bambu,White,300\nPETG,Bambu,Black,0\n"
response = await async_client.post("/api/v1/inventory/spools/import", files=_csv_upload(csv_text))
assert response.status_code == 200, response.text
by_material = {s.material: s.weight_locked for s in await _spools(db_session)}
assert by_material == {"PLA": True, "PETG": False}
async def test_a_negative_weight_is_refused(async_client: AsyncClient):
created = await async_client.post("/api/v1/inventory/spools", json={**BASE, "weight_used": -10})
assert created.status_code == 422
spool = (await async_client.post("/api/v1/inventory/spools", json=BASE)).json()
edited = await async_client.patch(f"/api/v1/inventory/spools/{spool['id']}", json={"weight_used": -1})
assert edited.status_code == 422
async def test_more_than_the_label_is_still_taken(async_client: AsyncClient):
"""A mis-labelled spool really can give more than its label."""
response = await async_client.post("/api/v1/inventory/spools", json={**BASE, "weight_used": 1100})
assert response.status_code == 200, response.text
async def test_a_negative_weight_stored_before_still_lists(async_client: AsyncClient, db_session):
db_session.add(Spool(material="PLA", label_weight=1000, core_weight=250, weight_used=-20))
await db_session.commit()
response = await async_client.get("/api/v1/inventory/spools")
assert response.status_code == 200, response.text
@@ -0,0 +1,33 @@
"""Spoolman's "last used" means a print used the spool.
``update_spool`` stamped ``last_used = now`` on every call, and it is called by
the AMS sync for every loaded spool, by clearing a removed spool's location and
by weighing on the SpoolBuddy scale: every sync marked every loaded spool as
just used. Print usage goes through Spoolman's /use endpoint, which sets
last_used itself (Spoolman's database.spool.use_weight), so update_spool no
longer touches it. Local mode already behaved this way.
"""
from unittest.mock import AsyncMock, MagicMock
import pytest
from backend.app.services.spoolman import SpoolmanClient
@pytest.mark.asyncio
@pytest.mark.parametrize(
"kwargs",
[{"remaining_weight": 500.0}, {"location": "Printer 1 AMS A"}, {"clear_location": True}],
ids=["weight", "location", "clear-location"],
)
async def test_an_update_does_not_mark_the_spool_used(kwargs):
client = SpoolmanClient("http://spoolman.invalid")
response = MagicMock()
response.json.return_value = {"id": 3}
client._request_spool = AsyncMock(return_value=response)
await client.update_spool(spool_id=3, **kwargs)
body = client._request_spool.await_args.kwargs["json_body"]
assert "last_used" not in body
@@ -29,6 +29,8 @@ vi.mock('../../api/client', () => ({
searchColors: vi.fn(),
getColorCatalog: vi.fn(),
resetAmsSlot: vi.fn(),
// The fitted nozzle's flow; none reported unless a test sets one.
getPrinterStatus: vi.fn().mockResolvedValue({}),
},
}));
@@ -80,6 +82,7 @@ const defaultProps = {
describe('ConfigureAmsSlotModal', () => {
beforeEach(() => {
vi.clearAllMocks();
(api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({});
// Mock scrollIntoView which is not available in jsdom
Element.prototype.scrollIntoView = vi.fn();
(api.getCloudSettings as ReturnType<typeof vi.fn>).mockResolvedValue(mockCloudSettings);
@@ -778,7 +781,7 @@ describe('ConfigureAmsSlotModal', () => {
// own calibration table for GFL99.
for (const profile of genericPlaProfiles) {
expect(
screen.getByRole('option', { name: `${profile.name} (K=${profile.k_value})` }),
screen.getByRole('option', { name: `[HF] ${profile.name} (K=${profile.k_value})` }),
).toBeInTheDocument();
}
});
@@ -834,7 +837,7 @@ describe('ConfigureAmsSlotModal', () => {
// Every GFL99 profile is demoted to the other group: the brand gate on
// "Bambu" still applies, and none of these names carry it.
for (const profile of genericPlaProfiles) {
const option = screen.getByRole('option', { name: `${profile.name} (K=${profile.k_value})` });
const option = screen.getByRole('option', { name: `[HF] ${profile.name} (K=${profile.k_value})` });
expect(option.closest('optgroup')).toHaveAttribute(
'label',
'Other K profiles on this printer',
@@ -1017,6 +1020,32 @@ describe('ConfigureAmsSlotModal', () => {
expect(other.parentElement?.tagName).toBe('OPTGROUP');
});
it('offers the profiles of the fitted nozzle flow, the others under Other with their flow', async () => {
// H2D with a standard nozzle fitted: the high-flow copy of the same
// filament is not applied at assign time, so it must not be the match
// the picker selects (FD-16).
(api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
nozzles: [{ nozzle_type: 'HS01', nozzle_diameter: '0.4' }],
});
(api.getKProfiles as ReturnType<typeof vi.fn>).mockResolvedValue({
profiles: [
{ ...sharedProfiles[0], extruder_id: 0, nozzle_id: 'HH00-0.4', k_value: '0.030', slot_id: 1 },
{ ...sharedProfiles[0], extruder_id: 0, nozzle_id: 'HS00-0.4', k_value: '0.020', slot_id: 2 },
],
});
render(<ConfigureAmsSlotModal {...defaultProps} slotInfo={{ ...rightHandSlot, extruderId: 0, caliIdx: null }} />);
await waitFor(() => {
expect(screen.getByRole('option', { name: /\[S\] PLA \(K=0.020\)/ })).toBeInTheDocument();
});
const matches = Array.from(screen.getByRole('combobox').querySelectorAll(':scope > option'))
.map(o => (o as HTMLOptionElement).value)
.filter(Boolean);
expect(matches).toEqual(['0|PLA|0.020']);
const highFlow = screen.getByRole('option', { name: /\[HF\] PLA \(K=0.030\)/ });
expect(highFlow.parentElement?.tagName).toBe('OPTGROUP');
});
it('names each hotend once, not two or three times', async () => {
// The label rendered kProfileNozzleSuffix twice in the matching group and
// three times under "Other", so every option on a dual-nozzle printer
@@ -1113,6 +1142,7 @@ describe('ConfigureAmsSlotModal — per-nozzle K-profiles', () => {
beforeEach(() => {
vi.clearAllMocks();
(api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({});
Element.prototype.scrollIntoView = vi.fn();
(api.getCloudSettings as ReturnType<typeof vi.fn>).mockResolvedValue(mockCloudSettings);
(api.getBuiltinFilaments as ReturnType<typeof vi.fn>).mockResolvedValue([
@@ -10,6 +10,7 @@ import { Button } from './Button';
import { getAmsLabel } from '../utils/amsHelpers';
import { getSwatchStyle } from '../utils/colors';
import { useCancellableTimeout } from '../hooks/useCancellableTimeout';
import { flowApplies, flowLabel, normaliseFlow } from '../utils/nozzleFlow';
import { useToast } from '../contexts/ToastContext';
interface SlotInfo {
@@ -111,6 +112,13 @@ function parsePresetName(name: string): { material: string; brand: string; varia
// are both offered, because such a slot can reach either nozzle. Keying on
// name+K alone made the two indistinguishable and, when their K happened to
// match, silently collapsed them into whichever came first.
// "[HF] " / "[S] " for a profile measured on a high-flow or standard nozzle;
// nothing where the printer declares no flow (an X1C declares none).
function kProfileFlowPrefix(profile: KProfile): string {
const label = flowLabel(normaliseFlow(profile.nozzle_id));
return label ? `[${label}] ` : '';
}
function kProfileOptionValue(profile: KProfile): string {
return `${profile.extruder_id ?? 0}|${profile.name}|${profile.k_value}`;
}
@@ -907,6 +915,19 @@ export function ConfigureAmsSlotModal({
[kprofilesData?.profiles]
);
// The flow of the nozzle on the slot's hotend, from the printer status the
// printer card already keeps (H2-series report HH01 / HS01; others nothing).
const { data: printerStatus } = useQuery({
queryKey: ['printerStatus', printerId],
queryFn: () => api.getPrinterStatus(printerId),
enabled: isOpen,
});
const fittedFlow = useMemo(() => {
const nozzles = printerStatus?.nozzles ?? [];
const extruder = slotInfo.extruderId ?? 0;
return normaliseFlow(nozzles[extruder]?.nozzle_type) ?? normaliseFlow(nozzles[0]?.nozzle_type);
}, [printerStatus?.nozzles, slotInfo.extruderId]);
const matchingKProfiles = useMemo(() => {
if (!kprofilesData?.profiles) return [];
if (!selectedPresetInfo) {
@@ -1010,9 +1031,14 @@ export function ConfigureAmsSlotModal({
// the list instead — the X2D case in #3044, where the second AMS's slots
// point at the first's entries — so the tagging is ignored rather than
// enforced.
const onThisNozzle = tableNamesThisHotend(kprofilesData.profiles, slotInfo.extruderId)
const onThisHotend = tableNamesThisHotend(kprofilesData.profiles, slotInfo.extruderId)
? filtered.filter(p => (p.extruder_id ?? 0) === slotInfo.extruderId)
: filtered;
// And to the flow of the nozzle fitted there: a K measured on a high-flow
// nozzle is not a fact about a standard one, and the backend does not apply
// it at assign time. Unknown flow on either side matches (an X1C reports
// none). The others stay reachable under "Other", labelled [HF] / [S].
const onThisNozzle = onThisHotend.filter(p => flowApplies(p.nozzle_id, fittedFlow));
// Deduplicate genuine duplicates — same nozzle, same name, same K.
const seen = new Map<string, KProfile>();
@@ -1045,7 +1071,7 @@ export function ConfigureAmsSlotModal({
}
return result;
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId, slotInfo.caliIdx, slotInfo.trayInfoIdx, selectedPresetId, openedPresetId]);
}, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId, slotInfo.caliIdx, slotInfo.trayInfoIdx, selectedPresetId, openedPresetId, fittedFlow]);
// Every remaining K-profile the printer holds, offered under a separate group
// after the matching ones (#2710). The matcher works off preset names and
@@ -1402,14 +1428,14 @@ export function ConfigureAmsSlotModal({
<option value="">{t('configureAmsSlot.noKProfile')}</option>
{matchingKProfiles.map((profile) => (
<option key={kProfileOptionValue(profile)} value={kProfileOptionValue(profile)}>
{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
{kProfileFlowPrefix(profile)}{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
</option>
))}
{otherKProfiles.length > 0 && (
<optgroup label={t('configureAmsSlot.otherKProfiles')}>
{otherKProfiles.map((profile) => (
<option key={kProfileOptionValue(profile)} value={kProfileOptionValue(profile)}>
{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
{kProfileFlowPrefix(profile)}{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
</option>
))}
</optgroup>
@@ -1647,14 +1673,14 @@ export function ConfigureAmsSlotModal({
<option value="">{t('configureAmsSlot.noKProfile')}</option>
{matchingKProfiles.map((profile) => (
<option key={kProfileOptionValue(profile)} value={kProfileOptionValue(profile)}>
{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
{kProfileFlowPrefix(profile)}{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
</option>
))}
{otherKProfiles.length > 0 && (
<optgroup label={t('configureAmsSlot.otherKProfiles')}>
{otherKProfiles.map((profile) => (
<option key={kProfileOptionValue(profile)} value={kProfileOptionValue(profile)}>
{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
{kProfileFlowPrefix(profile)}{profile.name} (K={profile.k_value}){kProfileNozzleSuffix(profile, isDualNozzleProfiles, t)}
</option>
))}
</optgroup>
@@ -11,7 +11,7 @@ import { hotendKey, isMatchingCalibration, presetKey } from './utils';
import { STANDARD_NOZZLE_DIAMETERS } from './constants';
import { PresetPicker } from './PresetPicker';
import { extractPresetModel, matchesPrinterModelSuffix } from '../../utils/slicerPrinterMatch';
import { flowLabel, normaliseFlow } from '../../utils/nozzleFlow';
import { flowApplies, flowLabel, normaliseFlow } from '../../utils/nozzleFlow';
import type { NozzleFlow } from '../../utils/nozzleFlow';
/**
@@ -588,7 +588,7 @@ export function PrinterProfilesSection({
// look configured and quietly do nothing.
const fitted = fittedFlow(entry, column.extruder);
const chosenFlow = normaliseFlow(chosen?.nozzle_id);
const flowMismatch = !!(fitted && chosenFlow && fitted !== chosenFlow);
const flowMismatch = !!chosen && !flowApplies(chosen.nozzle_id, fitted);
return (
<select
key={key}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,g as i,w as a}from"./client-p9vNUtaN.js";import{t as o}from"./useQuery-CDVFCu1z.js";import{a as s,t as c}from"./CameraWall-C6RI--gR.js";import{n as l}from"./AuthContext-BfmcZb9G.js";import{Bn as u,Gn as d,Vn as f}from"./index-BiT5tvve.js";var p=e(t(),1),m=a(),h=5e3,g=4,_=8,v=1,y=16,b=2,x=60;function S(e,t,n,r){let i=parseInt(e??``,10);return Number.isFinite(i)?Math.min(r,Math.max(n,i)):t}function C(e,t,n,r,i,a){return e.has(t)?S(e.get(t),r,i,a):S(localStorage.getItem(n),r,i,a)}function w(e){let t=e.get(`size`)?.toLowerCase(),n=s.indexOf(t);if(n>=0)return n+1;let r=parseInt(localStorage.getItem(`camWallTileSize`)??``,10);return r>=1&&r<=4?r:2}function T(){let{t:e}=n(),t=f(),{authEnabled:a,loading:s,user:S}=l(),[T]=d(),E=T.get(`token`),D=E!=null&&E!==``,[O,k]=(0,p.useState)(()=>C(T,`maxLive`,`camWallMaxLive`,g,v,y)),[A,j]=(0,p.useState)(()=>C(T,`interval`,`camWallSnapshotSec`,_,b,x)),[M]=(0,p.useState)(()=>w(T)),[N,P]=(0,p.useState)(()=>{let e=T.get(`status`)??localStorage.getItem(`camWallStatusMode`);return(D?[`off`,`compact`]:[`off`,`compact`,`full`]).includes(e)?e:`compact`});(0,p.useEffect)(()=>{if(D)return i(E),()=>i(null)},[D,E]);let F=o({queryKey:[`camwall-printers`,E],queryFn:()=>r.getCamWallPrinters(E??void 0),enabled:D,refetchInterval:h}),I=o({queryKey:[`printers`],queryFn:()=>r.getPrinters(),enabled:!D&&!s&&(!a||S!==null),refetchInterval:h}),L=(0,p.useMemo)(()=>{let e=new Map;for(let t of F.data??[])e.set(t.id,{connected:t.connected,state:t.state,progress:t.progress,remaining_time:t.remaining_time,layer_num:t.layer_num,total_layers:t.total_layers,hms_errors:t.hms_errors});return e},[F.data]);if(!D&&s)return(0,m.jsx)(`div`,{className:`flex min-h-screen items-center justify-center bg-bambu-dark text-bambu-gray`,children:e(`common.loading`)});if(!D&&a&&!S)return(0,m.jsx)(u,{to:`/login`,replace:!0,state:{from:t}});let R=D?F:I,z=D?F.data??[]:I.data??[];return R.isError?(0,m.jsx)(`div`,{className:`flex min-h-screen items-center justify-center bg-bambu-dark p-6 text-center`,children:(0,m.jsx)(`p`,{className:`max-w-md text-sm text-red-400`,children:e(D?`printers.camWall.page.tokenRejected`:`printers.camWall.page.loadFailed`)})}):(0,m.jsx)(`div`,{className:`min-h-screen bg-bambu-dark p-4`,children:(0,m.jsx)(c,{printers:z,maxLive:O,snapshotIntervalSec:A,statusMode:N,tileSize:M,statuses:D?L:void 0,showSettings:!D,onTileClick:D?void 0:e=>window.open(`/camera/${e}`,`camera-${e}`),onChangeMaxLive:e=>{k(e),localStorage.setItem(`camWallMaxLive`,String(e))},onChangeSnapshotIntervalSec:e=>{j(e),localStorage.setItem(`camWallSnapshotSec`,String(e))},onChangeStatusMode:e=>{P(e),localStorage.setItem(`camWallStatusMode`,e)}})})}export{T as CamWallPage,T as default};
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,g as i,w as a}from"./client-p9vNUtaN.js";import{t as o}from"./useQuery-CDVFCu1z.js";import{a as s,t as c}from"./CameraWall-K1MJhr_0.js";import{n as l}from"./AuthContext-BfmcZb9G.js";import{Bn as u,Gn as d,Vn as f}from"./index-DAlcmDFx.js";var p=e(t(),1),m=a(),h=5e3,g=4,_=8,v=1,y=16,b=2,x=60;function S(e,t,n,r){let i=parseInt(e??``,10);return Number.isFinite(i)?Math.min(r,Math.max(n,i)):t}function C(e,t,n,r,i,a){return e.has(t)?S(e.get(t),r,i,a):S(localStorage.getItem(n),r,i,a)}function w(e){let t=e.get(`size`)?.toLowerCase(),n=s.indexOf(t);if(n>=0)return n+1;let r=parseInt(localStorage.getItem(`camWallTileSize`)??``,10);return r>=1&&r<=4?r:2}function T(){let{t:e}=n(),t=f(),{authEnabled:a,loading:s,user:S}=l(),[T]=d(),E=T.get(`token`),D=E!=null&&E!==``,[O,k]=(0,p.useState)(()=>C(T,`maxLive`,`camWallMaxLive`,g,v,y)),[A,j]=(0,p.useState)(()=>C(T,`interval`,`camWallSnapshotSec`,_,b,x)),[M]=(0,p.useState)(()=>w(T)),[N,P]=(0,p.useState)(()=>{let e=T.get(`status`)??localStorage.getItem(`camWallStatusMode`);return(D?[`off`,`compact`]:[`off`,`compact`,`full`]).includes(e)?e:`compact`});(0,p.useEffect)(()=>{if(D)return i(E),()=>i(null)},[D,E]);let F=o({queryKey:[`camwall-printers`,E],queryFn:()=>r.getCamWallPrinters(E??void 0),enabled:D,refetchInterval:h}),I=o({queryKey:[`printers`],queryFn:()=>r.getPrinters(),enabled:!D&&!s&&(!a||S!==null),refetchInterval:h}),L=(0,p.useMemo)(()=>{let e=new Map;for(let t of F.data??[])e.set(t.id,{connected:t.connected,state:t.state,progress:t.progress,remaining_time:t.remaining_time,layer_num:t.layer_num,total_layers:t.total_layers,hms_errors:t.hms_errors});return e},[F.data]);if(!D&&s)return(0,m.jsx)(`div`,{className:`flex min-h-screen items-center justify-center bg-bambu-dark text-bambu-gray`,children:e(`common.loading`)});if(!D&&a&&!S)return(0,m.jsx)(u,{to:`/login`,replace:!0,state:{from:t}});let R=D?F:I,z=D?F.data??[]:I.data??[];return R.isError?(0,m.jsx)(`div`,{className:`flex min-h-screen items-center justify-center bg-bambu-dark p-6 text-center`,children:(0,m.jsx)(`p`,{className:`max-w-md text-sm text-red-400`,children:e(D?`printers.camWall.page.tokenRejected`:`printers.camWall.page.loadFailed`)})}):(0,m.jsx)(`div`,{className:`min-h-screen bg-bambu-dark p-4`,children:(0,m.jsx)(c,{printers:z,maxLive:O,snapshotIntervalSec:A,statusMode:N,tileSize:M,statuses:D?L:void 0,showSettings:!D,onTileClick:D?void 0:e=>window.open(`/camera/${e}`,`camera-${e}`),onChangeMaxLive:e=>{k(e),localStorage.setItem(`camWallMaxLive`,String(e))},onChangeSnapshotIntervalSec:e=>{j(e),localStorage.setItem(`camWallSnapshotSec`,String(e))},onChangeStatusMode:e=>{P(e),localStorage.setItem(`camWallStatusMode`,e)}})})}export{T as CamWallPage,T as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,w as r}from"./client-p9vNUtaN.js";import{a as i,i as a}from"./info-Ddwe8b0j.js";import{t as o}from"./eye-off-CLSafL7x.js";import{t as s}from"./grip-vertical-CjfVnrUE.js";import{n as c,t as l}from"./Card-DgaxYs1A.js";import{t as u}from"./Button-B7HH19dk.js";import{Cn as d,Kt as f}from"./index-BiT5tvve.js";var p=e(t(),1),m=r();function h({isOpen:e,onClose:t,columns:r,defaultColumns:h,onSave:g}){let{t:_}=n(),[v,y]=(0,p.useState)(r),[b,x]=(0,p.useState)(null),S=(0,p.useRef)(null);if((0,p.useEffect)(()=>{e&&y(r.map(e=>({...e})))},[e,r]),(0,p.useEffect)(()=>{if(!e)return;let n=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[e,t]),!e)return null;let C=e=>{y(t=>t.map((t,n)=>n===e?{...t,visible:!t.visible}:t))},w=(e,t)=>{t<0||t>=v.length||y(n=>{let r=[...n],[i]=r.splice(e,1);return r.splice(t,0,i),r})},T=(e,t)=>{S.current=t,x(t),e.dataTransfer.effectAllowed=`move`},E=(e,t)=>{e.preventDefault(),e.dataTransfer.dropEffect=`move`;let n=S.current;n!==null&&n!==t&&(w(n,t),S.current=t,x(t))},D=e=>{e.preventDefault()},O=()=>{S.current=null,x(null)},k=()=>{y(h.map(e=>({...e})))},A=()=>{g(v),t()},j=v.filter(e=>e.visible).length;return(0,m.jsx)(`div`,{className:`fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4`,onClick:t,children:(0,m.jsx)(l,{className:`w-full max-w-md max-h-[80vh] flex flex-col`,onClick:e=>e.stopPropagation(),children:(0,m.jsxs)(c,{className:`p-6 flex flex-col min-h-0`,children:[(0,m.jsx)(`h3`,{className:`text-lg font-semibold text-white mb-2`,children:_(`inventory.configureColumns`)}),(0,m.jsxs)(`p`,{className:`text-sm text-bambu-gray mb-4`,children:[_(`inventory.configureColumnsDesc`),(0,m.jsxs)(`span`,{className:`ml-2 text-bambu-gray/60`,children:[`(`,j,` `,_(`inventory.of`),` `,v.length,` `,_(`inventory.visible`),`)`]})]}),(0,m.jsx)(`div`,{className:`space-y-1 overflow-y-auto flex-1 min-h-0 pr-1`,children:v.map((e,t)=>(0,m.jsxs)(`div`,{className:`flex items-center gap-2 p-2 rounded-lg border transition-colors ${b===t?`border-bambu-green bg-bambu-green/10`:`border-bambu-dark-tertiary bg-bambu-dark-tertiary/50`} ${e.visible?``:`opacity-50`}`,draggable:!0,onDragStart:e=>T(e,t),onDragOver:e=>E(e,t),onDrop:D,onDragEnd:O,children:[(0,m.jsx)(`div`,{className:`cursor-grab text-bambu-gray/50 hover:text-bambu-gray`,children:(0,m.jsx)(s,{className:`w-4 h-4`})}),(0,m.jsx)(`span`,{className:`flex-1 font-medium text-sm text-white`,children:e.label}),(0,m.jsxs)(`div`,{className:`flex items-center gap-0.5`,children:[(0,m.jsx)(`button`,{onClick:()=>w(t,t-1),disabled:t===0,className:`p-1 rounded text-bambu-gray hover:bg-bambu-dark-secondary disabled:opacity-30 disabled:cursor-not-allowed`,title:_(`inventory.moveUp`),children:(0,m.jsx)(a,{className:`w-4 h-4`})}),(0,m.jsx)(`button`,{onClick:()=>w(t,t+1),disabled:t===v.length-1,className:`p-1 rounded text-bambu-gray hover:bg-bambu-dark-secondary disabled:opacity-30 disabled:cursor-not-allowed`,title:_(`inventory.moveDown`),children:(0,m.jsx)(i,{className:`w-4 h-4`})})]}),(0,m.jsx)(`button`,{onClick:()=>C(t),className:`p-1.5 rounded transition-colors ${e.visible?`text-bambu-green hover:bg-bambu-green/10`:`text-bambu-gray/50 hover:bg-bambu-dark-secondary`}`,title:e.visible?_(`inventory.hideColumn`):_(`inventory.showColumn`),children:e.visible?(0,m.jsx)(d,{className:`w-4 h-4`}):(0,m.jsx)(o,{className:`w-4 h-4`})})]},e.id))}),(0,m.jsxs)(`div`,{className:`flex items-center gap-3 mt-4 pt-4 border-t border-bambu-dark-tertiary`,children:[(0,m.jsxs)(u,{variant:`secondary`,onClick:k,className:`mr-auto`,children:[(0,m.jsx)(f,{className:`w-4 h-4`}),_(`inventory.reset`)]}),(0,m.jsx)(u,{variant:`secondary`,onClick:t,children:_(`inventory.cancel`)}),(0,m.jsx)(u,{onClick:A,children:_(`inventory.applyChanges`)})]})]})})})}export{h as t};
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,w as r}from"./client-p9vNUtaN.js";import{a as i,i as a}from"./info-Ddwe8b0j.js";import{t as o}from"./eye-off-CLSafL7x.js";import{t as s}from"./grip-vertical-CjfVnrUE.js";import{n as c,t as l}from"./Card-DgaxYs1A.js";import{t as u}from"./Button-B7HH19dk.js";import{Cn as d,Kt as f}from"./index-DAlcmDFx.js";var p=e(t(),1),m=r();function h({isOpen:e,onClose:t,columns:r,defaultColumns:h,onSave:g}){let{t:_}=n(),[v,y]=(0,p.useState)(r),[b,x]=(0,p.useState)(null),S=(0,p.useRef)(null);if((0,p.useEffect)(()=>{e&&y(r.map(e=>({...e})))},[e,r]),(0,p.useEffect)(()=>{if(!e)return;let n=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[e,t]),!e)return null;let C=e=>{y(t=>t.map((t,n)=>n===e?{...t,visible:!t.visible}:t))},w=(e,t)=>{t<0||t>=v.length||y(n=>{let r=[...n],[i]=r.splice(e,1);return r.splice(t,0,i),r})},T=(e,t)=>{S.current=t,x(t),e.dataTransfer.effectAllowed=`move`},E=(e,t)=>{e.preventDefault(),e.dataTransfer.dropEffect=`move`;let n=S.current;n!==null&&n!==t&&(w(n,t),S.current=t,x(t))},D=e=>{e.preventDefault()},O=()=>{S.current=null,x(null)},k=()=>{y(h.map(e=>({...e})))},A=()=>{g(v),t()},j=v.filter(e=>e.visible).length;return(0,m.jsx)(`div`,{className:`fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4`,onClick:t,children:(0,m.jsx)(l,{className:`w-full max-w-md max-h-[80vh] flex flex-col`,onClick:e=>e.stopPropagation(),children:(0,m.jsxs)(c,{className:`p-6 flex flex-col min-h-0`,children:[(0,m.jsx)(`h3`,{className:`text-lg font-semibold text-white mb-2`,children:_(`inventory.configureColumns`)}),(0,m.jsxs)(`p`,{className:`text-sm text-bambu-gray mb-4`,children:[_(`inventory.configureColumnsDesc`),(0,m.jsxs)(`span`,{className:`ml-2 text-bambu-gray/60`,children:[`(`,j,` `,_(`inventory.of`),` `,v.length,` `,_(`inventory.visible`),`)`]})]}),(0,m.jsx)(`div`,{className:`space-y-1 overflow-y-auto flex-1 min-h-0 pr-1`,children:v.map((e,t)=>(0,m.jsxs)(`div`,{className:`flex items-center gap-2 p-2 rounded-lg border transition-colors ${b===t?`border-bambu-green bg-bambu-green/10`:`border-bambu-dark-tertiary bg-bambu-dark-tertiary/50`} ${e.visible?``:`opacity-50`}`,draggable:!0,onDragStart:e=>T(e,t),onDragOver:e=>E(e,t),onDrop:D,onDragEnd:O,children:[(0,m.jsx)(`div`,{className:`cursor-grab text-bambu-gray/50 hover:text-bambu-gray`,children:(0,m.jsx)(s,{className:`w-4 h-4`})}),(0,m.jsx)(`span`,{className:`flex-1 font-medium text-sm text-white`,children:e.label}),(0,m.jsxs)(`div`,{className:`flex items-center gap-0.5`,children:[(0,m.jsx)(`button`,{onClick:()=>w(t,t-1),disabled:t===0,className:`p-1 rounded text-bambu-gray hover:bg-bambu-dark-secondary disabled:opacity-30 disabled:cursor-not-allowed`,title:_(`inventory.moveUp`),children:(0,m.jsx)(a,{className:`w-4 h-4`})}),(0,m.jsx)(`button`,{onClick:()=>w(t,t+1),disabled:t===v.length-1,className:`p-1 rounded text-bambu-gray hover:bg-bambu-dark-secondary disabled:opacity-30 disabled:cursor-not-allowed`,title:_(`inventory.moveDown`),children:(0,m.jsx)(i,{className:`w-4 h-4`})})]}),(0,m.jsx)(`button`,{onClick:()=>C(t),className:`p-1.5 rounded transition-colors ${e.visible?`text-bambu-green hover:bg-bambu-green/10`:`text-bambu-gray/50 hover:bg-bambu-dark-secondary`}`,title:e.visible?_(`inventory.hideColumn`):_(`inventory.showColumn`),children:e.visible?(0,m.jsx)(d,{className:`w-4 h-4`}):(0,m.jsx)(o,{className:`w-4 h-4`})})]},e.id))}),(0,m.jsxs)(`div`,{className:`flex items-center gap-3 mt-4 pt-4 border-t border-bambu-dark-tertiary`,children:[(0,m.jsxs)(u,{variant:`secondary`,onClick:k,className:`mr-auto`,children:[(0,m.jsx)(f,{className:`w-4 h-4`}),_(`inventory.reset`)]}),(0,m.jsx)(u,{variant:`secondary`,onClick:t,children:_(`inventory.cancel`)}),(0,m.jsx)(u,{onClick:A,children:_(`inventory.applyChanges`)})]})]})})})}export{h as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,t as i,w as a}from"./client-p9vNUtaN.js";import{t as o}from"./link-2-BAYN3M4f.js";import{t as s}from"./loader-circle-mjS43L3L.js";import{t as c}from"./triangle-alert-US3vrQEH.js";import{n as l}from"./AuthContext-BfmcZb9G.js";import{Gn as u}from"./index-BiT5tvve.js";var d=e(t(),1),f=a();function p(e,t){let n=new URL(e);for(let[e,r]of Object.entries(t))r&&n.searchParams.set(e,r);return n.toString()}function m(){let{t:e}=n(),{authEnabled:t,loading:a}=l(),[m]=u(),[h,g]=(0,d.useState)({kind:`loading`}),_=(0,d.useRef)(!1),v=m.get(`client_id`)??``,y=m.get(`redirect_uri`)??``,b=m.get(`state`)??``,x=m.get(`code_challenge`)??``,S=m.get(`code_challenge_method`)??``,C=async()=>{g({kind:`redirecting`});try{let e=await r.connectAuthorize({client_id:v,redirect_uri:y,code_challenge:x,code_challenge_method:`S256`});window.location.replace(p(e.redirect_uri,{code:e.code,state:b}))}catch(t){g({kind:`error`,message:t instanceof Error?t.message:e(`connectedApps.authorize.failed`)})}};return(0,d.useEffect)(()=>{if(!(a||_.current)){if(_.current=!0,!t){g({kind:`error`,message:e(`connectedApps.authorize.authDisabled`)});return}if(!v||!y||!x||S!==`S256`){g({kind:`error`,message:e(`connectedApps.authorize.invalidRequest`)});return}r.getConnectAuthorizeInfo(v,y).then(e=>{e.already_granted?C():g({kind:`consent`,info:e})}).catch(t=>{g({kind:`error`,message:t instanceof i&&t.status===409?e(`connectedApps.authorize.authDisabled`):e(`connectedApps.authorize.invalidRequest`)})})}},[a,t]),(0,f.jsx)(`div`,{className:`min-h-screen flex items-center justify-center bg-bambu-dark p-4`,children:(0,f.jsxs)(`div`,{className:`max-w-md w-full space-y-6 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg`,children:[(h.kind===`loading`||h.kind===`redirecting`)&&(0,f.jsxs)(`div`,{className:`flex flex-col items-center gap-3 text-bambu-gray`,role:`status`,children:[(0,f.jsx)(s,{className:`w-8 h-8 animate-spin text-bambu-green`}),(0,f.jsx)(`p`,{children:h.kind===`loading`?e(`connectedApps.authorize.checking`):e(`connectedApps.authorize.redirecting`)})]}),h.kind===`error`&&(0,f.jsxs)(`div`,{className:`text-center space-y-3`,children:[(0,f.jsx)(c,{className:`w-10 h-10 mx-auto text-yellow-600 dark:text-yellow-400`}),(0,f.jsx)(`h1`,{className:`text-xl font-semibold text-white`,children:e(`connectedApps.authorize.errorTitle`)}),(0,f.jsx)(`p`,{className:`text-sm text-bambu-gray`,children:h.message})]}),h.kind===`consent`&&(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`div`,{className:`text-center space-y-3`,children:[(0,f.jsx)(`div`,{className:`w-14 h-14 mx-auto rounded-full bg-bambu-green/20 flex items-center justify-center`,children:(0,f.jsx)(o,{className:`w-7 h-7 text-bambu-green`})}),(0,f.jsx)(`h1`,{className:`text-xl font-semibold text-white text-balance`,children:e(`connectedApps.authorize.title`,{app:h.info.app_name})}),(0,f.jsx)(`p`,{className:`text-sm text-bambu-gray`,children:e(`connectedApps.authorize.signedInAs`,{username:h.info.username})})]}),(0,f.jsx)(`p`,{className:`text-sm text-bambu-gray`,children:e(`connectedApps.authorize.sharedData`,{app:h.info.app_name})}),(0,f.jsxs)(`div`,{className:`flex gap-3`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>{window.location.replace(p(y,{error:`access_denied`,state:b}))},className:`flex-1 px-4 py-2 bg-bambu-dark-tertiary text-white rounded-md hover:bg-bambu-dark-tertiary/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-bambu-green`,children:e(`connectedApps.authorize.deny`)}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void C(),className:`flex-1 px-4 py-2 bg-bambu-green text-white rounded-md hover:bg-bambu-green/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-white`,children:e(`connectedApps.authorize.allow`)})]})]})]})})}export{m as ConnectAuthorizePage};
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,t as i,w as a}from"./client-p9vNUtaN.js";import{t as o}from"./link-2-BAYN3M4f.js";import{t as s}from"./loader-circle-mjS43L3L.js";import{t as c}from"./triangle-alert-US3vrQEH.js";import{n as l}from"./AuthContext-BfmcZb9G.js";import{Gn as u}from"./index-DAlcmDFx.js";var d=e(t(),1),f=a();function p(e,t){let n=new URL(e);for(let[e,r]of Object.entries(t))r&&n.searchParams.set(e,r);return n.toString()}function m(){let{t:e}=n(),{authEnabled:t,loading:a}=l(),[m]=u(),[h,g]=(0,d.useState)({kind:`loading`}),_=(0,d.useRef)(!1),v=m.get(`client_id`)??``,y=m.get(`redirect_uri`)??``,b=m.get(`state`)??``,x=m.get(`code_challenge`)??``,S=m.get(`code_challenge_method`)??``,C=async()=>{g({kind:`redirecting`});try{let e=await r.connectAuthorize({client_id:v,redirect_uri:y,code_challenge:x,code_challenge_method:`S256`});window.location.replace(p(e.redirect_uri,{code:e.code,state:b}))}catch(t){g({kind:`error`,message:t instanceof Error?t.message:e(`connectedApps.authorize.failed`)})}};return(0,d.useEffect)(()=>{if(!(a||_.current)){if(_.current=!0,!t){g({kind:`error`,message:e(`connectedApps.authorize.authDisabled`)});return}if(!v||!y||!x||S!==`S256`){g({kind:`error`,message:e(`connectedApps.authorize.invalidRequest`)});return}r.getConnectAuthorizeInfo(v,y).then(e=>{e.already_granted?C():g({kind:`consent`,info:e})}).catch(t=>{g({kind:`error`,message:t instanceof i&&t.status===409?e(`connectedApps.authorize.authDisabled`):e(`connectedApps.authorize.invalidRequest`)})})}},[a,t]),(0,f.jsx)(`div`,{className:`min-h-screen flex items-center justify-center bg-bambu-dark p-4`,children:(0,f.jsxs)(`div`,{className:`max-w-md w-full space-y-6 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg`,children:[(h.kind===`loading`||h.kind===`redirecting`)&&(0,f.jsxs)(`div`,{className:`flex flex-col items-center gap-3 text-bambu-gray`,role:`status`,children:[(0,f.jsx)(s,{className:`w-8 h-8 animate-spin text-bambu-green`}),(0,f.jsx)(`p`,{children:h.kind===`loading`?e(`connectedApps.authorize.checking`):e(`connectedApps.authorize.redirecting`)})]}),h.kind===`error`&&(0,f.jsxs)(`div`,{className:`text-center space-y-3`,children:[(0,f.jsx)(c,{className:`w-10 h-10 mx-auto text-yellow-600 dark:text-yellow-400`}),(0,f.jsx)(`h1`,{className:`text-xl font-semibold text-white`,children:e(`connectedApps.authorize.errorTitle`)}),(0,f.jsx)(`p`,{className:`text-sm text-bambu-gray`,children:h.message})]}),h.kind===`consent`&&(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`div`,{className:`text-center space-y-3`,children:[(0,f.jsx)(`div`,{className:`w-14 h-14 mx-auto rounded-full bg-bambu-green/20 flex items-center justify-center`,children:(0,f.jsx)(o,{className:`w-7 h-7 text-bambu-green`})}),(0,f.jsx)(`h1`,{className:`text-xl font-semibold text-white text-balance`,children:e(`connectedApps.authorize.title`,{app:h.info.app_name})}),(0,f.jsx)(`p`,{className:`text-sm text-bambu-gray`,children:e(`connectedApps.authorize.signedInAs`,{username:h.info.username})})]}),(0,f.jsx)(`p`,{className:`text-sm text-bambu-gray`,children:e(`connectedApps.authorize.sharedData`,{app:h.info.app_name})}),(0,f.jsxs)(`div`,{className:`flex gap-3`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>{window.location.replace(p(y,{error:`access_denied`,state:b}))},className:`flex-1 px-4 py-2 bg-bambu-dark-tertiary text-white rounded-md hover:bg-bambu-dark-tertiary/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-bambu-green`,children:e(`connectedApps.authorize.deny`)}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void C(),className:`flex-1 px-4 py-2 bg-bambu-green text-white rounded-md hover:bg-bambu-green/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-white`,children:e(`connectedApps.authorize.allow`)})]})]})]})})}export{m as ConnectAuthorizePage};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,w as i}from"./client-p9vNUtaN.js";import{t as a}from"./useQuery-CDVFCu1z.js";import{t as o}from"./loader-circle-mjS43L3L.js";import{t as s}from"./triangle-alert-US3vrQEH.js";import{Hn as c,Wn as l,bt as u}from"./index-BiT5tvve.js";var d=e(t(),1),f=i();function p(){let{t:e}=n(),{id:t}=l(),{mode:i}=u(),c=(0,d.useRef)(null),p=m(c);g(c);let{data:h,isLoading:_,error:v}=a({queryKey:[`external-link`,t],queryFn:()=>r.getExternalLink(Number(t)),enabled:!!t});return _?(0,f.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,f.jsx)(o,{className:`w-8 h-8 text-bambu-green animate-spin`})}):v||!h?(0,f.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-4 text-bambu-gray`,children:[(0,f.jsx)(s,{className:`w-12 h-12`}),(0,f.jsx)(`p`,{children:e(`common.linkNotFound`)})]}):(0,f.jsx)(`iframe`,{ref:c,src:h.url,onLoad:p,className:`h-full w-full border-0`,style:{colorScheme:i},title:h.name,sandbox:`allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox`})}function m(e){let{resolvedMode:t,darkStyle:n,darkBackground:r,darkAccent:i,lightStyle:a,lightBackground:o,lightAccent:s}=u(),c=t===`dark`,l=c?n:a,f=c?r:o,p=c?i:s,m=(0,d.useCallback)(()=>{let n=e.current;if(!n?.contentWindow)return;let r;try{r=new URL(n.src).origin}catch{return}n.contentWindow.postMessage({type:`bambuddy:theme`,mode:t,style:l,background:f,accent:p,canNavigate:!0},r)},[e,t,l,f,p]);return(0,d.useEffect)(()=>m(),[m]),(0,d.useEffect)(()=>{let t=t=>{t.source===e.current?.contentWindow&&t.data?.type===`bambuddy:theme-request`&&m()};return window.addEventListener(`message`,t),()=>window.removeEventListener(`message`,t)},[e,m]),m}var h=512;function g(e){let t=c();(0,d.useEffect)(()=>{let n=n=>{let r=e.current;if(!r?.contentWindow||n.source!==r.contentWindow||n.data?.type!==`bambuddy:navigate`)return;let i;try{i=new URL(r.src).origin}catch{return}if(n.origin!==i)return;let a=_(n.data.path);a!==null&&t(a)};return window.addEventListener(`message`,n),()=>window.removeEventListener(`message`,n)},[e,t])}function _(e){if(typeof e!=`string`||e.length>h||!e.startsWith(`/`)||e.startsWith(`//`)||e.includes(`\\`))return null;let t;try{t=new URL(e,window.location.origin)}catch{return null}return t.origin===window.location.origin?t.pathname+t.search+t.hash:null}export{p as ExternalLinkPage};
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,w as i}from"./client-p9vNUtaN.js";import{t as a}from"./useQuery-CDVFCu1z.js";import{t as o}from"./loader-circle-mjS43L3L.js";import{t as s}from"./triangle-alert-US3vrQEH.js";import{Hn as c,Wn as l,bt as u}from"./index-DAlcmDFx.js";var d=e(t(),1),f=i();function p(){let{t:e}=n(),{id:t}=l(),{mode:i}=u(),c=(0,d.useRef)(null),p=m(c);g(c);let{data:h,isLoading:_,error:v}=a({queryKey:[`external-link`,t],queryFn:()=>r.getExternalLink(Number(t)),enabled:!!t});return _?(0,f.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,f.jsx)(o,{className:`w-8 h-8 text-bambu-green animate-spin`})}):v||!h?(0,f.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-4 text-bambu-gray`,children:[(0,f.jsx)(s,{className:`w-12 h-12`}),(0,f.jsx)(`p`,{children:e(`common.linkNotFound`)})]}):(0,f.jsx)(`iframe`,{ref:c,src:h.url,onLoad:p,className:`h-full w-full border-0`,style:{colorScheme:i},title:h.name,sandbox:`allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox`})}function m(e){let{resolvedMode:t,darkStyle:n,darkBackground:r,darkAccent:i,lightStyle:a,lightBackground:o,lightAccent:s}=u(),c=t===`dark`,l=c?n:a,f=c?r:o,p=c?i:s,m=(0,d.useCallback)(()=>{let n=e.current;if(!n?.contentWindow)return;let r;try{r=new URL(n.src).origin}catch{return}n.contentWindow.postMessage({type:`bambuddy:theme`,mode:t,style:l,background:f,accent:p,canNavigate:!0},r)},[e,t,l,f,p]);return(0,d.useEffect)(()=>m(),[m]),(0,d.useEffect)(()=>{let t=t=>{t.source===e.current?.contentWindow&&t.data?.type===`bambuddy:theme-request`&&m()};return window.addEventListener(`message`,t),()=>window.removeEventListener(`message`,t)},[e,m]),m}var h=512;function g(e){let t=c();(0,d.useEffect)(()=>{let n=n=>{let r=e.current;if(!r?.contentWindow||n.source!==r.contentWindow||n.data?.type!==`bambuddy:navigate`)return;let i;try{i=new URL(r.src).origin}catch{return}if(n.origin!==i)return;let a=_(n.data.path);a!==null&&t(a)};return window.addEventListener(`message`,n),()=>window.removeEventListener(`message`,n)},[e,t])}function _(e){if(typeof e!=`string`||e.length>h||!e.startsWith(`/`)||e.startsWith(`//`)||e.includes(`\\`))return null;let t;try{t=new URL(e,window.location.origin)}catch{return null}return t.origin===window.location.origin?t.pathname+t.search+t.hash:null}export{p as ExternalLinkPage};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,w as i}from"./client-p9vNUtaN.js";import{t as a}from"./useQuery-CDVFCu1z.js";import{t as o}from"./arrow-left-DG60mWnu.js";import{t as s}from"./GcodeToolpathViewer-B5RO-cba.js";import{Gn as c,Hn as l}from"./index-BiT5tvve.js";var u=e(t(),1),d=i();function f(){let e=l(),[t,i]=c(),{t:f}=n(),p=t.get(`archive`),m=t.get(`library_file`),h=t.get(`plate`),g=a({queryKey:[`gcode-viewer-archive-colors`,p],queryFn:()=>r.getArchiveCapabilities(Number(p)),enabled:!!p,staleTime:5*6e4,retry:!1}),_=a({queryKey:[`gcode-viewer-library-colors`,m],queryFn:()=>r.getLibraryFilePlates(Number(m)),enabled:!!m,staleTime:5*6e4,retry:!1}),v=a({queryKey:[`gcode-viewer-archive-plates`,p],queryFn:()=>r.getArchivePlates(Number(p)),enabled:!!p,staleTime:5*6e4,retry:!1}),y=(0,u.useMemo)(()=>(p?v.data?.plates:_.data?.plates)??[],[p,v.data,_.data]),b=(0,u.useMemo)(()=>h?Number(h):y.length===0?null:Math.min(...y.map(e=>e.index)),[h,y]),x=e=>{if(e===b)return;let n=new URLSearchParams(t);n.set(`plate`,String(e)),i(n,{replace:!0})},S=(0,u.useMemo)(()=>{if(p)return g.data?.filament_colors;let e=y.find(e=>e.index===b)||y[0];if(!e?.filaments?.length)return;let t=[];for(let n of e.filaments){let e=Math.max(0,(n.slot_id??1)-1);n.color&&(t[e]=n.color)}return t.length>0?t:void 0},[p,g.data,y,b]),C=(0,u.useMemo)(()=>{let e=e=>h?`${e}?plate=${encodeURIComponent(h)}`:e;return p?e(r.getArchiveGcode(Number(p))):m?e(r.getLibraryFileGcodeUrl(Number(m))):null},[p,m,h]),w=()=>{window.history.length>1?e(-1):e(p?`/archives`:`/files`)},T=p?f(`gcodeViewer.backToArchives`,`Back to Archives`):f(`gcodeViewer.backToFiles`,`Back to File Manager`);return(0,d.jsxs)(`div`,{className:`flex flex-col min-h-[calc(100vh-64px)] lg:h-screen`,children:[(0,d.jsxs)(`div`,{className:`flex-shrink-0 px-4 py-2 border-b border-bambu-dark-tertiary flex flex-wrap items-center gap-x-4 gap-y-2`,children:[(0,d.jsxs)(`button`,{type:`button`,onClick:w,className:`inline-flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors`,children:[(0,d.jsx)(o,{className:`w-4 h-4`}),T]}),y.length>1&&(0,d.jsxs)(`div`,{className:`flex flex-wrap items-center gap-1.5`,children:[(0,d.jsx)(`span`,{className:`text-xs text-bambu-gray`,children:f(`gcodeViewer.plates`,`Plates`)}),y.map(e=>(0,d.jsx)(`button`,{type:`button`,onClick:()=>x(e.index),"aria-pressed":e.index===b,title:e.name??void 0,className:`px-2 py-0.5 rounded text-xs transition-colors ${e.index===b?`bg-bambu-green text-white`:`bg-bambu-dark-tertiary text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white`}`,children:f(`gcodeViewer.plateN`,`Plate {{n}}`,{n:e.index})},e.index))]})]}),C?(0,d.jsx)(s,{gcodeUrl:C,filamentColors:S,className:`flex-1 min-h-0`}):(0,d.jsx)(`div`,{className:`flex-1 flex items-center justify-center text-sm text-bambu-gray`,children:f(`gcodeViewer.noSource`,`No file was given to preview.`)})]})}export{f as GCodeViewerPage};
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,w as i}from"./client-p9vNUtaN.js";import{t as a}from"./useQuery-CDVFCu1z.js";import{t as o}from"./arrow-left-DG60mWnu.js";import{t as s}from"./GcodeToolpathViewer-B5RO-cba.js";import{Gn as c,Hn as l}from"./index-DAlcmDFx.js";var u=e(t(),1),d=i();function f(){let e=l(),[t,i]=c(),{t:f}=n(),p=t.get(`archive`),m=t.get(`library_file`),h=t.get(`plate`),g=a({queryKey:[`gcode-viewer-archive-colors`,p],queryFn:()=>r.getArchiveCapabilities(Number(p)),enabled:!!p,staleTime:5*6e4,retry:!1}),_=a({queryKey:[`gcode-viewer-library-colors`,m],queryFn:()=>r.getLibraryFilePlates(Number(m)),enabled:!!m,staleTime:5*6e4,retry:!1}),v=a({queryKey:[`gcode-viewer-archive-plates`,p],queryFn:()=>r.getArchivePlates(Number(p)),enabled:!!p,staleTime:5*6e4,retry:!1}),y=(0,u.useMemo)(()=>(p?v.data?.plates:_.data?.plates)??[],[p,v.data,_.data]),b=(0,u.useMemo)(()=>h?Number(h):y.length===0?null:Math.min(...y.map(e=>e.index)),[h,y]),x=e=>{if(e===b)return;let n=new URLSearchParams(t);n.set(`plate`,String(e)),i(n,{replace:!0})},S=(0,u.useMemo)(()=>{if(p)return g.data?.filament_colors;let e=y.find(e=>e.index===b)||y[0];if(!e?.filaments?.length)return;let t=[];for(let n of e.filaments){let e=Math.max(0,(n.slot_id??1)-1);n.color&&(t[e]=n.color)}return t.length>0?t:void 0},[p,g.data,y,b]),C=(0,u.useMemo)(()=>{let e=e=>h?`${e}?plate=${encodeURIComponent(h)}`:e;return p?e(r.getArchiveGcode(Number(p))):m?e(r.getLibraryFileGcodeUrl(Number(m))):null},[p,m,h]),w=()=>{window.history.length>1?e(-1):e(p?`/archives`:`/files`)},T=p?f(`gcodeViewer.backToArchives`,`Back to Archives`):f(`gcodeViewer.backToFiles`,`Back to File Manager`);return(0,d.jsxs)(`div`,{className:`flex flex-col min-h-[calc(100vh-64px)] lg:h-screen`,children:[(0,d.jsxs)(`div`,{className:`flex-shrink-0 px-4 py-2 border-b border-bambu-dark-tertiary flex flex-wrap items-center gap-x-4 gap-y-2`,children:[(0,d.jsxs)(`button`,{type:`button`,onClick:w,className:`inline-flex items-center gap-1.5 text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors`,children:[(0,d.jsx)(o,{className:`w-4 h-4`}),T]}),y.length>1&&(0,d.jsxs)(`div`,{className:`flex flex-wrap items-center gap-1.5`,children:[(0,d.jsx)(`span`,{className:`text-xs text-bambu-gray`,children:f(`gcodeViewer.plates`,`Plates`)}),y.map(e=>(0,d.jsx)(`button`,{type:`button`,onClick:()=>x(e.index),"aria-pressed":e.index===b,title:e.name??void 0,className:`px-2 py-0.5 rounded text-xs transition-colors ${e.index===b?`bg-bambu-green text-white`:`bg-bambu-dark-tertiary text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white`}`,children:f(`gcodeViewer.plateN`,`Plate {{n}}`,{n:e.index})},e.index))]})]}),C?(0,d.jsx)(s,{gcodeUrl:C,filamentColors:S,className:`flex-1 min-h-0`}):(0,d.jsx)(`div`,{className:`flex-1 flex items-center justify-center text-sm text-bambu-gray`,children:f(`gcodeViewer.noSource`,`No file was given to preview.`)})]})}export{f as GCodeViewerPage};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,l as i,w as a}from"./client-p9vNUtaN.js";import{t as o}from"./loader-circle-mjS43L3L.js";import{n as s,t as c}from"./zoom-out-BSUus6gf.js";import{t as l}from"./file-DDkFXH-b.js";import{Kt as u,dn as d}from"./index-BiT5tvve.js";import{n as f,r as p,t as m}from"./usePreviewFullscreen-D9MTc9Wr.js";var h=e(t(),1),g=a(),_=.4,v=8,y=1.25,b=30,x=Math.log(y)/b,S=e=>Math.min(v,Math.max(_,e)),C={x:0,y:0};function w(e){return e.deltaMode===1?e.deltaY*16:e.deltaMode===2?e.deltaY*100:e.deltaY}function T({libraryFileId:e,filename:t,fileSize:a,onClose:_}){let{t:v}=n(),T=m(),{isFullscreen:E,toggleFullscreen:D}=T,O=(0,h.useRef)(null),k=(0,h.useRef)(null),[A,j]=(0,h.useState)(null),[M,N]=(0,h.useState)(!1),[P,F]=(0,h.useState)(null),[I,L]=(0,h.useState)(1),[R,z]=(0,h.useState)(C),[B,V]=(0,h.useState)(!1),H=(0,h.useRef)(1),U=(0,h.useRef)(C),W=(0,h.useRef)(new Map),G=(0,h.useRef)(null),K=(0,h.useCallback)((e,t)=>{let n=O.current,r=k.current;if(!n||!r)return e;let i=Math.max(0,(r.offsetWidth*t-n.clientWidth)/2),a=Math.max(0,(r.offsetHeight*t-n.clientHeight)/2);return{x:Math.min(i,Math.max(-i,e.x)),y:Math.min(a,Math.max(-a,e.y))}},[]),q=(0,h.useCallback)((e,t)=>{H.current=e,U.current=t,L(e),z(t)},[]),J=(0,h.useCallback)((e,t,n)=>{let r=H.current,i=S(r*e);if(i===r)return;let a=O.current,o=U.current,s={x:o.x*(i/r),y:o.y*(i/r)};if(a){let e=a.getBoundingClientRect(),c=(t??e.left+e.width/2)-(e.left+e.width/2),l=(n??e.top+e.height/2)-(e.top+e.height/2),u=i/r;s={x:c-(c-o.x)*u,y:l-(l-o.y)*u}}q(i,K(s,i))},[K,q]),Y=(0,h.useCallback)(()=>q(1,C),[q]);(0,h.useEffect)(()=>{let t=!1,n=null;if(j(null),N(!1),F(null),q(1,C),a>52428800){F(v(`fileManager.preview.tooLarge`,{size:l(a)}));return}let o={},s=i();return s&&(o.Authorization=`Bearer ${s}`),(async()=>{let i=await fetch(r.getLibraryFileDownloadUrl(e),{headers:o});if(!i.ok)throw Error(`HTTP ${i.status}`);let a=await i.blob();t||(n=URL.createObjectURL(a),j(n))})().catch(e=>{console.error(`[image-preview] load failed`,e),t||F(v(`fileManager.preview.error`))}),()=>{t=!0,n&&URL.revokeObjectURL(n)}},[e,a,v,q]),(0,h.useEffect)(()=>{let e=e=>{e.ctrlKey||e.metaKey||e.altKey||(e.key===`+`||e.key===`=`?(e.preventDefault(),J(y)):e.key===`-`?(e.preventDefault(),J(1/y)):e.key===`0`&&(e.preventDefault(),Y()))};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[Y,J]),(0,h.useEffect)(()=>{let e=O.current;if(!e)return;let t=e=>{e.preventDefault();let t=Math.max(-30,Math.min(b,w(e)));J(Math.exp(-t*x),e.clientX,e.clientY)};return e.addEventListener(`wheel`,t,{passive:!1}),()=>e.removeEventListener(`wheel`,t)},[J,A]);let X=e=>{W.current.set(e.pointerId,{x:e.clientX,y:e.clientY}),G.current=null,W.current.size===1&&H.current>1&&(e.currentTarget.setPointerCapture(e.pointerId),V(!0))},Z=e=>{W.current.delete(e.pointerId),G.current=null,W.current.size===0&&V(!1)},Q=e=>{let t=W.current,n=t.get(e.pointerId);if(!n)return;if(e.pointerType===`mouse`&&e.buttons===0){Z(e);return}if(t.set(e.pointerId,{x:e.clientX,y:e.clientY}),t.size===1){if(H.current<=1)return;let t={x:U.current.x+(e.clientX-n.x),y:U.current.y+(e.clientY-n.y)};q(H.current,K(t,H.current));return}if(t.size!==2)return;let[r,i]=Array.from(t.values()),a=Math.hypot(r.x-i.x,r.y-i.y),o=G.current;G.current=a,o!=null&&o>0&&a>0&&J(a/o,(r.x+i.x)/2,(r.y+i.y)/2)},$=I>1;return(0,g.jsx)(f,{title:t,fullscreen:T,onClose:_,icon:(0,g.jsx)(d,{className:`w-5 h-5 text-bambu-green flex-shrink-0`}),actions:A?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`button`,{onClick:()=>J(1/y),className:p,"aria-label":v(`fileManager.preview.zoomOut`),children:(0,g.jsx)(c,{className:`w-4 h-4`})}),(0,g.jsx)(`button`,{onClick:()=>J(y),className:p,"aria-label":v(`fileManager.preview.zoomIn`),children:(0,g.jsx)(s,{className:`w-4 h-4`})}),(0,g.jsx)(`button`,{onClick:Y,className:p,"aria-label":v(`fileManager.preview.resetZoom`),children:(0,g.jsx)(u,{className:`w-4 h-4`})})]}):void 0,children:(0,g.jsx)(`div`,{ref:O,"data-testid":`image-preview-content`,className:`relative flex-1 min-h-0 overflow-hidden bg-bambu-dark flex items-center justify-center touch-none ${E?``:`rounded-b-lg`} ${$?B?`cursor-grabbing`:`cursor-grab`:``}`,onDoubleClick:D,onPointerDown:X,onPointerMove:Q,onPointerUp:Z,onPointerCancel:Z,onPointerLeave:Z,children:P?(0,g.jsx)(`p`,{className:`text-bambu-gray text-center p-6`,children:P}):(0,g.jsxs)(g.Fragment,{children:[A&&(0,g.jsx)(`img`,{ref:k,src:A,alt:t,draggable:!1,onLoad:()=>N(!0),onError:()=>F(v(`fileManager.preview.error`)),style:{transform:`translate(${R.x}px, ${R.y}px) scale(${I})`},className:`max-w-full max-h-full object-contain select-none`}),!M&&(0,g.jsx)(`div`,{className:`absolute inset-0 flex items-center justify-center pointer-events-none`,children:(0,g.jsx)(o,{className:`w-8 h-8 text-bambu-green animate-spin`})})]})})})}export{T as ImagePreviewModal};
import{i as e}from"./chunk-aKtaBQYM.js";import{O as t,T as n,a as r,l as i,w as a}from"./client-p9vNUtaN.js";import{t as o}from"./loader-circle-mjS43L3L.js";import{n as s,t as c}from"./zoom-out-BSUus6gf.js";import{t as l}from"./file-DDkFXH-b.js";import{Kt as u,dn as d}from"./index-DAlcmDFx.js";import{n as f,r as p,t as m}from"./usePreviewFullscreen-D9MTc9Wr.js";var h=e(t(),1),g=a(),_=.4,v=8,y=1.25,b=30,x=Math.log(y)/b,S=e=>Math.min(v,Math.max(_,e)),C={x:0,y:0};function w(e){return e.deltaMode===1?e.deltaY*16:e.deltaMode===2?e.deltaY*100:e.deltaY}function T({libraryFileId:e,filename:t,fileSize:a,onClose:_}){let{t:v}=n(),T=m(),{isFullscreen:E,toggleFullscreen:D}=T,O=(0,h.useRef)(null),k=(0,h.useRef)(null),[A,j]=(0,h.useState)(null),[M,N]=(0,h.useState)(!1),[P,F]=(0,h.useState)(null),[I,L]=(0,h.useState)(1),[R,z]=(0,h.useState)(C),[B,V]=(0,h.useState)(!1),H=(0,h.useRef)(1),U=(0,h.useRef)(C),W=(0,h.useRef)(new Map),G=(0,h.useRef)(null),K=(0,h.useCallback)((e,t)=>{let n=O.current,r=k.current;if(!n||!r)return e;let i=Math.max(0,(r.offsetWidth*t-n.clientWidth)/2),a=Math.max(0,(r.offsetHeight*t-n.clientHeight)/2);return{x:Math.min(i,Math.max(-i,e.x)),y:Math.min(a,Math.max(-a,e.y))}},[]),q=(0,h.useCallback)((e,t)=>{H.current=e,U.current=t,L(e),z(t)},[]),J=(0,h.useCallback)((e,t,n)=>{let r=H.current,i=S(r*e);if(i===r)return;let a=O.current,o=U.current,s={x:o.x*(i/r),y:o.y*(i/r)};if(a){let e=a.getBoundingClientRect(),c=(t??e.left+e.width/2)-(e.left+e.width/2),l=(n??e.top+e.height/2)-(e.top+e.height/2),u=i/r;s={x:c-(c-o.x)*u,y:l-(l-o.y)*u}}q(i,K(s,i))},[K,q]),Y=(0,h.useCallback)(()=>q(1,C),[q]);(0,h.useEffect)(()=>{let t=!1,n=null;if(j(null),N(!1),F(null),q(1,C),a>52428800){F(v(`fileManager.preview.tooLarge`,{size:l(a)}));return}let o={},s=i();return s&&(o.Authorization=`Bearer ${s}`),(async()=>{let i=await fetch(r.getLibraryFileDownloadUrl(e),{headers:o});if(!i.ok)throw Error(`HTTP ${i.status}`);let a=await i.blob();t||(n=URL.createObjectURL(a),j(n))})().catch(e=>{console.error(`[image-preview] load failed`,e),t||F(v(`fileManager.preview.error`))}),()=>{t=!0,n&&URL.revokeObjectURL(n)}},[e,a,v,q]),(0,h.useEffect)(()=>{let e=e=>{e.ctrlKey||e.metaKey||e.altKey||(e.key===`+`||e.key===`=`?(e.preventDefault(),J(y)):e.key===`-`?(e.preventDefault(),J(1/y)):e.key===`0`&&(e.preventDefault(),Y()))};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[Y,J]),(0,h.useEffect)(()=>{let e=O.current;if(!e)return;let t=e=>{e.preventDefault();let t=Math.max(-30,Math.min(b,w(e)));J(Math.exp(-t*x),e.clientX,e.clientY)};return e.addEventListener(`wheel`,t,{passive:!1}),()=>e.removeEventListener(`wheel`,t)},[J,A]);let X=e=>{W.current.set(e.pointerId,{x:e.clientX,y:e.clientY}),G.current=null,W.current.size===1&&H.current>1&&(e.currentTarget.setPointerCapture(e.pointerId),V(!0))},Z=e=>{W.current.delete(e.pointerId),G.current=null,W.current.size===0&&V(!1)},Q=e=>{let t=W.current,n=t.get(e.pointerId);if(!n)return;if(e.pointerType===`mouse`&&e.buttons===0){Z(e);return}if(t.set(e.pointerId,{x:e.clientX,y:e.clientY}),t.size===1){if(H.current<=1)return;let t={x:U.current.x+(e.clientX-n.x),y:U.current.y+(e.clientY-n.y)};q(H.current,K(t,H.current));return}if(t.size!==2)return;let[r,i]=Array.from(t.values()),a=Math.hypot(r.x-i.x,r.y-i.y),o=G.current;G.current=a,o!=null&&o>0&&a>0&&J(a/o,(r.x+i.x)/2,(r.y+i.y)/2)},$=I>1;return(0,g.jsx)(f,{title:t,fullscreen:T,onClose:_,icon:(0,g.jsx)(d,{className:`w-5 h-5 text-bambu-green flex-shrink-0`}),actions:A?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`button`,{onClick:()=>J(1/y),className:p,"aria-label":v(`fileManager.preview.zoomOut`),children:(0,g.jsx)(c,{className:`w-4 h-4`})}),(0,g.jsx)(`button`,{onClick:()=>J(y),className:p,"aria-label":v(`fileManager.preview.zoomIn`),children:(0,g.jsx)(s,{className:`w-4 h-4`})}),(0,g.jsx)(`button`,{onClick:Y,className:p,"aria-label":v(`fileManager.preview.resetZoom`),children:(0,g.jsx)(u,{className:`w-4 h-4`})})]}):void 0,children:(0,g.jsx)(`div`,{ref:O,"data-testid":`image-preview-content`,className:`relative flex-1 min-h-0 overflow-hidden bg-bambu-dark flex items-center justify-center touch-none ${E?``:`rounded-b-lg`} ${$?B?`cursor-grabbing`:`cursor-grab`:``}`,onDoubleClick:D,onPointerDown:X,onPointerMove:Q,onPointerUp:Z,onPointerCancel:Z,onPointerLeave:Z,children:P?(0,g.jsx)(`p`,{className:`text-bambu-gray text-center p-6`,children:P}):(0,g.jsxs)(g.Fragment,{children:[A&&(0,g.jsx)(`img`,{ref:k,src:A,alt:t,draggable:!1,onLoad:()=>N(!0),onError:()=>F(v(`fileManager.preview.error`)),style:{transform:`translate(${R.x}px, ${R.y}px) scale(${I})`},className:`max-w-full max-h-full object-contain select-none`}),!M&&(0,g.jsx)(`div`,{className:`absolute inset-0 flex items-center justify-center pointer-events-none`,children:(0,g.jsx)(o,{className:`w-8 h-8 text-bambu-green animate-spin`})})]})})})}export{T as ImagePreviewModal};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{mt as e}from"./index-BiT5tvve.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};
import{mt as e}from"./index-DAlcmDFx.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function e(e){let t=(e??``).trim().toUpperCase().slice(0,2);return t===`HH`||t===`HS`?t:null}function t(e){return e?e===`HH`?`HF`:`S`:null}function n(t,n){let r=e(t),i=e(n);return!r||!i?!0:r===i}export{t as n,e as r,n as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BiT5tvve.js"></script>
<script type="module" crossorigin src="/assets/index-DAlcmDFx.js"></script>
<link rel="modulepreload" crossorigin href="/assets/chunk-aKtaBQYM.js">
<link rel="modulepreload" crossorigin href="/assets/client-p9vNUtaN.js">
<link rel="modulepreload" crossorigin href="/assets/QueryClientProvider-B5zKGR0V.js">