mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Security hardening (maziggy/bambuddy-security #20)
This commit is contained in:
@@ -40,9 +40,11 @@ from dataclasses import dataclass, field as dataclasses_field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.api.routes._spoolman_helpers import assert_safe_spoolman_url
|
||||
from backend.app.core.database import async_session
|
||||
from backend.app.core.printer_scope import drop_printers_from_obico
|
||||
from backend.app.models.archive import PrintArchive
|
||||
@@ -53,9 +55,11 @@ from backend.app.models.settings import Settings
|
||||
from backend.app.models.spool import Spool
|
||||
from backend.app.models.spool_usage_history import SpoolUsageHistory
|
||||
from backend.app.models.user import User
|
||||
from backend.app.schemas.auth import SMTPSettings
|
||||
from backend.app.schemas.github_backup import RestoreCategory
|
||||
from backend.app.schemas.settings import AppSettings
|
||||
from backend.app.schemas.settings import AppSettings, AppSettingsUpdate
|
||||
from backend.app.services.git_providers.factory import get_provider_backend
|
||||
from backend.app.services.model_providers.manyfold.config import normalize_url
|
||||
from backend.app.services.printer_manager import printer_manager
|
||||
from backend.app.services.secret_fields import SETTINGS_SECRETS, SMTP_DESTINATIONS, is_secret_setting_key
|
||||
|
||||
@@ -198,6 +202,102 @@ def _is_protected_setting_key(key: str) -> bool:
|
||||
return key in _PROTECTED_SETTING_KEYS or key.lower().startswith(_PROTECTED_SETTING_PREFIXES)
|
||||
|
||||
|
||||
# A user's email code sign-in switch. Per user, and it decides how that user
|
||||
# signs in: never taken from a backup, whatever its value.
|
||||
_TWO_FACTOR_KEY = re.compile(r"^user_\d+_email_2fa_enabled$")
|
||||
|
||||
# Settings kept outside AppSettings that a restore writes, each checked as the
|
||||
# editor that owns it checks it: (model, field) for a field of that editor's
|
||||
# request model. Anything else outside AppSettings is runtime state, a login's
|
||||
# account state, automation that needs its own permission (the purge
|
||||
# schedules), a migration flag or unknown, and is not restored.
|
||||
_EXTRA_RESTORABLE_KEYS: dict[str, tuple[type[BaseModel], str]] = {
|
||||
key: (SMTPSettings, key)
|
||||
for key in (
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_security",
|
||||
"smtp_auth_enabled",
|
||||
"smtp_from_email",
|
||||
"smtp_from_name",
|
||||
"smtp_username",
|
||||
"smtp_use_tls",
|
||||
)
|
||||
}
|
||||
|
||||
# Settings that decide where Bambuddy sends something: the links in its emails
|
||||
# (password reset and welcome links carry a sign-in token), the mail server
|
||||
# itself, the full database backups, printer status, camera snapshots, model
|
||||
# files, inventory. A backup naming another address would send it there, so a
|
||||
# value that differs from this instance's is kept as it is here.
|
||||
_ADDRESS_KEYS = (
|
||||
"external_url",
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_security",
|
||||
"smtp_use_tls",
|
||||
"local_backup_path",
|
||||
"mqtt_broker",
|
||||
"mqtt_port",
|
||||
"mqtt_use_tls",
|
||||
"ha_url",
|
||||
"obico_ml_url",
|
||||
"bambuddy_internal_url",
|
||||
"orcaslicer_api_url",
|
||||
"bambu_studio_api_url",
|
||||
"spoolman_url",
|
||||
"manyfold_url",
|
||||
)
|
||||
|
||||
# Restored without a model check: the Manyfold editor checks the URL with
|
||||
# normalize_url and takes the client id as text.
|
||||
_MANYFOLD_URL_KEY = "manyfold_url"
|
||||
_MANYFOLD_CLIENT_ID_KEY = "manyfold_client_id"
|
||||
|
||||
# The settings whose saved null (the text "None") the settings reader reads as
|
||||
# null. Anywhere else it is refused, as the editor refuses a null.
|
||||
_READS_A_SAVED_NULL = frozenset({"ams_temp_alarm", "default_printer_id", "open_in_slicer"})
|
||||
|
||||
|
||||
def _stored_form(value: object) -> str:
|
||||
"""A checked value as its editor stores it."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if value is None:
|
||||
return "None"
|
||||
return str(value)
|
||||
|
||||
|
||||
def _checked_setting(key: str, value: object) -> tuple[bool | None, str]:
|
||||
"""A restored value checked as its editor checks it.
|
||||
|
||||
``(True, stored form)`` if the editor would accept it, ``(False, "")`` if
|
||||
not, ``(None, "")`` if no editor writes the key, so it is not restored.
|
||||
The stored form is what the editor would write: a value the check only
|
||||
coerced ("587.0", "yes") is written as the editor writes it.
|
||||
"""
|
||||
candidate = None if value == "None" and key in _READS_A_SAVED_NULL else value
|
||||
try:
|
||||
if key in AppSettingsUpdate.model_fields:
|
||||
checked = getattr(AppSettingsUpdate.model_validate({key: candidate}), key)
|
||||
# Checked where it is used rather than where it is saved: checked
|
||||
# here as well, before it is written.
|
||||
if key == "spoolman_url" and checked:
|
||||
assert_safe_spoolman_url(str(checked))
|
||||
return True, _stored_form(checked)
|
||||
if key in _EXTRA_RESTORABLE_KEYS:
|
||||
model, field = _EXTRA_RESTORABLE_KEYS[key]
|
||||
instance = model.__pydantic_validator__.validate_assignment(model.model_construct(), field, candidate)
|
||||
return True, _stored_form(getattr(instance, field))
|
||||
if key == _MANYFOLD_URL_KEY:
|
||||
return True, normalize_url(str(candidate)) if candidate else ""
|
||||
except ValueError:
|
||||
return False, ""
|
||||
if key == _MANYFOLD_CLIENT_ID_KEY:
|
||||
return True, str(value)
|
||||
return None, ""
|
||||
|
||||
|
||||
# There used to be an ``_is_skipped_setting_key`` here, the union of the two
|
||||
# predicates above, shared by the preview and the restore so neither could drift
|
||||
# from the other. It is gone because a name is no longer enough to decide: the
|
||||
@@ -325,19 +425,37 @@ class _SettingsPlan:
|
||||
protected: tuple[str, ...] = ()
|
||||
companion: tuple[str, ...] = ()
|
||||
destination: tuple[str, ...] = ()
|
||||
two_factor: tuple[str, ...] = ()
|
||||
invalid: tuple[str, ...] = ()
|
||||
unknown: tuple[str, ...] = ()
|
||||
# Bambuddy's own one-shot flags (a leading underscore): refused without a
|
||||
# word, as every backup carries them.
|
||||
internal: tuple[str, ...] = ()
|
||||
address: tuple[str, ...] = ()
|
||||
# Each key to write, in the form its editor stores.
|
||||
stored: dict[str, str] = dataclasses_field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def refused(self) -> frozenset[str]:
|
||||
def _buckets(self) -> tuple[tuple[str, ...], ...]:
|
||||
return (
|
||||
frozenset(self.blocked)
|
||||
| frozenset(self.protected)
|
||||
| frozenset(self.companion)
|
||||
| frozenset(self.destination)
|
||||
self.blocked,
|
||||
self.protected,
|
||||
self.companion,
|
||||
self.destination,
|
||||
self.two_factor,
|
||||
self.invalid,
|
||||
self.unknown,
|
||||
self.internal,
|
||||
self.address,
|
||||
)
|
||||
|
||||
@property
|
||||
def refused(self) -> frozenset[str]:
|
||||
return frozenset(key for bucket in self._buckets for key in bucket)
|
||||
|
||||
@property
|
||||
def refused_count(self) -> int:
|
||||
return len(self.blocked) + len(self.protected) + len(self.companion) + len(self.destination)
|
||||
return sum(len(bucket) for bucket in self._buckets)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -507,6 +625,11 @@ class GitHubRestoreService:
|
||||
"""
|
||||
blocked: list[str] = []
|
||||
protected: list[str] = []
|
||||
two_factor: list[str] = []
|
||||
invalid: list[str] = []
|
||||
unknown: list[str] = []
|
||||
internal: list[str] = []
|
||||
stored: dict[str, str] = {}
|
||||
# Toggle -> credential for the pairs that survived the payload-only
|
||||
# conditions and still need local state to judge.
|
||||
candidates: dict[str, str] = {}
|
||||
@@ -520,12 +643,29 @@ class GitHubRestoreService:
|
||||
if _is_protected_setting_key(key):
|
||||
protected.append(key)
|
||||
continue
|
||||
if _TWO_FACTOR_KEY.match(key):
|
||||
two_factor.append(key)
|
||||
continue
|
||||
if key.startswith("_"):
|
||||
internal.append(key)
|
||||
continue
|
||||
# A null is skipped by the restore itself, not refused.
|
||||
if value is not None:
|
||||
valid, form = _checked_setting(key, value)
|
||||
if valid is None:
|
||||
unknown.append(key)
|
||||
continue
|
||||
if not valid:
|
||||
invalid.append(key)
|
||||
continue
|
||||
stored[key] = form
|
||||
|
||||
credential = _COMPANION_CREDENTIALS.get(key)
|
||||
if credential is None:
|
||||
continue
|
||||
# Turning something *off* is always safe to write.
|
||||
if not _setting_value_is_true(value):
|
||||
# Judged as written: a spelling the check reads as on is stored as on.
|
||||
if not _setting_value_is_true(stored.get(key, value)):
|
||||
continue
|
||||
# Expressed as the predicate rather than assumed, so the map cannot
|
||||
# go quietly inert if the secret hints are ever edited: a credential
|
||||
@@ -548,15 +688,31 @@ class GitHubRestoreService:
|
||||
destinations = {
|
||||
key: secrets
|
||||
for key, value in values.items()
|
||||
if value is not None and (secrets := _DESTINATION_SECRETS.get(key)) and key not in protected
|
||||
if value is not None
|
||||
and (secrets := _DESTINATION_SECRETS.get(key))
|
||||
and key not in protected
|
||||
and key not in invalid
|
||||
and key not in unknown
|
||||
and key not in internal
|
||||
}
|
||||
|
||||
if not candidates and not destinations:
|
||||
return _SettingsPlan(blocked=tuple(blocked), protected=tuple(protected))
|
||||
addresses = [key for key in _ADDRESS_KEYS if key in stored]
|
||||
|
||||
if not candidates and not destinations and not addresses:
|
||||
return _SettingsPlan(
|
||||
blocked=tuple(blocked),
|
||||
protected=tuple(protected),
|
||||
two_factor=tuple(two_factor),
|
||||
invalid=tuple(invalid),
|
||||
unknown=tuple(unknown),
|
||||
internal=tuple(internal),
|
||||
stored=stored,
|
||||
)
|
||||
|
||||
# One SELECT covering both halves of every candidate pair.
|
||||
wanted = set(candidates) | set(candidates.values()) | set(destinations)
|
||||
wanted |= {secret for secrets in destinations.values() for secret in secrets}
|
||||
wanted |= set(addresses)
|
||||
rows = await db.execute(select(Settings).where(Settings.key.in_(wanted)))
|
||||
local = {row.key: row.value for row in rows.scalars().all()}
|
||||
|
||||
@@ -600,14 +756,38 @@ class GitHubRestoreService:
|
||||
# counted as a change.
|
||||
destination.append(key)
|
||||
continue
|
||||
if _as_setting_text(values[key]) != _as_setting_text(current):
|
||||
if _as_setting_text(stored.get(key, values[key])) != _as_setting_text(current):
|
||||
destination.append(key)
|
||||
|
||||
# Compared as written with this instance's value: its row, else the
|
||||
# schema default, else nothing.
|
||||
address = [
|
||||
key
|
||||
for key in addresses
|
||||
if key not in destination
|
||||
and _as_setting_text(stored[key])
|
||||
!= _as_setting_text(
|
||||
local[key]
|
||||
if key in local
|
||||
else AppSettings.model_fields[key].get_default(call_default_factory=True)
|
||||
if key in AppSettings.model_fields
|
||||
else None
|
||||
)
|
||||
]
|
||||
for key in address:
|
||||
del stored[key]
|
||||
|
||||
return _SettingsPlan(
|
||||
blocked=tuple(blocked),
|
||||
protected=tuple(protected),
|
||||
companion=tuple(companion),
|
||||
address=tuple(address),
|
||||
destination=tuple(destination),
|
||||
two_factor=tuple(two_factor),
|
||||
invalid=tuple(invalid),
|
||||
unknown=tuple(unknown),
|
||||
internal=tuple(internal),
|
||||
stored=stored,
|
||||
)
|
||||
|
||||
async def preview(self, db: AsyncSession, config: GitHubBackupConfig, ref: str = "HEAD") -> dict:
|
||||
@@ -719,6 +899,7 @@ class GitHubRestoreService:
|
||||
# policy keys stay unmentioned on purpose.
|
||||
plan = await self._plan_settings(db, values)
|
||||
detail = None
|
||||
not_restorable = len(plan.two_factor) + len(plan.invalid) + len(plan.unknown) + len(plan.address)
|
||||
if plan.destination:
|
||||
detail = _Detail(
|
||||
"settingsDestinationWillKeep",
|
||||
@@ -726,6 +907,13 @@ class GitHubRestoreService:
|
||||
"stored here somewhere else; credentials are never restored",
|
||||
{"destination": len(plan.destination)},
|
||||
)
|
||||
elif not_restorable:
|
||||
detail = _Detail(
|
||||
"settingsNotRestorableWillSkip",
|
||||
f"{not_restorable} setting(s) will be skipped - values Settings would not accept, or keys a "
|
||||
"backup does not restore",
|
||||
{"count": not_restorable},
|
||||
)
|
||||
elif plan.companion and not plan.blocked:
|
||||
# An exposure toggle becomes a candidate whether or not the
|
||||
# backup carried its credential, so this commit can refuse a
|
||||
@@ -1667,13 +1855,13 @@ class GitHubRestoreService:
|
||||
if not overwrite:
|
||||
tally.skipped += 1
|
||||
continue
|
||||
existing.value = str(value)
|
||||
existing.value = plan.stored[key]
|
||||
tally.restored += 1
|
||||
if keys_written is not None:
|
||||
keys_written.add(key)
|
||||
continue
|
||||
|
||||
db.add(Settings(key=key, value=str(value)))
|
||||
db.add(Settings(key=key, value=plan.stored[key]))
|
||||
tally.restored += 1
|
||||
if keys_written is not None:
|
||||
keys_written.add(key)
|
||||
@@ -1705,6 +1893,38 @@ class GitHubRestoreService:
|
||||
keys=keys,
|
||||
count=len(plan.companion),
|
||||
)
|
||||
if plan.two_factor:
|
||||
tally.note(
|
||||
"settingsTwoFactorSkipped",
|
||||
f"{len(plan.two_factor)} email sign-in code setting(s) skipped - each user switches email codes "
|
||||
"on or off in their own account",
|
||||
count=len(plan.two_factor),
|
||||
)
|
||||
if plan.address:
|
||||
keys = ", ".join(sorted(plan.address))
|
||||
tally.note(
|
||||
"settingsAddressKept",
|
||||
f"{keys} left as they are - they decide where Bambuddy sends data, emails or backups; change them "
|
||||
"in Settings",
|
||||
keys=keys,
|
||||
count=len(plan.address),
|
||||
)
|
||||
if plan.invalid:
|
||||
keys = ", ".join(sorted(plan.invalid))
|
||||
tally.note(
|
||||
"settingsInvalidSkipped",
|
||||
f"{keys} skipped - the value in the backup is not one Settings would accept",
|
||||
keys=keys,
|
||||
count=len(plan.invalid),
|
||||
)
|
||||
if plan.unknown:
|
||||
keys = ", ".join(sorted(plan.unknown))
|
||||
tally.note(
|
||||
"settingsUnknownSkipped",
|
||||
f"{keys} skipped - not settings a backup restores (runtime state, account sessions or unknown keys)",
|
||||
keys=keys,
|
||||
count=len(plan.unknown),
|
||||
)
|
||||
if plan.destination:
|
||||
keys = ", ".join(sorted(plan.destination))
|
||||
tally.note(
|
||||
|
||||
@@ -235,13 +235,13 @@ class TestRestoreSettings:
|
||||
@pytest.mark.asyncio
|
||||
async def test_inserts_missing_keys(self, db_session):
|
||||
tally = _CategoryTally()
|
||||
payload = {"version": "1.0", "settings": {"currency": "EUR", "theme": "dark"}}
|
||||
payload = {"version": "1.0", "settings": {"currency": "EUR", "language": "de"}}
|
||||
|
||||
await _service()._restore_settings(db_session, payload, overwrite=False, tally=tally)
|
||||
await db_session.commit()
|
||||
|
||||
rows = {s.key: s.value for s in (await db_session.execute(select(Settings))).scalars().all()}
|
||||
assert rows == {"currency": "EUR", "theme": "dark"}
|
||||
assert rows == {"currency": "EUR", "language": "de"}
|
||||
assert tally.restored == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -428,11 +428,11 @@ class TestCompanionCredentials:
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_toggle_is_refused_when_the_environment_has_no_token(self, db_session, monkeypatch):
|
||||
monkeypatch.delenv("HA_TOKEN", raising=False)
|
||||
await self._restore(db_session, ha_enabled="true", ha_token="s3cret", ha_url="http://ha.local")
|
||||
await self._restore(db_session, ha_enabled="true", ha_token="s3cret", currency="EUR")
|
||||
|
||||
rows = await self._rows(db_session)
|
||||
assert "ha_enabled" not in rows
|
||||
assert rows["ha_url"] == "http://ha.local"
|
||||
assert rows["currency"] == "EUR"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_blank_local_credential_row_is_not_usable(self, db_session):
|
||||
@@ -464,12 +464,12 @@ class TestCompanionCredentials:
|
||||
companion one were all counted as ``skipped`` despite never being in the
|
||||
preview's count.
|
||||
"""
|
||||
db_session.add(Settings(key="theme", value="light"))
|
||||
db_session.add(Settings(key="language", value="en"))
|
||||
await db_session.commit()
|
||||
|
||||
values = {
|
||||
"currency": "EUR", # inserted -> restored
|
||||
"theme": "dark", # exists, overwrite off -> skipped
|
||||
"language": "de", # exists, overwrite off -> skipped
|
||||
"low_stock_threshold": None, # no value -> skipped
|
||||
"": "junk", # unusable key -> failed
|
||||
"bambu_cloud_token": "x", # blocked -> refused
|
||||
@@ -666,9 +666,11 @@ class TestCompanionCredentials:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("value", ["1", "on", "yes"])
|
||||
async def test_spellings_no_reader_treats_as_on_are_written(self, db_session, value):
|
||||
async def test_spellings_the_editor_reads_as_on_are_judged_as_on(self, db_session, value):
|
||||
"""The restore writes them as the editor stores them, "true", so the
|
||||
rule judges them as on: no usable token here, the switch stays off."""
|
||||
await self._restore(db_session, prometheus_enabled=value, prometheus_token="s3cret")
|
||||
assert (await self._rows(db_session))["prometheus_enabled"] == value
|
||||
assert "prometheus_enabled" not in await self._rows(db_session)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_token_in_the_environment_counts_as_usable(self, db_session, monkeypatch):
|
||||
@@ -708,7 +710,8 @@ class TestCompanionCredentials:
|
||||
async def test_plan_leaves_unusable_key_names_in_no_bucket(self, db_session):
|
||||
"""They are the restore's ``failed``, not a refusal."""
|
||||
plan = await _service()._plan_settings(db_session, {"": "x", 7: "y", "currency": "EUR"})
|
||||
assert plan == _SettingsPlan()
|
||||
assert plan.refused == frozenset()
|
||||
assert plan.stored == {"currency": "EUR"}
|
||||
|
||||
|
||||
class TestSpoolTagOverwrite:
|
||||
@@ -3080,12 +3083,12 @@ class TestMqttRelayReconfigure:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_settings_reports_the_keys_it_wrote(self, db_session):
|
||||
db_session.add(Settings(key="mqtt_broker", value="old.local"))
|
||||
db_session.add(Settings(key="mqtt_topic_prefix", value="old.local"))
|
||||
await db_session.commit()
|
||||
written: set[str] = set()
|
||||
payload = {
|
||||
"settings": {
|
||||
"mqtt_broker": "new.local",
|
||||
"mqtt_topic_prefix": "new.local",
|
||||
"currency": "EUR",
|
||||
"mqtt_password": "leaked",
|
||||
"auth_enabled": "false",
|
||||
@@ -3098,17 +3101,17 @@ class TestMqttRelayReconfigure:
|
||||
|
||||
# Skipped keys are not "written", or a blocked mqtt_password would
|
||||
# trigger a pointless reconnect.
|
||||
assert written == {"mqtt_broker", "currency"}
|
||||
assert written == {"mqtt_topic_prefix", "currency"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keys_skipped_for_overwrite_off_are_not_reported(self, db_session):
|
||||
db_session.add(Settings(key="mqtt_broker", value="old.local"))
|
||||
db_session.add(Settings(key="mqtt_topic_prefix", value="old.local"))
|
||||
await db_session.commit()
|
||||
written: set[str] = set()
|
||||
|
||||
await _service()._restore_settings(
|
||||
db_session,
|
||||
{"settings": {"mqtt_broker": "new.local"}},
|
||||
{"settings": {"mqtt_topic_prefix": "new.local"}},
|
||||
overwrite=False,
|
||||
tally=_CategoryTally(),
|
||||
keys_written=written,
|
||||
@@ -3129,13 +3132,13 @@ class TestMqttRelayReconfigure:
|
||||
|
||||
await _service()._restore_settings(
|
||||
db_session,
|
||||
{"settings": {"mqtt_enabled": "true", "mqtt_password": "refused", "mqtt_broker": "new.local"}},
|
||||
{"settings": {"mqtt_enabled": "true", "mqtt_password": "refused", "mqtt_topic_prefix": "new.local"}},
|
||||
overwrite=True,
|
||||
tally=_CategoryTally(),
|
||||
keys_written=written,
|
||||
)
|
||||
|
||||
assert written == {"mqtt_broker"}
|
||||
assert written == {"mqtt_topic_prefix"}
|
||||
|
||||
|
||||
class TestApplyOrdering:
|
||||
@@ -3236,7 +3239,7 @@ class TestKprofilePhaseFailure:
|
||||
leaving the relay pointed at the pre-restore broker.
|
||||
"""
|
||||
|
||||
_SETTINGS = {"version": "1.0", "settings": {"mqtt_broker": "restored.local", "currency": "EUR"}}
|
||||
_SETTINGS = {"version": "1.0", "settings": {"mqtt_topic_prefix": "restored.local", "currency": "EUR"}}
|
||||
|
||||
def _payload(self, profiles=None):
|
||||
return {
|
||||
@@ -3278,7 +3281,9 @@ class TestKprofilePhaseFailure:
|
||||
|
||||
assert results[RestoreCategory.SETTINGS.value].restored == 2
|
||||
rows = {s.key: s.value for s in (await db_session.execute(select(Settings))).scalars().all()}
|
||||
assert rows == {"mqtt_broker": "restored.local", "currency": "EUR"}, "committed before the phase that failed"
|
||||
assert rows == {"mqtt_topic_prefix": "restored.local", "currency": "EUR"}, (
|
||||
"committed before the phase that failed"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_failure_is_counted_and_explained(self, db_session):
|
||||
@@ -3315,7 +3320,7 @@ class TestKprofilePhaseFailure:
|
||||
assert result["results"][RestoreCategory.SETTINGS.value]["restored"] == 2
|
||||
assert result["results"][RestoreCategory.KPROFILES.value]["failed"] == 1
|
||||
relay.configure.assert_awaited_once()
|
||||
assert relay.configure.await_args.args[0]["mqtt_broker"] == "restored.local"
|
||||
assert relay.configure.await_args.args[0]["mqtt_topic_prefix"] == "restored.local"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_failure_before_the_commit_still_reports_nothing_restored(self, db_session):
|
||||
|
||||
@@ -49,13 +49,16 @@ async def test_a_changed_broker_is_left_as_it_is_while_the_password_is_kept(db_s
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_without_a_stored_credential_the_destination_is_restored(db_session):
|
||||
async def test_without_a_stored_credential_the_destination_is_still_kept(db_session):
|
||||
"""No credential to protect, but the broker still decides where printer
|
||||
status goes: kept as an address."""
|
||||
await _local(db_session, {"mqtt_broker": "here.local", "mqtt_password": " "})
|
||||
|
||||
_, written = await _restore(db_session, {"mqtt_broker": "elsewhere.example"})
|
||||
tally, written = await _restore(db_session, {"mqtt_broker": "elsewhere.example"})
|
||||
|
||||
assert await _value(db_session, "mqtt_broker") == "elsewhere.example"
|
||||
assert written == {"mqtt_broker"}
|
||||
assert await _value(db_session, "mqtt_broker") == "here.local"
|
||||
assert written == set()
|
||||
assert "settingsAddressKept" in [note["code"] for note in tally.notes]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -100,14 +103,16 @@ async def test_a_token_does_not_follow_a_changed_url(db_session, monkeypatch, se
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_deployment_token_may_follow_a_changed_url(db_session, monkeypatch):
|
||||
"""Set in the environment, the token is the deployment's own: as in the editor."""
|
||||
async def test_with_a_deployment_token_a_changed_url_is_still_kept_as_an_address(db_session, monkeypatch):
|
||||
"""The environment's token may follow a URL change in the editor, but a
|
||||
restore never changes where Bambuddy sends things."""
|
||||
monkeypatch.setenv("HA_TOKEN", "env-value")
|
||||
await _local(db_session, {"ha_token": "local-value", "ha_url": "http://here.local"})
|
||||
|
||||
await _restore(db_session, {"ha_url": "http://elsewhere.example"})
|
||||
tally, _written = await _restore(db_session, {"ha_url": "http://elsewhere.example"})
|
||||
|
||||
assert await _value(db_session, "ha_url") == "http://elsewhere.example"
|
||||
assert await _value(db_session, "ha_url") == "http://here.local"
|
||||
assert "settingsAddressKept" in [note["code"] for note in tally.notes]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -163,10 +168,24 @@ async def test_the_manyfold_secret_does_not_follow_a_changed_url(db_session):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_without_a_stored_smtp_password_the_server_is_restored(db_session):
|
||||
async def test_without_a_stored_smtp_password_the_server_is_still_kept(db_session):
|
||||
"""An unauthenticated relay: no password to protect, but the server still
|
||||
receives every email, password reset tokens among them."""
|
||||
await _local(db_session, {"smtp_host": "mail.here.local"})
|
||||
|
||||
_, written = await _restore(db_session, {"smtp_host": "mail.elsewhere.example", "smtp_security": "ssl"})
|
||||
|
||||
assert await _value(db_session, "smtp_host") == "mail.elsewhere.example"
|
||||
assert written == {"smtp_host", "smtp_security"}
|
||||
assert await _value(db_session, "smtp_host") == "mail.here.local"
|
||||
assert await _value(db_session, "smtp_security") is None
|
||||
assert written == set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_destination_is_compared_as_it_would_be_written(db_session):
|
||||
""" " 587" is stored as "587": the same port, so not held back."""
|
||||
await _local(db_session, {"smtp_password": "local-value", "smtp_port": "587"})
|
||||
|
||||
tally, written = await _restore(db_session, {"smtp_port": " 587"})
|
||||
|
||||
assert written == {"smtp_port"}
|
||||
assert "settingsDestinationKept" not in [note["code"] for note in tally.notes]
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
"""A settings restore writes only what the settings editors would accept.
|
||||
|
||||
Each value is checked as the editor that owns it checks it. A key no editor
|
||||
writes (runtime state, a login's account state, a migration flag, anything
|
||||
unknown) is not restored, and neither is a user's email code sign-in flag.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.app.models.settings import Settings
|
||||
from backend.app.schemas.github_backup import RestoreCategory
|
||||
from backend.app.services.github_restore import _ADDRESS_KEYS, SETTINGS_PATH, GitHubRestoreService, _CategoryTally
|
||||
|
||||
|
||||
async def _restore(db_session, values: dict, local: dict | None = None):
|
||||
if local:
|
||||
db_session.add_all(Settings(key=key, value=value) for key, value in local.items())
|
||||
await db_session.commit()
|
||||
tally, written = _CategoryTally(), set()
|
||||
await GitHubRestoreService()._restore_settings(
|
||||
db_session, {"settings": values}, overwrite=True, tally=tally, keys_written=written
|
||||
)
|
||||
await db_session.commit()
|
||||
rows = (await db_session.execute(select(Settings))).scalars().all()
|
||||
return tally, written, {row.key: row.value for row in rows}
|
||||
|
||||
|
||||
def _note(tally: _CategoryTally, code: str) -> dict | None:
|
||||
return next((note for note in tally.notes if note["code"] == code), None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("key", "refused", "accepted"),
|
||||
[
|
||||
("ha_url", "http://169.254.169.254/latest", "http://ha.local:8123"),
|
||||
("bambuddy_internal_url", "ftp://bambuddy.local", "http://bambuddy.local:8000"),
|
||||
("docker_compose_dir", "/opt/bambuddy; echo x", "/opt/bambuddy"),
|
||||
("gcode_snippets", "not json", '{"X1C": {"start": ""}}'),
|
||||
("obico_action", "explode", "notify"),
|
||||
("energy_price_source", "mars", "fixed"),
|
||||
("low_stock_threshold", "lots", "20"),
|
||||
("manyfold_url", "http://169.254.169.254", "http://manyfold.local"),
|
||||
("spoolman_url", "http://169.254.169.254:7912", "http://spoolman.local:7912"),
|
||||
("smtp_port", "five", "587"),
|
||||
],
|
||||
)
|
||||
async def test_a_value_the_editor_refuses_is_not_restored(db_session, key, refused, accepted):
|
||||
tally, written, stored = await _restore(db_session, {key: refused}, local={key: accepted})
|
||||
|
||||
assert stored[key] == accepted
|
||||
assert written == set()
|
||||
note = _note(tally, "settingsInvalidSkipped")
|
||||
assert note is not None and note["params"]["keys"] == key
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("key", "value"),
|
||||
[
|
||||
("ha_url", "http://ha.local:8123"),
|
||||
("docker_compose_dir", "/opt/bambuddy"),
|
||||
("auto_archive", "true"),
|
||||
("ams_temp_alarm", "None"),
|
||||
("energy_cost_per_kwh", "0.31"),
|
||||
("manyfold_url", "http://manyfold.local"),
|
||||
("manyfold_client_id", "client"),
|
||||
("smtp_host", "mail.local"),
|
||||
("smtp_port", "587"),
|
||||
("smtp_security", "ssl"),
|
||||
("smtp_auth_enabled", "false"),
|
||||
("smtp_from_email", "a@mail.local"),
|
||||
("smtp_from_name", "Bambuddy"),
|
||||
("smtp_username", "mailer"),
|
||||
("smtp_use_tls", "true"),
|
||||
],
|
||||
)
|
||||
async def test_a_value_the_editor_accepts_is_restored(db_session, key, value):
|
||||
# An address is restored only as this instance already has it.
|
||||
local = {key: value} if key in _ADDRESS_KEYS else None
|
||||
tally, written, stored = await _restore(db_session, {key: value}, local=local)
|
||||
|
||||
assert stored[key] == value
|
||||
assert written == {key}
|
||||
assert _note(tally, "settingsInvalidSkipped") is None
|
||||
assert _note(tally, "settingsUnknownSkipped") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
[
|
||||
"something_from_elsewhere",
|
||||
"debug_logging_enabled",
|
||||
"ams_drying_alarm_latch",
|
||||
"announcements_feed_serial",
|
||||
"stock_alerts_notified",
|
||||
"archive_auto_purge_last_run",
|
||||
"archive_auto_purge_enabled",
|
||||
"archive_auto_purge_days",
|
||||
"archive_auto_purge_stats",
|
||||
"library_trash_retention_days",
|
||||
"library_auto_purge_enabled",
|
||||
"library_auto_purge_days",
|
||||
"library_auto_purge_include_never_printed",
|
||||
"bambu_cloud_email",
|
||||
"orca_cloud_email",
|
||||
"orca_cloud_pending_at",
|
||||
"virtual_printer_model",
|
||||
"ha_env_managed",
|
||||
],
|
||||
)
|
||||
async def test_a_key_no_editor_writes_is_not_restored(db_session, key):
|
||||
tally, written, stored = await _restore(db_session, {key: "true", "currency": "EUR"})
|
||||
|
||||
assert key not in stored
|
||||
assert written == {"currency"}
|
||||
note = _note(tally, "settingsUnknownSkipped")
|
||||
assert note is not None and note["params"]["keys"] == key
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_users_email_code_sign_in_is_never_switched_by_a_restore(db_session):
|
||||
tally, written, stored = await _restore(
|
||||
db_session, {"user_1_email_2fa_enabled": "false"}, local={"user_1_email_2fa_enabled": "true"}
|
||||
)
|
||||
|
||||
assert stored["user_1_email_2fa_enabled"] == "true"
|
||||
assert written == set()
|
||||
assert _note(tally, "settingsTwoFactorSkipped") is not None
|
||||
assert _note(tally, "settingsUnknownSkipped") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_preview_counts_only_what_will_be_written(db_session):
|
||||
parsed = {
|
||||
SETTINGS_PATH: {
|
||||
"settings": {
|
||||
"currency": "EUR",
|
||||
"ha_url": "http://169.254.169.254/",
|
||||
"something_from_elsewhere": "true",
|
||||
"user_1_email_2fa_enabled": "false",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
count, detail = await GitHubRestoreService()._count_items(db_session, RestoreCategory.SETTINGS, parsed)
|
||||
|
||||
assert count == 1
|
||||
assert detail is not None and detail.code == "settingsNotRestorableWillSkip"
|
||||
assert detail.params == {"count": 3}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bambuddys_own_one_shot_flags_are_skipped_without_a_word(db_session):
|
||||
"""Every backup carries them; naming them on every restore would be noise."""
|
||||
tally, written, stored = await _restore(
|
||||
db_session, {"_backfill_1425_pipeline_permissions_done": "true", "currency": "EUR"}
|
||||
)
|
||||
parsed = {SETTINGS_PATH: {"settings": {"_backfill_1425_pipeline_permissions_done": "true", "currency": "EUR"}}}
|
||||
count, detail = await GitHubRestoreService()._count_items(db_session, RestoreCategory.SETTINGS, parsed)
|
||||
|
||||
assert "_backfill_1425_pipeline_permissions_done" not in stored
|
||||
assert written == {"currency"}
|
||||
assert tally.notes == []
|
||||
assert (count, detail) == (1, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("key", ["mqtt_port", "use_slicer_api", "energy_price_source", "auto_archive", "ftp_timeout"])
|
||||
async def test_a_saved_null_is_refused_where_the_settings_cannot_read_one(db_session, key):
|
||||
tally, written, stored = await _restore(db_session, {key: "None"})
|
||||
|
||||
assert key not in stored
|
||||
assert _note(tally, "settingsInvalidSkipped") is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("key", ["ams_temp_alarm", "default_printer_id", "open_in_slicer"])
|
||||
async def test_a_saved_null_is_restored_where_the_settings_read_one(db_session, key):
|
||||
_tally, written, stored = await _restore(db_session, {key: "None"})
|
||||
|
||||
assert stored[key] == "None"
|
||||
assert written == {key}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("key", "value", "stored_as"),
|
||||
[
|
||||
("smtp_port", "587.0", "587"),
|
||||
("smtp_port", " 587", "587"),
|
||||
("smtp_auth_enabled", "1", "true"),
|
||||
("mqtt_enabled", "yes", "true"),
|
||||
("mqtt_port", "1883.0", "1883"),
|
||||
("auto_archive", "True", "true"),
|
||||
("smtp_use_tls", "0", "false"),
|
||||
],
|
||||
)
|
||||
async def test_a_value_is_written_in_the_form_its_editor_stores(db_session, key, value, stored_as):
|
||||
local = {key: stored_as} if key in _ADDRESS_KEYS else None
|
||||
_tally, written, stored = await _restore(db_session, {key: value}, local=local)
|
||||
|
||||
assert stored[key] == stored_as
|
||||
assert written == {key}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("local", [{"external_url": "https://bambuddy.here.example"}, None])
|
||||
async def test_the_address_sign_in_links_point_to_is_never_changed_by_a_restore(db_session, local):
|
||||
"""Password reset and welcome links are built from it: a backup naming
|
||||
another address would send their tokens there."""
|
||||
tally, written, stored = await _restore(
|
||||
db_session, {"external_url": "https://elsewhere.example", "currency": "EUR"}, local=local
|
||||
)
|
||||
parsed = {SETTINGS_PATH: {"settings": {"external_url": "https://elsewhere.example", "currency": "EUR"}}}
|
||||
count, detail = await GitHubRestoreService()._count_items(db_session, RestoreCategory.SETTINGS, parsed)
|
||||
|
||||
assert stored.get("external_url") == (local or {}).get("external_url")
|
||||
assert written == {"currency"}
|
||||
note = _note(tally, "settingsAddressKept")
|
||||
assert note is not None and note["params"]["keys"] == "external_url"
|
||||
assert (count, detail.code if detail else None) == (1, "settingsNotRestorableWillSkip")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_same_sign_in_link_address_restores_quietly(db_session):
|
||||
tally, _written, stored = await _restore(
|
||||
db_session,
|
||||
{"external_url": "https://bambuddy.here.example"},
|
||||
local={"external_url": "https://bambuddy.here.example"},
|
||||
)
|
||||
|
||||
assert stored["external_url"] == "https://bambuddy.here.example"
|
||||
assert _note(tally, "settingsAddressKept") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("key", "here", "elsewhere"),
|
||||
[
|
||||
("external_url", "https://bambuddy.here.example", "https://elsewhere.example"),
|
||||
("smtp_host", "mail.here.example", "mail.elsewhere.example"),
|
||||
("smtp_port", "587", "25"),
|
||||
("smtp_security", "starttls", "none"),
|
||||
("smtp_use_tls", "true", "false"),
|
||||
("local_backup_path", "/data/backups", "/srv/share"),
|
||||
("mqtt_broker", "broker.here.example", "broker.elsewhere.example"),
|
||||
("mqtt_port", "1883", "1884"),
|
||||
("mqtt_use_tls", "false", "true"),
|
||||
("ha_url", "http://ha.here.example:8123", "http://ha.elsewhere.example:8123"),
|
||||
("obico_ml_url", "http://obico.here.example:3333", "http://obico.elsewhere.example:3333"),
|
||||
("bambuddy_internal_url", "http://bambuddy.here.example:8000", "http://bambuddy.elsewhere.example:8000"),
|
||||
("orcaslicer_api_url", "http://orca.here.example:3000", "http://orca.elsewhere.example:3000"),
|
||||
("bambu_studio_api_url", "http://studio.here.example:3000", "http://studio.elsewhere.example:3000"),
|
||||
("spoolman_url", "http://spoolman.here.example:7912", "http://spoolman.elsewhere.example:7912"),
|
||||
("manyfold_url", "http://manyfold.here.example", "http://manyfold.elsewhere.example"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_row", [True, False])
|
||||
async def test_a_backup_never_changes_where_bambuddy_sends_things(db_session, key, here, elsewhere, has_row):
|
||||
"""Mail, backups, printer status, snapshots, model files: an address from a
|
||||
backup is kept as this instance has it, set or not."""
|
||||
tally, written, stored = await _restore(db_session, {key: elsewhere}, local={key: here} if has_row else None)
|
||||
|
||||
assert stored.get(key) == (here if has_row else None)
|
||||
assert written == set()
|
||||
note = _note(tally, "settingsAddressKept")
|
||||
assert note is not None and note["params"]["keys"] == key
|
||||
|
||||
|
||||
def test_every_address_key_is_one_a_restore_would_otherwise_write():
|
||||
"""An address key nothing checks would be dead weight in the list."""
|
||||
from backend.app.services.github_restore import _checked_setting
|
||||
|
||||
assert set(_ADDRESS_KEYS) == {key for key in _ADDRESS_KEYS if _checked_setting(key, "")[0] is not None}
|
||||
@@ -5676,6 +5676,7 @@ export default {
|
||||
settingsCompanionWillSkip: '{{count}} zugangsdatenähnliche Schlüssel werden übersprungen und {{companion}} davon abhängige Schalter bleiben aus',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} Schalter bleiben aus - die dafür nötigen Zugangsdaten können nicht aus einem Backup wiederhergestellt werden',
|
||||
settingsDestinationWillKeep: '{{destination}} Einstellung(en) bleiben unverändert - sie würden hier gespeicherte Zugangsdaten an ein anderes Ziel senden; Zugangsdaten werden nie wiederhergestellt',
|
||||
settingsNotRestorableWillSkip: '{{count}} Einstellung(en) werden übersprungen - Werte, die die Einstellungen nicht annehmen würden, oder Schlüssel, die ein Backup nicht wiederherstellt',
|
||||
spoolsUsageCount: 'davon {{count}} Verbrauchseinträge',
|
||||
archivesMetadataOnly: 'Nur Metadaten - 3MF-Dateien und Vorschaubilder sind nicht im Git-Backup enthalten',
|
||||
kprofilesPrinterCount: 'über {{count}} Drucker',
|
||||
@@ -5696,6 +5697,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} Authentifizierungseinstellungen übersprungen - ändern Sie diese unter Einstellungen > Authentifizierung, damit die Aussperrprüfungen greifen',
|
||||
settingsCompanionSkipped: '{{keys}} bleiben ausgeschaltet - die jeweils benötigten Zugangsdaten lassen sich nicht aus einem Backup wiederherstellen und sind auf dieser Instanz nicht hinterlegt, ein Einschalten würde die Integration also ohne Authentifizierung lassen',
|
||||
settingsDestinationKept: '{{keys}} unverändert gelassen - diese Instanz hat gespeicherte Zugangsdaten, die sonst an die Adresse aus dem Backup gesendet würden. Ändere sie in den Einstellungen und gib die Zugangsdaten erneut ein.',
|
||||
settingsTwoFactorSkipped: '{{count}} Einstellung(en) für Anmeldecodes per E-Mail übersprungen - jeder Benutzer schaltet E-Mail-Codes in seinem eigenen Konto ein oder aus',
|
||||
settingsInvalidSkipped: '{{keys}} übersprungen - der Wert im Backup ist keiner, den die Einstellungen annehmen würden',
|
||||
settingsUnknownSkipped: '{{keys}} übersprungen - keine Einstellungen, die ein Backup wiederherstellt (Laufzeitzustand, Kontositzungen oder unbekannte Schlüssel)',
|
||||
settingsAddressKept: '{{keys}} unverändert gelassen - sie bestimmen, wohin Bambuddy Daten, E-Mails oder Backups sendet; ändere sie in den Einstellungen',
|
||||
settingsMqttRelayFailed: 'MQTT-Einstellungen wiederhergestellt, aber das Relay konnte nicht neu verbunden werden - Bambuddy neu starten',
|
||||
kprofilesAlwaysOverwrite: 'K-Profile überschreiben immer den passenden Slot auf dem Drucker',
|
||||
kprofilesAckUnreliable: 'Ein Drucker, der nicht antwortet, zählt weiterhin als wiederhergestellt - überprüfen Sie die Profile am Drucker',
|
||||
|
||||
@@ -5725,6 +5725,7 @@ export default {
|
||||
settingsCompanionWillSkip: '{{count}} credential-like key(s) will be skipped, and {{companion}} switch(es) that depend on them will be left off',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} switch(es) will be left off - the credential each one needs cannot be restored from a backup',
|
||||
settingsDestinationWillKeep: '{{destination}} setting(s) will be left as they are - they would send a credential stored here somewhere else; credentials are never restored',
|
||||
settingsNotRestorableWillSkip: '{{count}} setting(s) will be skipped - values Settings would not accept, or keys a backup does not restore',
|
||||
spoolsUsageCount: 'including {{count}} usage record(s)',
|
||||
archivesMetadataOnly: 'Metadata only - 3MF files and thumbnails are not in a Git backup',
|
||||
kprofilesPrinterCount: 'across {{count}} printer(s)',
|
||||
@@ -5748,6 +5749,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} authentication setting(s) skipped - change those in Settings > Authentication so the lockout checks still run',
|
||||
settingsCompanionSkipped: '{{keys}} left switched off - the credential each one needs cannot be restored from a backup and this instance has none stored, so switching them on would leave the integration unauthenticated',
|
||||
settingsDestinationKept: '{{keys}} left as they are - this instance has a stored credential that would otherwise be sent to the address in the backup. Change them in Settings and enter the credential again.',
|
||||
settingsTwoFactorSkipped: '{{count}} email sign-in code setting(s) skipped - each user switches email codes on or off in their own account',
|
||||
settingsInvalidSkipped: '{{keys}} skipped - the value in the backup is not one Settings would accept',
|
||||
settingsUnknownSkipped: '{{keys}} skipped - not settings a backup restores (runtime state, account sessions or unknown keys)',
|
||||
settingsAddressKept: '{{keys}} left as they are - they decide where Bambuddy sends data, emails or backups; change them in Settings',
|
||||
settingsMqttRelayFailed: 'MQTT settings restored, but the relay could not be reconnected - restart Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'K-profiles always overwrite the matching slot on the printer',
|
||||
kprofilesAckUnreliable: 'A printer that does not answer still counts as restored - verify the profiles on the printer',
|
||||
|
||||
@@ -5683,6 +5683,7 @@ export default {
|
||||
settingsCompanionWillSkip: 'Se omitirán {{count}} claves con aspecto de credencial y {{companion}} interruptores que dependen de ellas quedarán desactivados',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} interruptores quedarán desactivados - la credencial que necesita cada uno no se puede restaurar desde una copia de seguridad',
|
||||
settingsDestinationWillKeep: '{{destination}} ajuste(s) se dejarán como están - enviarían una credencial guardada aquí a otro destino; las credenciales nunca se restauran',
|
||||
settingsNotRestorableWillSkip: '{{count}} ajuste(s) se omitirán - valores que Ajustes no aceptaría o claves que una copia de seguridad no restaura',
|
||||
spoolsUsageCount: 'incluidos {{count}} registros de consumo',
|
||||
archivesMetadataOnly: 'Solo metadatos - los archivos 3MF y las miniaturas no están en una copia de Git',
|
||||
kprofilesPrinterCount: 'en {{count}} impresoras',
|
||||
@@ -5703,6 +5704,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} ajustes de autenticación omitidos - cámbialos en Ajustes > Autenticación para que sigan aplicándose las comprobaciones de bloqueo',
|
||||
settingsCompanionSkipped: '{{keys}} se han dejado desactivados - la credencial que cada uno necesita no puede restaurarse desde una copia y esta instancia no tiene ninguna guardada, así que activarlos dejaría la integración sin autenticación',
|
||||
settingsDestinationKept: '{{keys}} se han dejado como están - esta instancia tiene una credencial guardada que, de lo contrario, se enviaría a la dirección de la copia de seguridad. Cámbialos en Ajustes e introduce la credencial de nuevo.',
|
||||
settingsTwoFactorSkipped: '{{count}} ajuste(s) de códigos de inicio de sesión por correo omitidos - cada usuario activa o desactiva los códigos por correo en su propia cuenta',
|
||||
settingsInvalidSkipped: '{{keys}} omitidos - el valor de la copia de seguridad no es uno que Ajustes aceptaría',
|
||||
settingsUnknownSkipped: '{{keys}} omitidos - no son ajustes que una copia de seguridad restaure (estado en ejecución, sesiones de cuenta o claves desconocidas)',
|
||||
settingsAddressKept: '{{keys}} se han dejado como están - deciden adónde envía Bambuddy datos, correos o copias de seguridad; cámbialos en Ajustes',
|
||||
settingsMqttRelayFailed: 'Ajustes MQTT restaurados, pero no se pudo reconectar el relé - reinicia Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'Los perfiles K siempre sobrescriben la ranura correspondiente en la impresora',
|
||||
kprofilesAckUnreliable: 'Una impresora que no responde sigue contando como restaurada - verifica los perfiles en la impresora',
|
||||
|
||||
@@ -5663,6 +5663,7 @@ export default {
|
||||
settingsCompanionWillSkip: '{{count}} clés ressemblant à des identifiants seront ignorées, et {{companion}} interrupteurs qui en dépendent resteront désactivés',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} interrupteurs resteront désactivés - les identifiants dont chacun a besoin ne peuvent pas être restaurés depuis une sauvegarde',
|
||||
settingsDestinationWillKeep: '{{destination}} paramètre(s) resteront inchangés - ils enverraient un identifiant enregistré ici vers une autre destination ; les identifiants ne sont jamais restaurés',
|
||||
settingsNotRestorableWillSkip: '{{count}} paramètre(s) seront ignorés - des valeurs que les Paramètres n\'accepteraient pas, ou des clés qu\'une sauvegarde ne restaure pas',
|
||||
spoolsUsageCount: 'dont {{count}} enregistrements de consommation',
|
||||
archivesMetadataOnly: 'Métadonnées uniquement - les fichiers 3MF et les miniatures ne sont pas dans une sauvegarde Git',
|
||||
kprofilesPrinterCount: 'sur {{count}} imprimantes',
|
||||
@@ -5683,6 +5684,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} réglages d\'authentification ignorés - modifiez-les dans Réglages > Authentification pour que les contrôles de verrouillage s\'appliquent',
|
||||
settingsCompanionSkipped: '{{keys}} laissés désactivés - l\'identifiant dont chacun a besoin ne peut pas être restauré depuis une sauvegarde et cette instance n\'en a aucun enregistré ; les activer laisserait donc l\'intégration sans authentification',
|
||||
settingsDestinationKept: '{{keys}} laissés inchangés - cette instance possède un identifiant enregistré qui serait sinon envoyé à l\'adresse de la sauvegarde. Modifiez-les dans les Paramètres et saisissez à nouveau l\'identifiant.',
|
||||
settingsTwoFactorSkipped: '{{count}} paramètre(s) de code de connexion par e-mail ignorés - chaque utilisateur active ou désactive les codes par e-mail dans son propre compte',
|
||||
settingsInvalidSkipped: '{{keys}} ignorés - la valeur de la sauvegarde n\'est pas une valeur que les Paramètres accepteraient',
|
||||
settingsUnknownSkipped: '{{keys}} ignorés - ce ne sont pas des paramètres qu\'une sauvegarde restaure (état d\'exécution, sessions de compte ou clés inconnues)',
|
||||
settingsAddressKept: '{{keys}} laissés tels quels - ils déterminent où Bambuddy envoie des données, des e-mails ou des sauvegardes ; modifiez-les dans les Paramètres',
|
||||
settingsMqttRelayFailed: 'Réglages MQTT restaurés, mais le relais n\'a pas pu être reconnecté - redémarrez Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'Les profils K écrasent toujours l\'emplacement correspondant sur l\'imprimante',
|
||||
kprofilesAckUnreliable: 'Une imprimante qui ne répond pas compte quand même comme restaurée - vérifiez les profils sur l\'imprimante',
|
||||
|
||||
@@ -5662,6 +5662,7 @@ export default {
|
||||
settingsCompanionWillSkip: '{{count}} chiavi simili a credenziali verranno saltate e {{companion}} interruttori che dipendono da esse resteranno disattivati',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} interruttori resteranno disattivati - le credenziali necessarie a ciascuno non possono essere ripristinate da un backup',
|
||||
settingsDestinationWillKeep: "{{destination}} impostazione/i resteranno invariate - invierebbero una credenziale salvata qui a un'altra destinazione; le credenziali non vengono mai ripristinate",
|
||||
settingsNotRestorableWillSkip: '{{count}} impostazione/i verranno saltate - valori che le Impostazioni non accetterebbero o chiavi che un backup non ripristina',
|
||||
spoolsUsageCount: 'inclusi {{count}} record di consumo',
|
||||
archivesMetadataOnly: 'Solo metadati - i file 3MF e le miniature non sono in un backup Git',
|
||||
kprofilesPrinterCount: 'su {{count}} stampanti',
|
||||
@@ -5682,6 +5683,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} impostazioni di autenticazione saltate - modificale in Impostazioni > Autenticazione così i controlli di blocco restano attivi',
|
||||
settingsCompanionSkipped: '{{keys}} lasciati disattivati - la credenziale richiesta da ciascuno non può essere ripristinata da un backup e questa istanza non ne ha nessuna salvata, quindi attivarli lascerebbe l\'integrazione senza autenticazione',
|
||||
settingsDestinationKept: "{{keys}} lasciati invariati - questa istanza ha una credenziale salvata che altrimenti verrebbe inviata all'indirizzo del backup. Modificali nelle Impostazioni e inserisci di nuovo la credenziale.",
|
||||
settingsTwoFactorSkipped: '{{count}} impostazione/i dei codici di accesso via email saltate - ogni utente attiva o disattiva i codici via email nel proprio account',
|
||||
settingsInvalidSkipped: '{{keys}} saltati - il valore nel backup non è uno che le Impostazioni accetterebbero',
|
||||
settingsUnknownSkipped: '{{keys}} saltati - non sono impostazioni che un backup ripristina (stato di esecuzione, sessioni di account o chiavi sconosciute)',
|
||||
settingsAddressKept: '{{keys}} lasciati invariati - decidono dove Bambuddy invia dati, email o backup; modificali nelle Impostazioni',
|
||||
settingsMqttRelayFailed: 'Impostazioni MQTT ripristinate, ma il relay non è stato riconnesso - riavvia Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'I profili K sovrascrivono sempre lo slot corrispondente sulla stampante',
|
||||
kprofilesAckUnreliable: 'Una stampante che non risponde conta comunque come ripristinata - verifica i profili sulla stampante',
|
||||
|
||||
@@ -5676,6 +5676,7 @@ export default {
|
||||
settingsCompanionWillSkip: '認証情報のようなキー {{count}} 件はスキップされ、それらに依存するスイッチ {{companion}} 件はオフのままになります',
|
||||
settingsCompanionOnlyWillSkip: 'スイッチ {{companion}} 件はオフのままになります - それぞれに必要な認証情報はバックアップから復元できません',
|
||||
settingsDestinationWillKeep: '{{destination}} 件の設定はそのままになります - ここに保存された認証情報を別の宛先に送ってしまうためです。認証情報は復元されません',
|
||||
settingsNotRestorableWillSkip: '{{count}} 件の設定はスキップされます - 設定画面が受け付けない値、またはバックアップから復元しないキーです',
|
||||
spoolsUsageCount: '使用履歴 {{count}} 件を含む',
|
||||
archivesMetadataOnly: 'メタデータのみ - 3MF ファイルとサムネイルは Git バックアップに含まれません',
|
||||
kprofilesPrinterCount: 'プリンター {{count}} 台分',
|
||||
@@ -5696,6 +5697,10 @@ export default {
|
||||
settingsAuthSkipped: '認証設定 {{count}} 件をスキップしました - ロックアウトチェックが働くよう、設定 > 認証で変更してください',
|
||||
settingsCompanionSkipped: '{{keys}} はオフのままにしました - 各項目に必要な認証情報はバックアップから復元できず、このインスタンスにも保存されていないため、オンにすると連携が未認証のままになります',
|
||||
settingsDestinationKept: '{{keys}} はそのままにしました - このインスタンスには保存済みの認証情報があり、変更するとバックアップのアドレスに送られてしまいます。設定で変更し、認証情報をもう一度入力してください。',
|
||||
settingsTwoFactorSkipped: 'メールのサインインコード設定 {{count}} 件をスキップしました - メールコードのオン・オフは各ユーザーが自分のアカウントで切り替えます',
|
||||
settingsInvalidSkipped: '{{keys}} をスキップしました - バックアップの値は設定画面が受け付ける値ではありません',
|
||||
settingsUnknownSkipped: '{{keys}} をスキップしました - バックアップから復元する設定ではありません(実行時の状態、アカウントのセッション、または不明なキー)',
|
||||
settingsAddressKept: '{{keys}} はそのままにしました - Bambuddy がデータ、メール、バックアップを送る先を決める設定です。設定画面で変更してください',
|
||||
settingsMqttRelayFailed: 'MQTT 設定を復元しましたが、リレーを再接続できませんでした - Bambuddy を再起動してください',
|
||||
kprofilesAlwaysOverwrite: 'K プロファイルは常にプリンター側の該当スロットを上書きします',
|
||||
kprofilesAckUnreliable: '応答しないプリンターも復元済みとして数えます - プロファイルはプリンター側で確認してください',
|
||||
|
||||
@@ -5438,6 +5438,7 @@ export default {
|
||||
settingsCompanionWillSkip: '자격 증명처럼 보이는 키 {{count}}개를 건너뛰고, 이에 의존하는 스위치 {{companion}}개는 꺼진 상태로 둡니다',
|
||||
settingsCompanionOnlyWillSkip: '스위치 {{companion}}개는 꺼진 상태로 둡니다 - 각각에 필요한 자격 증명은 백업에서 복원할 수 없습니다',
|
||||
settingsDestinationWillKeep: '설정 {{destination}}개는 그대로 둡니다 - 여기에 저장된 자격 증명을 다른 곳으로 보내게 되기 때문입니다. 자격 증명은 복원되지 않습니다',
|
||||
settingsNotRestorableWillSkip: '설정 {{count}}개를 건너뜁니다 - 설정 화면이 받아들이지 않는 값이거나 백업에서 복원하지 않는 키입니다',
|
||||
spoolsUsageCount: '사용 기록 {{count}}건 포함',
|
||||
archivesMetadataOnly: '메타데이터만 - 3MF 파일과 썸네일은 Git 백업에 포함되지 않습니다',
|
||||
kprofilesPrinterCount: '프린터 {{count}}대 분량',
|
||||
@@ -5458,6 +5459,10 @@ export default {
|
||||
settingsAuthSkipped: '인증 설정 {{count}}개를 건너뛰었습니다 - 잠금 검사가 계속 동작하도록 설정 > 인증에서 변경하세요',
|
||||
settingsCompanionSkipped: '{{keys}}을(를) 꺼진 상태로 두었습니다 - 각 항목에 필요한 자격 증명은 백업에서 복원할 수 없고 이 인스턴스에도 저장되어 있지 않아, 켜면 연동이 인증 없이 열립니다',
|
||||
settingsDestinationKept: '{{keys}}을(를) 그대로 두었습니다 - 이 인스턴스에 저장된 자격 증명이 백업의 주소로 전송될 수 있기 때문입니다. 설정에서 변경하고 자격 증명을 다시 입력하세요.',
|
||||
settingsTwoFactorSkipped: '이메일 로그인 코드 설정 {{count}}개를 건너뛰었습니다 - 이메일 코드는 각 사용자가 자신의 계정에서 켜거나 끕니다',
|
||||
settingsInvalidSkipped: '{{keys}}을(를) 건너뛰었습니다 - 백업의 값은 설정 화면이 받아들이는 값이 아닙니다',
|
||||
settingsUnknownSkipped: '{{keys}}을(를) 건너뛰었습니다 - 백업에서 복원하는 설정이 아닙니다(실행 상태, 계정 세션 또는 알 수 없는 키)',
|
||||
settingsAddressKept: '{{keys}}을(를) 그대로 두었습니다 - Bambuddy가 데이터, 이메일 또는 백업을 보내는 곳을 정합니다. 설정에서 변경하세요',
|
||||
settingsMqttRelayFailed: 'MQTT 설정을 복원했지만 릴레이를 다시 연결하지 못했습니다 - Bambuddy를 재시작하세요',
|
||||
kprofilesAlwaysOverwrite: 'K 프로파일은 항상 프린터의 해당 슬롯을 덮어씁니다',
|
||||
kprofilesAckUnreliable: '응답하지 않는 프린터도 복원됨으로 집계됩니다 - 프린터에서 프로파일을 확인하세요',
|
||||
|
||||
@@ -5723,6 +5723,7 @@ export default {
|
||||
settingsCompanionWillSkip: '{{count}} sleutel(s) die op inloggegevens lijken worden overgeslagen, en {{companion}} schakelaar(s) die daarvan afhankelijk zijn blijven uitgeschakeld',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} schakelaar(s) blijven uitgeschakeld - de benodigde inloggegevens kunnen niet uit een back-up worden hersteld',
|
||||
settingsDestinationWillKeep: '{{destination}} instelling(en) blijven ongewijzigd - ze zouden hier opgeslagen inloggegevens naar een andere bestemming sturen; inloggegevens worden nooit hersteld',
|
||||
settingsNotRestorableWillSkip: '{{count}} instelling(en) worden overgeslagen - waarden die Instellingen niet zou accepteren, of sleutels die een back-up niet herstelt',
|
||||
spoolsUsageCount: 'inclusief {{count}} verbruiksrecord(s)',
|
||||
archivesMetadataOnly: 'Alleen metadata - 3MF-bestanden en miniaturen zitten niet in een Git-back-up',
|
||||
kprofilesPrinterCount: 'over {{count}} printer(s)',
|
||||
@@ -5746,6 +5747,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} authenticatie-instelling(en) overgeslagen - wijzig deze onder Instellingen > Authenticatie zodat vergrendelingscontroles blijven werken',
|
||||
settingsCompanionSkipped: '{{keys}} blijven uitgeschakeld - de benodigde inloggegevens kunnen niet uit een back-up worden hersteld en deze instantie heeft er geen opgeslagen, dus inschakelen zou de integratie zonder authenticatie achterlaten',
|
||||
settingsDestinationKept: '{{keys}} ongewijzigd gelaten - deze instantie heeft opgeslagen inloggegevens die anders naar het adres uit de back-up zouden worden gestuurd. Wijzig ze in Instellingen en voer de inloggegevens opnieuw in.',
|
||||
settingsTwoFactorSkipped: '{{count}} instelling(en) voor inlogcodes per e-mail overgeslagen - elke gebruiker zet e-mailcodes aan of uit in zijn eigen account',
|
||||
settingsInvalidSkipped: '{{keys}} overgeslagen - de waarde in de back-up is er geen die Instellingen zou accepteren',
|
||||
settingsUnknownSkipped: '{{keys}} overgeslagen - geen instellingen die een back-up herstelt (runtimestatus, accountsessies of onbekende sleutels)',
|
||||
settingsAddressKept: '{{keys}} ongewijzigd gelaten - ze bepalen waar Bambuddy gegevens, e-mails of back-ups naartoe stuurt; wijzig ze in Instellingen',
|
||||
settingsMqttRelayFailed: 'MQTT-instellingen hersteld, maar de relay kon niet opnieuw verbinden - start Bambuddy opnieuw',
|
||||
kprofilesAlwaysOverwrite: 'K-profielen overschrijven altijd de overeenkomende sleuf op de printer',
|
||||
kprofilesAckUnreliable: 'Een printer die niet antwoordt telt nog steeds als hersteld - controleer de profielen op de printer',
|
||||
|
||||
@@ -5662,6 +5662,7 @@ export default {
|
||||
settingsCompanionWillSkip: '{{count}} chaves parecidas com credenciais serão ignoradas, e {{companion}} chaves que dependem delas ficarão desligadas',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} chaves ficarão desligadas - a credencial que cada uma precisa não pode ser restaurada de um backup',
|
||||
settingsDestinationWillKeep: '{{destination}} configuração(ões) ficarão como estão - enviariam uma credencial salva aqui para outro destino; credenciais nunca são restauradas',
|
||||
settingsNotRestorableWillSkip: '{{count}} configuração(ões) serão ignoradas - valores que as Configurações não aceitariam ou chaves que um backup não restaura',
|
||||
spoolsUsageCount: 'incluindo {{count}} registros de consumo',
|
||||
archivesMetadataOnly: 'Somente metadados - arquivos 3MF e miniaturas não ficam em um backup Git',
|
||||
kprofilesPrinterCount: 'em {{count}} impressoras',
|
||||
@@ -5682,6 +5683,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} configurações de autenticação ignoradas - altere-as em Configurações > Autenticação para que as verificações de bloqueio continuem valendo',
|
||||
settingsCompanionSkipped: '{{keys}} ficaram desligados - a credencial que cada um precisa não pode ser restaurada de um backup e esta instância não tem nenhuma armazenada, então ligá-los deixaria a integração sem autenticação',
|
||||
settingsDestinationKept: '{{keys}} ficaram como estão - esta instância tem uma credencial salva que, caso contrário, seria enviada ao endereço do backup. Altere-os em Configurações e informe a credencial novamente.',
|
||||
settingsTwoFactorSkipped: '{{count}} configuração(ões) de código de login por e-mail ignoradas - cada usuário ativa ou desativa os códigos por e-mail na própria conta',
|
||||
settingsInvalidSkipped: '{{keys}} ignorados - o valor no backup não é um que as Configurações aceitariam',
|
||||
settingsUnknownSkipped: '{{keys}} ignorados - não são configurações que um backup restaura (estado de execução, sessões de conta ou chaves desconhecidas)',
|
||||
settingsAddressKept: '{{keys}} ficaram como estão - definem para onde o Bambuddy envia dados, e-mails ou backups; altere em Configurações',
|
||||
settingsMqttRelayFailed: 'Configurações MQTT restauradas, mas o relay não pôde ser reconectado - reinicie o Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'Os perfis K sempre sobrescrevem o slot correspondente na impressora',
|
||||
kprofilesAckUnreliable: 'Uma impressora que não responde ainda conta como restaurada - verifique os perfis na impressora',
|
||||
|
||||
@@ -5430,6 +5430,7 @@ export default {
|
||||
settingsCompanionWillSkip: 'Ключей, похожих на учётные данные, будет пропущено: {{count}}, а зависящие от них переключатели ({{companion}}) останутся выключенными',
|
||||
settingsCompanionOnlyWillSkip: 'Переключатели ({{companion}}) останутся выключенными - учётные данные, нужные каждому из них, нельзя восстановить из резервной копии',
|
||||
settingsDestinationWillKeep: 'Настройки ({{destination}}) останутся без изменений - они отправили бы сохранённые здесь учётные данные в другое место; учётные данные никогда не восстанавливаются',
|
||||
settingsNotRestorableWillSkip: 'Настройки ({{count}}) будут пропущены - значения, которые настройки не приняли бы, или ключи, которые резервная копия не восстанавливает',
|
||||
spoolsUsageCount: 'включая записей расхода: {{count}}',
|
||||
archivesMetadataOnly: 'Только метаданные - файлы 3MF и миниатюры не входят в резервную копию Git',
|
||||
kprofilesPrinterCount: 'по {{count}} принтерам',
|
||||
@@ -5450,6 +5451,10 @@ export default {
|
||||
settingsAuthSkipped: 'Настроек аутентификации пропущено: {{count}} - меняйте их в разделе «Настройки > Аутентификация», чтобы продолжали работать проверки блокировки',
|
||||
settingsCompanionSkipped: '{{keys}} оставлены выключенными - нужные им учётные данные нельзя восстановить из резервной копии, и в этом экземпляре они не сохранены, поэтому включение оставило бы интеграцию без аутентификации',
|
||||
settingsDestinationKept: '{{keys}} оставлены без изменений - у этого экземпляра есть сохранённые учётные данные, которые иначе были бы отправлены на адрес из резервной копии. Измените их в настройках и введите учётные данные заново.',
|
||||
settingsTwoFactorSkipped: 'Настройки входа по коду из письма ({{count}}) пропущены - каждый пользователь включает или выключает коды по почте в своей учётной записи',
|
||||
settingsInvalidSkipped: '{{keys}} пропущены - значение в резервной копии не из тех, что приняли бы настройки',
|
||||
settingsUnknownSkipped: '{{keys}} пропущены - это не настройки, которые восстанавливает резервная копия (состояние работы, сеансы учётных записей или неизвестные ключи)',
|
||||
settingsAddressKept: '{{keys}} оставлены без изменений - они определяют, куда Bambuddy отправляет данные, письма или резервные копии; измените их в настройках',
|
||||
settingsMqttRelayFailed: 'Настройки MQTT восстановлены, но переподключить реле не удалось - перезапустите Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'K-профили всегда перезаписывают соответствующий слот на принтере',
|
||||
kprofilesAckUnreliable: 'Принтер, который не отвечает, всё равно считается восстановленным - проверьте профили на принтере',
|
||||
|
||||
@@ -5721,6 +5721,7 @@ errors: {
|
||||
settingsCompanionWillSkip: '{{count}} uppgift(er) av typen lösenord kommer att hoppas över, och {{companion}} växlar som beror på dem kommer att lämnas av',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} växlar kommer att lämnas av — den uppgift som var och en behöver kan inte återställas från en säkerhetskopia',
|
||||
settingsDestinationWillKeep: '{{destination}} inställning(ar) lämnas oförändrade - de skulle skicka en uppgift som sparats här någon annanstans; inloggningsuppgifter återställs aldrig',
|
||||
settingsNotRestorableWillSkip: '{{count}} inställning(ar) hoppas över - värden som Inställningar inte skulle godta, eller nycklar som en säkerhetskopia inte återställer',
|
||||
spoolsUsageCount: 'inklusive {{count}} användningspost(er)',
|
||||
archivesMetadataOnly: 'Endast metadata — 3MF-filer och minibilder finns inte i en Git-säkerhetskopia',
|
||||
kprofilesPrinterCount: 'över {{count}} skrivare',
|
||||
@@ -5744,6 +5745,10 @@ errors: {
|
||||
settingsAuthSkipped: '{{count}} autentiseringsinställning(ar) hoppade över — ändra dem i Inställningar > Autentisering så att uteslutningskontrollerna fortfarande körs',
|
||||
settingsCompanionSkipped: '{{keys}} lämnade avstängda — den uppgift som var och en behöver kan inte återställas från en säkerhetskopia och denna instans har ingen lagrad, så att slå på dem skulle lämna integrationen oautentiserad',
|
||||
settingsDestinationKept: '{{keys}} lämnade oförändrade - den här instansen har en sparad inloggningsuppgift som annars skulle skickas till adressen i säkerhetskopian. Ändra dem i Inställningar och ange uppgiften igen.',
|
||||
settingsTwoFactorSkipped: '{{count}} inställning(ar) för inloggningskoder via e-post hoppades över - varje användare slår på eller av e-postkoder i sitt eget konto',
|
||||
settingsInvalidSkipped: '{{keys}} hoppades över - värdet i säkerhetskopian är inte ett som Inställningar skulle godta',
|
||||
settingsUnknownSkipped: '{{keys}} hoppades över - inte inställningar som en säkerhetskopia återställer (körtillstånd, kontosessioner eller okända nycklar)',
|
||||
settingsAddressKept: '{{keys}} lämnades oförändrade - de avgör vart Bambuddy skickar data, e-post eller säkerhetskopior; ändra dem i Inställningar',
|
||||
settingsMqttRelayFailed: 'MQTT-inställningar återställda, men reläet kunde inte återanslutas — starta om Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'K-profiler skriver alltid över det matchande facket på skrivaren',
|
||||
kprofilesAckUnreliable: 'En skrivare som inte svarar räknas ändå som återställd — verifiera profilerna på skrivaren',
|
||||
|
||||
@@ -5651,6 +5651,7 @@ export default {
|
||||
settingsCompanionWillSkip: 'Kimlik bilgisi benzeri {{count}} anahtar atlanacak ve bunlara bağlı {{companion}} anahtar kapalı bırakılacak',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} anahtar kapalı bırakılacak - her birinin ihtiyaç duyduğu kimlik bilgisi bir yedekten geri yüklenemez',
|
||||
settingsDestinationWillKeep: '{{destination}} ayar olduğu gibi bırakılacak - burada kayıtlı bir kimlik bilgisini başka bir yere gönderirlerdi; kimlik bilgileri asla geri yüklenmez',
|
||||
settingsNotRestorableWillSkip: "{{count}} ayar atlanacak - Ayarlar'ın kabul etmeyeceği değerler veya bir yedeğin geri yüklemediği anahtarlar",
|
||||
spoolsUsageCount: '{{count}} kullanım kaydı dahil',
|
||||
archivesMetadataOnly: 'Yalnızca üst veri - 3MF dosyaları ve küçük resimler Git yedeğinde yer almaz',
|
||||
kprofilesPrinterCount: '{{count}} yazıcı genelinde',
|
||||
@@ -5671,6 +5672,10 @@ export default {
|
||||
settingsAuthSkipped: '{{count}} kimlik doğrulama ayarı atlandı - kilitlenme kontrolleri çalışmaya devam etsin diye bunları Ayarlar > Kimlik Doğrulama bölümünden değiştirin',
|
||||
settingsCompanionSkipped: '{{keys}} kapalı bırakıldı - her birinin ihtiyaç duyduğu kimlik bilgisi bir yedekten geri yüklenemez ve bu örnekte kayıtlı değil, dolayısıyla açmak entegrasyonu kimlik doğrulamasız bırakırdı',
|
||||
settingsDestinationKept: "{{keys}} olduğu gibi bırakıldı - bu örnekte kayıtlı bir kimlik bilgisi var ve aksi halde yedekteki adrese gönderilirdi. Bunları Ayarlar'dan değiştirin ve kimlik bilgisini yeniden girin.",
|
||||
settingsTwoFactorSkipped: '{{count}} e-posta oturum açma kodu ayarı atlandı - her kullanıcı e-posta kodlarını kendi hesabında açar veya kapatır',
|
||||
settingsInvalidSkipped: "{{keys}} atlandı - yedekteki değer Ayarlar'ın kabul edeceği bir değer değil",
|
||||
settingsUnknownSkipped: '{{keys}} atlandı - bir yedeğin geri yüklediği ayarlar değil (çalışma durumu, hesap oturumları veya bilinmeyen anahtarlar)',
|
||||
settingsAddressKept: "{{keys}} olduğu gibi bırakıldı - Bambuddy'nin verileri, e-postaları veya yedekleri nereye gönderdiğini belirler; Ayarlar'dan değiştirin",
|
||||
settingsMqttRelayFailed: 'MQTT ayarları geri yüklendi ancak röle yeniden bağlanamadı - Bambuddy\'yi yeniden başlatın',
|
||||
kprofilesAlwaysOverwrite: 'K profilleri yazıcıdaki eşleşen yuvanın her zaman üzerine yazar',
|
||||
kprofilesAckUnreliable: 'Yanıt vermeyen bir yazıcı yine de geri yüklendi sayılır - profilleri yazıcıda doğrulayın',
|
||||
|
||||
@@ -5716,6 +5716,7 @@ export default {
|
||||
settingsCompanionWillSkip: "Ключів, схожих на облікові дані, буде пропущено: {{count}}, а залежні від них перемикачі ({{companion}}) залишаться вимкненими",
|
||||
settingsCompanionOnlyWillSkip: 'Перемикачі ({{companion}}) залишаться вимкненими - облікові дані, потрібні кожному з них, не можна відновити з резервної копії',
|
||||
settingsDestinationWillKeep: 'Налаштування ({{destination}}) залишаться без змін - вони надіслали б збережені тут облікові дані в інше місце; облікові дані ніколи не відновлюються',
|
||||
settingsNotRestorableWillSkip: 'Налаштування ({{count}}) буде пропущено - значення, яких не прийняли б налаштування, або ключі, які резервна копія не відновлює',
|
||||
spoolsUsageCount: "включно із записами використання: {{count}}",
|
||||
archivesMetadataOnly: "Лише метадані - файли 3MF і мініатюри не входять до резервної копії Git",
|
||||
kprofilesPrinterCount: "по {{count}} принтерах",
|
||||
@@ -5736,6 +5737,10 @@ export default {
|
||||
settingsAuthSkipped: "Налаштувань автентифікації пропущено: {{count}} - змінюйте їх у розділі «Налаштування > Автентифікація», щоб перевірки блокування й далі працювали",
|
||||
settingsCompanionSkipped: "{{keys}} залишено вимкненими - потрібні їм облікові дані не можна відновити з резервної копії, і в цьому екземплярі вони не збережені, тож увімкнення залишило б інтеграцію без автентифікації",
|
||||
settingsDestinationKept: '{{keys}} залишено без змін - цей екземпляр має збережені облікові дані, які інакше були б надіслані на адресу з резервної копії. Змініть їх у налаштуваннях і введіть облікові дані знову.',
|
||||
settingsTwoFactorSkipped: 'Налаштування входу за кодом з листа ({{count}}) пропущено - кожен користувач вмикає або вимикає коди поштою у власному обліковому записі',
|
||||
settingsInvalidSkipped: '{{keys}} пропущено - значення в резервній копії не з тих, що прийняли б налаштування',
|
||||
settingsUnknownSkipped: '{{keys}} пропущено - це не налаштування, які відновлює резервна копія (стан роботи, сеанси облікових записів або невідомі ключі)',
|
||||
settingsAddressKept: '{{keys}} залишено без змін - вони визначають, куди Bambuddy надсилає дані, листи чи резервні копії; змініть їх у налаштуваннях',
|
||||
settingsMqttRelayFailed: "Налаштування MQTT відновлено, але реле не вдалося перепідключити - перезапустіть Bambuddy",
|
||||
kprofilesAlwaysOverwrite: "K-профілі завжди перезаписують відповідний слот на принтері",
|
||||
kprofilesAckUnreliable: "Принтер, який не відповідає, усе одно вважається відновленим - перевірте профілі на принтері",
|
||||
|
||||
@@ -5662,6 +5662,7 @@ export default {
|
||||
settingsCompanionWillSkip: '将跳过 {{count}} 个疑似凭据的键,依赖它们的 {{companion}} 个开关将保持关闭',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} 个开关将保持关闭 - 每个开关所需的凭据无法从备份中恢复',
|
||||
settingsDestinationWillKeep: '{{destination}} 项设置将保持不变 - 它们会把此处存储的凭据发送到别处;凭据永远不会被恢复',
|
||||
settingsNotRestorableWillSkip: '{{count}} 项设置将被跳过 - 设置不会接受的值,或备份不恢复的键',
|
||||
spoolsUsageCount: '其中含 {{count}} 条使用记录',
|
||||
archivesMetadataOnly: '仅元数据 - 3MF 文件和缩略图不在 Git 备份中',
|
||||
kprofilesPrinterCount: '涉及 {{count}} 台打印机',
|
||||
@@ -5682,6 +5683,10 @@ export default {
|
||||
settingsAuthSkipped: '已跳过 {{count}} 项认证设置 - 请在“设置 > 认证”中修改,以便锁定检查继续生效',
|
||||
settingsCompanionSkipped: '{{keys}} 保持关闭 - 它们各自所需的凭据无法从备份恢复,本实例也没有存储,开启会让集成处于未认证状态',
|
||||
settingsDestinationKept: '{{keys}} 保持不变 - 本实例存储了凭据,否则会被发送到备份中的地址。请在设置中修改它们并重新输入凭据。',
|
||||
settingsTwoFactorSkipped: '已跳过 {{count}} 项邮件登录验证码设置 - 每个用户在自己的账户中开启或关闭邮件验证码',
|
||||
settingsInvalidSkipped: '已跳过 {{keys}} - 备份中的值不是设置会接受的值',
|
||||
settingsUnknownSkipped: '已跳过 {{keys}} - 不是备份会恢复的设置(运行时状态、账户会话或未知的键)',
|
||||
settingsAddressKept: '{{keys}} 保持不变 - 它们决定 Bambuddy 将数据、邮件或备份发送到哪里;请在设置中修改',
|
||||
settingsMqttRelayFailed: 'MQTT 设置已恢复,但中继无法重新连接 - 请重启 Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'K 值配置总是覆盖打印机上对应的槽位',
|
||||
kprofilesAckUnreliable: '打印机不回应时仍计为已恢复 - 请在打印机上核对配置',
|
||||
|
||||
@@ -5662,6 +5662,7 @@ export default {
|
||||
settingsCompanionWillSkip: '將略過 {{count}} 個疑似憑證的鍵,依賴它們的 {{companion}} 個開關會維持關閉',
|
||||
settingsCompanionOnlyWillSkip: '{{companion}} 個開關會維持關閉 - 每個開關所需的憑證無法從備份還原',
|
||||
settingsDestinationWillKeep: '{{destination}} 項設定會維持不變 - 它們會把此處儲存的憑證傳送到別處;憑證永遠不會被還原',
|
||||
settingsNotRestorableWillSkip: '{{count}} 項設定將被略過 - 設定不會接受的值,或備份不還原的鍵',
|
||||
spoolsUsageCount: '其中含 {{count}} 筆使用紀錄',
|
||||
archivesMetadataOnly: '僅中繼資料 - 3MF 檔案與縮圖不在 Git 備份中',
|
||||
kprofilesPrinterCount: '涵蓋 {{count}} 台印表機',
|
||||
@@ -5682,6 +5683,10 @@ export default {
|
||||
settingsAuthSkipped: '已略過 {{count}} 項驗證設定 - 請在「設定 > 驗證」中修改,讓鎖定檢查繼續生效',
|
||||
settingsCompanionSkipped: '{{keys}} 維持關閉 - 它們各自所需的憑證無法從備份還原,本執行個體也沒有儲存,開啟會讓整合處於未驗證狀態',
|
||||
settingsDestinationKept: '{{keys}} 維持不變 - 本執行個體儲存了憑證,否則會被傳送到備份中的位址。請在設定中修改它們並重新輸入憑證。',
|
||||
settingsTwoFactorSkipped: '已略過 {{count}} 項電子郵件登入驗證碼設定 - 每位使用者在自己的帳戶中開啟或關閉電子郵件驗證碼',
|
||||
settingsInvalidSkipped: '已略過 {{keys}} - 備份中的值不是設定會接受的值',
|
||||
settingsUnknownSkipped: '已略過 {{keys}} - 不是備份會還原的設定(執行時狀態、帳戶工作階段或未知的鍵)',
|
||||
settingsAddressKept: '{{keys}} 維持不變 - 它們決定 Bambuddy 將資料、電子郵件或備份傳送到哪裡;請在設定中修改',
|
||||
settingsMqttRelayFailed: 'MQTT 設定已還原,但中繼無法重新連線 - 請重新啟動 Bambuddy',
|
||||
kprofilesAlwaysOverwrite: 'K 值設定檔一律覆寫印表機上對應的插槽',
|
||||
kprofilesAckUnreliable: '印表機未回應時仍計為已還原 - 請在印表機上核對設定檔',
|
||||
|
||||
+1
-1
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r,g as i,w as a}from"./client-C3LdD0iY.js";import{t as o}from"./useQuery-BSYE6Prb.js";import{a as s,t as c}from"./CameraWall-GFGvfrgJ.js";import{Bn as l,Wn as u,yt as d,zn as f}from"./index-BCcru1D3.js";var p=e(n(),1),m=t(),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}=a(),t=l(),{authEnabled:n,loading:s,user:S}=d(),[T]=u(),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&&(!n||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&&n&&!S)return(0,m.jsx)(f,{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{C as t,D as n,a as r,g as i,w as a}from"./client-C3LdD0iY.js";import{t as o}from"./useQuery-BSYE6Prb.js";import{a as s,t as c}from"./CameraWall-CJNqVbxn.js";import{Bn as l,Wn as u,yt as d,zn as f}from"./index-fwznsy2U.js";var p=e(n(),1),m=t(),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}=a(),t=l(),{authEnabled:n,loading:s,user:S}=d(),[T]=u(),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&&(!n||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&&n&&!S)return(0,m.jsx)(f,{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};
|
||||
+1
-1
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
@@ -1 +1 @@
|
||||
import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,w as r}from"./client-C3LdD0iY.js";import{a as i,i as a}from"./info-jpcgNqAh.js";import{t as o}from"./eye-off-BKHC5oyQ.js";import{t as s}from"./grip-vertical-eoFY62AH.js";import{n as c,t as l}from"./Card-BvtE18YT.js";import{t as u}from"./Button-B2stjvVl.js";import{Gt as d,Sn as f}from"./index-BCcru1D3.js";var p=e(n(),1),m=t();function h({isOpen:e,onClose:t,columns:n,defaultColumns:h,onSave:g}){let{t:_}=r(),[v,y]=(0,p.useState)(n),[b,x]=(0,p.useState)(null),S=(0,p.useRef)(null);if((0,p.useEffect)(()=>{e&&y(n.map(e=>({...e})))},[e,n]),(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)(f,{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)(d,{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{C as t,D as n,w as r}from"./client-C3LdD0iY.js";import{a as i,i as a}from"./info-jpcgNqAh.js";import{t as o}from"./eye-off-BKHC5oyQ.js";import{t as s}from"./grip-vertical-eoFY62AH.js";import{n as c,t as l}from"./Card-BvtE18YT.js";import{t as u}from"./Button-B2stjvVl.js";import{Gt as d,Sn as f}from"./index-fwznsy2U.js";var p=e(n(),1),m=t();function h({isOpen:e,onClose:t,columns:n,defaultColumns:h,onSave:g}){let{t:_}=r(),[v,y]=(0,p.useState)(n),[b,x]=(0,p.useState)(null),S=(0,p.useRef)(null);if((0,p.useEffect)(()=>{e&&y(n.map(e=>({...e})))},[e,n]),(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)(f,{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)(d,{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};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r,t as i,w as a}from"./client-C3LdD0iY.js";import{t as o}from"./link-2-BrJ-i2LF.js";import{t as s}from"./loader-circle-CbaPxZRP.js";import{t as c}from"./triangle-alert-BUlaDyuA.js";import{Wn as l,yt as u}from"./index-BCcru1D3.js";var d=e(n(),1),f=t();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}=a(),{authEnabled:t,loading:n}=u(),[m]=l(),[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(!(n||_.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`)})})}},[n,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{C as t,D as n,a as r,t as i,w as a}from"./client-C3LdD0iY.js";import{t as o}from"./link-2-BrJ-i2LF.js";import{t as s}from"./loader-circle-CbaPxZRP.js";import{t as c}from"./triangle-alert-BUlaDyuA.js";import{Wn as l,yt as u}from"./index-fwznsy2U.js";var d=e(n(),1),f=t();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}=a(),{authEnabled:t,loading:n}=u(),[m]=l(),[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(!(n||_.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`)})})}},[n,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
@@ -1 +1 @@
|
||||
import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r,w as i}from"./client-C3LdD0iY.js";import{t as a}from"./useQuery-BSYE6Prb.js";import{t as o}from"./loader-circle-CbaPxZRP.js";import{t as s}from"./triangle-alert-BUlaDyuA.js";import{Un as c,Vn as l,vt as u}from"./index-BCcru1D3.js";var d=e(n(),1),f=t();function p(){let{t:e}=i(),{id:t}=c(),{mode:n}=u(),l=(0,d.useRef)(null),p=m(l);g(l);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:l,src:h.url,onLoad:p,className:`h-full w-full border-0`,style:{colorScheme:n},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=l();(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{C as t,D as n,a as r,w as i}from"./client-C3LdD0iY.js";import{t as a}from"./useQuery-BSYE6Prb.js";import{t as o}from"./loader-circle-CbaPxZRP.js";import{t as s}from"./triangle-alert-BUlaDyuA.js";import{Un as c,Vn as l,vt as u}from"./index-fwznsy2U.js";var d=e(n(),1),f=t();function p(){let{t:e}=i(),{id:t}=c(),{mode:n}=u(),l=(0,d.useRef)(null),p=m(l);g(l);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:l,src:h.url,onLoad:p,className:`h-full w-full border-0`,style:{colorScheme:n},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=l();(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};
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r,w as i}from"./client-C3LdD0iY.js";import{t as a}from"./useQuery-BSYE6Prb.js";import{t as o}from"./arrow-left-CSvb6Y3A.js";import{t as s}from"./GcodeToolpathViewer-N4Eow1eU.js";import{Vn as c,Wn as l}from"./index-BCcru1D3.js";var u=e(n(),1),d=t();function f(){let e=c(),[t,n]=l(),{t:f}=i(),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 r=new URLSearchParams(t);r.set(`plate`,String(e)),n(r,{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{C as t,D as n,a as r,w as i}from"./client-C3LdD0iY.js";import{t as a}from"./useQuery-BSYE6Prb.js";import{t as o}from"./arrow-left-CSvb6Y3A.js";import{t as s}from"./GcodeToolpathViewer-N4Eow1eU.js";import{Vn as c,Wn as l}from"./index-fwznsy2U.js";var u=e(n(),1),d=t();function f(){let e=c(),[t,n]=l(),{t:f}=i(),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 r=new URLSearchParams(t);r.set(`plate`,String(e)),n(r,{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
@@ -1 +1 @@
|
||||
import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r,l as i,w as a}from"./client-C3LdD0iY.js";import{t as o}from"./loader-circle-CbaPxZRP.js";import{n as s,t as c}from"./zoom-out-CpxEPaFQ.js";import{t as l}from"./file-DDkFXH-b.js";import{Gt as u,un as d}from"./index-BCcru1D3.js";import{n as f,r as p,t as m}from"./usePreviewFullscreen-Bd6UWAwo.js";var h=e(n(),1),g=t(),_=.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:n,onClose:_}){let{t:v}=a(),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,a=null;if(j(null),N(!1),F(null),q(1,C),n>52428800){F(v(`fileManager.preview.tooLarge`,{size:l(n)}));return}let o={},s=i();return s&&(o.Authorization=`Bearer ${s}`),(async()=>{let n=await fetch(r.getLibraryFileDownloadUrl(e),{headers:o});if(!n.ok)throw Error(`HTTP ${n.status}`);let i=await n.blob();t||(a=URL.createObjectURL(i),j(a))})().catch(e=>{console.error(`[image-preview] load failed`,e),t||F(v(`fileManager.preview.error`))}),()=>{t=!0,a&&URL.revokeObjectURL(a)}},[e,n,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{C as t,D as n,a as r,l as i,w as a}from"./client-C3LdD0iY.js";import{t as o}from"./loader-circle-CbaPxZRP.js";import{n as s,t as c}from"./zoom-out-CpxEPaFQ.js";import{t as l}from"./file-DDkFXH-b.js";import{Gt as u,un as d}from"./index-fwznsy2U.js";import{n as f,r as p,t as m}from"./usePreviewFullscreen-Bd6UWAwo.js";var h=e(n(),1),g=t(),_=.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:n,onClose:_}){let{t:v}=a(),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,a=null;if(j(null),N(!1),F(null),q(1,C),n>52428800){F(v(`fileManager.preview.tooLarge`,{size:l(n)}));return}let o={},s=i();return s&&(o.Authorization=`Bearer ${s}`),(async()=>{let n=await fetch(r.getLibraryFileDownloadUrl(e),{headers:o});if(!n.ok)throw Error(`HTTP ${n.status}`);let i=await n.blob();t||(a=URL.createObjectURL(i),j(a))})().catch(e=>{console.error(`[image-preview] load failed`,e),t||F(v(`fileManager.preview.error`))}),()=>{t=!0,a&&URL.revokeObjectURL(a)}},[e,n,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
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
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
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
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
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
@@ -1 +1 @@
|
||||
import{ft as e}from"./index-BCcru1D3.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{ft as e}from"./index-fwznsy2U.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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
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
@@ -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-BCcru1D3.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-fwznsy2U.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/chunk-aKtaBQYM.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/client-C3LdD0iY.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/QueryClientProvider-DzWmuIjj.js">
|
||||
|
||||
Reference in New Issue
Block a user