Prechádzať zdrojové kódy

fix(archives): the timelapse player fits the screen with a portrait video (#3281)

- The viewer dialog is capped at the window height and the video at
  the window height minus the header and controls (object-contain,
  letterboxed), so a 9:16 timelapse from a rotated camera no longer
  pushes the controls off screen.
- The editor's video is capped at 55dvh the same way.
- The viewer's Edit, Download and Speed labels and skip tooltips are
  translated (timelapseViewer.speed added in all locales).
maziggy 18 hodín pred
rodič
commit
b602ea21fb
51 zmenil súbory, kde vykonal 117 pridanie a 13 odobranie
  1. 3 0
      CHANGELOG.md
  2. 8 0
      frontend/src/__tests__/components/TimelapseEditorModalJob.test.tsx
  3. 66 0
      frontend/src/__tests__/components/TimelapseViewer.test.tsx
  4. 3 1
      frontend/src/components/TimelapseEditorModal.tsx
  5. 17 7
      frontend/src/components/TimelapseViewer.tsx
  6. 1 0
      frontend/src/i18n/locales/de.ts
  7. 1 0
      frontend/src/i18n/locales/en.ts
  8. 1 0
      frontend/src/i18n/locales/es.ts
  9. 1 0
      frontend/src/i18n/locales/fr.ts
  10. 1 0
      frontend/src/i18n/locales/it.ts
  11. 1 0
      frontend/src/i18n/locales/ja.ts
  12. 1 0
      frontend/src/i18n/locales/ko.ts
  13. 1 0
      frontend/src/i18n/locales/nl.ts
  14. 1 0
      frontend/src/i18n/locales/pt-BR.ts
  15. 1 0
      frontend/src/i18n/locales/ru.ts
  16. 1 0
      frontend/src/i18n/locales/sv.ts
  17. 1 0
      frontend/src/i18n/locales/tr.ts
  18. 1 0
      frontend/src/i18n/locales/uk.ts
  19. 1 0
      frontend/src/i18n/locales/zh-CN.ts
  20. 1 0
      frontend/src/i18n/locales/zh-TW.ts
  21. 0 0
      static/assets/ArchivesPage-8vBHyzCo.js
  22. 0 0
      static/assets/ArchivesPage-bnpiMQ83.js
  23. 0 0
      static/assets/CameraPage-CIl2Zkoz.js
  24. 0 0
      static/assets/FileManagerPage-DCVa_1EV.js
  25. 0 0
      static/assets/InventoryPage-CwNNmdnM.js
  26. 0 0
      static/assets/Layout-CejbX0wr.js
  27. 0 0
      static/assets/ModelSourcesPage-9Ky0Kxm4.js
  28. 0 0
      static/assets/PhotoGalleryModal-DZJosi1Y.js
  29. 0 0
      static/assets/PrintersPage-ScyysW61.js
  30. 0 0
      static/assets/SettingsPage-HnuOTVll.js
  31. 0 0
      static/assets/SliceModal-l5ksL4WM.js
  32. 1 1
      static/assets/SpoolBuddySettingsPage-B64al-SO.js
  33. 0 0
      static/assets/de-Y07IYI18.js
  34. 0 0
      static/assets/es-BTAbh73m.js
  35. 0 0
      static/assets/fr-DOkDc6Df.js
  36. 0 0
      static/assets/index-BYDR9bb5.js
  37. 1 0
      static/assets/index-CLHFMRO6.css
  38. 0 1
      static/assets/index-Cs9GKX3n.css
  39. 0 0
      static/assets/it-JnxaM7GA.js
  40. 0 0
      static/assets/ja-BBu9zMeB.js
  41. 0 0
      static/assets/ko-Boo838yz.js
  42. 0 0
      static/assets/locationSensorDefaults-CBsK44tA.js
  43. 0 0
      static/assets/nl-_09VLpWC.js
  44. 0 0
      static/assets/pt-BR-BRcE6p5_.js
  45. 0 0
      static/assets/ru-JOmPvHRx.js
  46. 0 0
      static/assets/sv-BrgzaxpK.js
  47. 0 0
      static/assets/tr-BJpf6Z8C.js
  48. 0 0
      static/assets/uk-Cf9gRJfz.js
  49. 0 0
      static/assets/zh-CN-CCek_yCc.js
  50. 0 0
      static/assets/zh-TW-pdp9uINk.js
  51. 3 3
      static/index.html

+ 3 - 0
CHANGELOG.md

@@ -174,6 +174,9 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **The timelapse player fits the screen with a portrait video (#3281, reported by @BurgerKerman)** — A timelapse from a camera turned to portrait with the External Camera **Rotation** setting came out taller than a landscape screen. The player grew with it, so the bottom of the video and the play, seek and speed controls were off screen and could not be reached.
+  - **Now:** the player is never taller than the window, and the video is scaled down to leave room for the controls, with black bars at the sides. A landscape timelapse is only scaled down when the window is too short for it. In the timelapse editor the video takes at most a little over half the window's height.
+  - **Translated:** the player's Edit, Download and Speed labels and the skip buttons' tooltips were always in English.
 - **A color set on the printer's touchscreen is named after the Bambu color it is (#3279, reported by @Lioncat55)** — A slot without an RFID tag reports a hex a step or two off Bambu Studio's, for example `#5A657B` for **Blue Grey** (`#5B6579`). No catalog entry matched it exactly, so the slot was called by a general name such as "Blue", while the printer's screen showed a gray.
   - **Now:** when no catalog entry has exactly that hex, the color is named after the closest **Bambu Lab** color in the Color Catalog, as long as it is close enough to be the same color.
   - **Within the slot's own range:** the range comes from the slot's material or, without RFID, from the filament preset picked on the printer's screen. A PETG Translucent slot is named from PETG Translucent colors, and a range Bambu makes never borrows a nearby color's name from another range. Only a filament outside Bambu's ranges is compared with all of them.

+ 8 - 0
frontend/src/__tests__/components/TimelapseEditorModalJob.test.tsx

@@ -62,6 +62,14 @@ describe('TimelapseEditorModal re-encode job', () => {
     vi.mocked(api.cancelTimelapseJob).mockReset().mockResolvedValue(job({ status: 'cancelled' }));
   });
 
+  it('keeps a portrait video short enough to leave the controls on screen (#3281)', async () => {
+    open();
+    await screen.findByRole('button', { name: /save/i });
+    const video = document.querySelector('video');
+    expect(video?.className).toMatch(/max-h-\[55dvh\]/);
+    expect(video?.className).toContain('object-contain');
+  });
+
   it('Save starts the job, shows its progress and closes with the result', async () => {
     vi.mocked(api.getTimelapseJob)
       .mockResolvedValueOnce(null)

+ 66 - 0
frontend/src/__tests__/components/TimelapseViewer.test.tsx

@@ -0,0 +1,66 @@
+/**
+ * The timelapse player fits the screen whatever the video's shape (#3281).
+ *
+ * The video was sized by the width of the dialog alone, and the dialog had no
+ * height limit. A portrait timelapse -- a 16:9 camera turned to 9:16 with the
+ * External Camera rotation -- came out taller than a landscape screen, and
+ * pushed the play, seek and speed controls below the bottom edge where they
+ * could not be reached.
+ *
+ * jsdom does no layout, so these pin the limits that make it fit: the dialog
+ * is never taller than the screen, and the video leaves room for the header
+ * and the controls under it.
+ */
+
+import { describe, it, expect, afterEach, beforeAll, vi } from 'vitest';
+import { screen } from '@testing-library/react';
+import { render } from '../utils';
+import i18n from '../../i18n';
+import { TimelapseViewer } from '../../components/TimelapseViewer';
+
+function renderViewer() {
+  return render(
+    <TimelapseViewer src="/timelapse.mp4" title="Bracket - Timelapse" downloadFilename="bracket.mp4" archiveId={7} onClose={vi.fn()} />,
+  );
+}
+
+describe('TimelapseViewer', () => {
+  beforeAll(() => {
+    // jsdom has no media playback; autoPlay must not throw.
+    vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined);
+    vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
+  });
+
+  afterEach(async () => {
+    await i18n.changeLanguage('en');
+  });
+
+  it('keeps the dialog within the height of the screen', () => {
+    renderViewer();
+    const dialog = screen.getByTestId('timelapse-viewer');
+    expect(dialog.className).toMatch(/max-h-\[calc\(100dvh-2rem\)\]/);
+    expect(dialog.className).toContain('overflow-y-auto');
+  });
+
+  it('limits the video height and fits it inside that box', () => {
+    const { container } = renderViewer();
+    const video = container.querySelector('video');
+    expect(video).not.toBeNull();
+    expect(video!.className).toMatch(/max-h-\[calc\(100dvh-[0-9.]+rem\)\]/);
+    expect(video!.className).toContain('object-contain');
+  });
+
+  it('labels its buttons in the language of the page', async () => {
+    await i18n.loadLanguages('de');
+    await i18n.changeLanguage('de');
+    renderViewer();
+
+    expect(screen.getByText(i18n.t('common.edit'))).toBeInTheDocument();
+    expect(screen.getByText(i18n.t('common.download'))).toBeInTheDocument();
+    expect(screen.getByText(i18n.t('timelapseViewer.speed'))).toBeInTheDocument();
+    expect(screen.getByTitle(i18n.t('timelapseViewer.skipBack5s'))).toBeInTheDocument();
+    expect(screen.getByTitle(i18n.t('timelapseViewer.skipForward5s'))).toBeInTheDocument();
+    // German really was loaded, so the checks above did not pass on English.
+    expect(i18n.t('common.download')).not.toBe('Download');
+  });
+});

+ 3 - 1
frontend/src/components/TimelapseEditorModal.tsx

@@ -337,7 +337,9 @@ export function TimelapseEditorModal({
             <video
               ref={videoRef}
               src={timelapseSrc}
-              className="w-full rounded-lg bg-black"
+              // Short enough to leave the trim and speed controls on screen with
+              // a portrait timelapse (#3281); object-contain letterboxes it.
+              className="w-full max-h-[55dvh] object-contain rounded-lg bg-black"
               onClick={togglePlay}
               muted={!!audioUrl}
             />

+ 17 - 7
frontend/src/components/TimelapseViewer.tsx

@@ -1,4 +1,5 @@
 import { useState, useRef, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
 import { X, Download, Film, Play, Pause, SkipBack, SkipForward, Pencil } from 'lucide-react';
 import { Button } from './Button';
 import { TimelapseEditorModal } from './TimelapseEditorModal';
@@ -23,6 +24,7 @@ export function TimelapseViewer({
   onClose,
   onEdit,
 }: TimelapseViewerProps) {
+  const { t } = useTranslation();
   const videoRef = useRef<HTMLVideoElement>(null);
   const [isPlaying, setIsPlaying] = useState(true);
   const [playbackRate, setPlaybackRate] = useState(1); // Default to 1x
@@ -107,7 +109,13 @@ export function TimelapseViewer({
 
   return (
     <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
-      <div className="relative bg-bambu-dark-secondary rounded-xl max-w-4xl w-full mx-4 overflow-hidden">
+      {/* Never taller than the screen, so the controls stay reachable whatever
+          the video's shape -- a portrait timelapse from a rotated camera
+          pushed them off the bottom (#3281). */}
+      <div
+        data-testid="timelapse-viewer"
+        className="relative bg-bambu-dark-secondary rounded-xl max-w-4xl w-full mx-4 max-h-[calc(100dvh-2rem)] overflow-y-auto"
+      >
         {/* Header */}
         <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
           <h3 className="text-lg font-semibold text-white flex items-center gap-2">
@@ -118,12 +126,12 @@ export function TimelapseViewer({
             {archiveId && (
               <Button variant="secondary" size="sm" onClick={() => setShowEditor(true)}>
                 <Pencil className="w-4 h-4" />
-                Edit
+                {t('common.edit')}
               </Button>
             )}
             <Button variant="secondary" size="sm" onClick={handleDownload}>
               <Download className="w-4 h-4" />
-              Download
+              {t('common.download')}
             </Button>
             <button
               onClick={onClose}
@@ -140,7 +148,9 @@ export function TimelapseViewer({
             ref={videoRef}
             src={src}
             autoPlay
-            className="w-full rounded-lg"
+            // Leaves room for the header and the controls below it (about
+            // 12rem together); object-contain letterboxes what is left over.
+            className="w-full max-h-[calc(100dvh-14rem)] object-contain bg-black rounded-lg"
             onClick={togglePlay}
           />
 
@@ -174,7 +184,7 @@ export function TimelapseViewer({
                 <button
                   onClick={skipBackward}
                   className="p-2 hover:bg-bambu-dark-tertiary rounded-lg transition-colors"
-                  title="Skip back 5s"
+                  title={t('timelapseViewer.skipBack5s')}
                 >
                   <SkipBack className="w-5 h-5 text-bambu-gray" />
                 </button>
@@ -191,7 +201,7 @@ export function TimelapseViewer({
                 <button
                   onClick={skipForward}
                   className="p-2 hover:bg-bambu-dark-tertiary rounded-lg transition-colors"
-                  title="Skip forward 5s"
+                  title={t('timelapseViewer.skipForward5s')}
                 >
                   <SkipForward className="w-5 h-5 text-bambu-gray" />
                 </button>
@@ -199,7 +209,7 @@ export function TimelapseViewer({
 
               {/* Right: Speed control */}
               <div className="flex items-center gap-2">
-                <span className="text-sm text-bambu-gray">Speed:</span>
+                <span className="text-sm text-bambu-gray whitespace-nowrap">{t('timelapseViewer.speed')}</span>
                 <div className="flex gap-1">
                   {SPEED_OPTIONS.map((speed) => (
                     <button

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -7210,6 +7210,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Geschwindigkeit:',
     skipBack5s: '5s zurückspringen',
     skipForward5s: '5s vorspringen',
   },

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -7263,6 +7263,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Speed:',
     skipBack5s: 'Skip back 5s',
     skipForward5s: 'Skip forward 5s',
   },

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -7218,6 +7218,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Velocidad:',
     skipBack5s: 'Retroceder 5 s',
     skipForward5s: 'Avanzar 5 s',
   },

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -7198,6 +7198,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Vitesse :',
     skipBack5s: '-5s',
     skipForward5s: '+5s',
   },

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -7197,6 +7197,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Velocità:',
     skipBack5s: 'Indietro 5s',
     skipForward5s: 'Avanti 5s',
   },

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -7211,6 +7211,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: '速度:',
     skipBack5s: '5秒戻る',
     skipForward5s: '5秒進む',
   },

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -6872,6 +6872,7 @@ export default {
     dragToResize: '드래그하여 크기 조정'
   },
   timelapseViewer: {
+    speed: '속도:',
     skipBack5s: '5초 뒤로',
     skipForward5s: '5초 앞으로'
   },

+ 1 - 0
frontend/src/i18n/locales/nl.ts

@@ -7261,6 +7261,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Snelheid:',
     skipBack5s: '5 sec terug',
     skipForward5s: '5 sec vooruit',
   },

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -7197,6 +7197,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Velocidade:',
     skipBack5s: 'Voltar 5s',
     skipForward5s: 'Avançar 5s',
   },

+ 1 - 0
frontend/src/i18n/locales/ru.ts

@@ -6859,6 +6859,7 @@ export default {
     dragToResize: "Перетащите, чтобы изменить размер",
   },
   timelapseViewer: {
+    speed: 'Скорость:',
     skipBack5s: "Назад на 5 с",
     skipForward5s: "Вперёд на 5 с",
   },

+ 1 - 0
frontend/src/i18n/locales/sv.ts

@@ -7259,6 +7259,7 @@ errors: {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Hastighet:',
     skipBack5s: 'Hoppa bakåt 5s',
     skipForward5s: 'Hoppa framåt 5s',
   },

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -7154,6 +7154,7 @@ export default {
 
   // Zaman Atlamalı Video Görüntüleyici
   timelapseViewer: {
+    speed: 'Hız:',
     skipBack5s: '5 sn geri al',
     skipForward5s: '5 sn ileri al',
   },

+ 1 - 0
frontend/src/i18n/locales/uk.ts

@@ -7251,6 +7251,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: 'Швидкість:',
     skipBack5s: "Перейти на 5 секунд назад",
     skipForward5s: "Перейти вперед на 5 с",
   },

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -7195,6 +7195,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: '速度:',
     skipBack5s: '后退 5 秒',
     skipForward5s: '前进 5 秒',
   },

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -7195,6 +7195,7 @@ export default {
 
   // Timelapse Viewer
   timelapseViewer: {
+    speed: '速度:',
     skipBack5s: '後退 5 秒',
     skipForward5s: '前進 5 秒',
   },

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/ArchivesPage-8vBHyzCo.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/ArchivesPage-bnpiMQ83.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/CameraPage-CIl2Zkoz.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/FileManagerPage-DCVa_1EV.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/InventoryPage-CwNNmdnM.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/Layout-CejbX0wr.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/ModelSourcesPage-9Ky0Kxm4.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/PhotoGalleryModal-DZJosi1Y.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/PrintersPage-ScyysW61.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/SettingsPage-HnuOTVll.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/SliceModal-l5ksL4WM.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 1
static/assets/SpoolBuddySettingsPage-B64al-SO.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/de-Y07IYI18.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/es-BTAbh73m.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/fr-DOkDc6Df.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/index-BYDR9bb5.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
static/assets/index-CLHFMRO6.css


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 1
static/assets/index-Cs9GKX3n.css


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/it-JnxaM7GA.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/ja-BBu9zMeB.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/ko-Boo838yz.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/locationSensorDefaults-CBsK44tA.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/nl-_09VLpWC.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/pt-BR-BRcE6p5_.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/ru-JOmPvHRx.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/sv-BrgzaxpK.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/tr-BJpf6Z8C.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/uk-Cf9gRJfz.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/zh-CN-CCek_yCc.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
static/assets/zh-TW-pdp9uINk.js


+ 3 - 3
static/index.html

@@ -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-BSJ3-ebL.js"></script>
+    <script type="module" crossorigin src="/assets/index-BYDR9bb5.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-DSw2TdH4.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-PmRWqOqI.js">
@@ -120,8 +120,8 @@
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-CpiTIx_j.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-jD6ObGRm.js">
     <link rel="modulepreload" crossorigin href="/assets/plateCheck-fqh7_usj.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-F1iGI_1j.js">
-    <link rel="stylesheet" crossorigin href="/assets/index-Cs9GKX3n.css">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-CejbX0wr.js">
+    <link rel="stylesheet" crossorigin href="/assets/index-CLHFMRO6.css">
   </head>
   <body>
     <div id="root"></div>

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov