fix(home): preview reflects what's on the frame, not what's queued
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
Both the backend preview endpoint and the frontend cache-buster were preferring lockedImage over currentImage. Locking is a queued override that doesn't take effect until the device's next poll, so showing it on Home before the device has actually pulled it lied about the frame's state. Always use currentImage now. Also: add a primary "+ Add Photo" button at the top of the Library page so users can upload without bouncing back to Home; updates the empty- state copy to point at the new button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -472,13 +472,25 @@ describe('HomeView', () => {
|
|||||||
expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBe('/api/devices/1/preview?v=42')
|
expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBe('/api/devices/1/preview?v=42')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prefers lockedImageId over currentImageId for the thumbnail', async () => {
|
it('always uses currentImageId for the thumbnail — lockedImageId is ignored', async () => {
|
||||||
|
// Locked-but-not-yet-pulled is the bug we explicitly fixed: the home
|
||||||
|
// preview must reflect what the frame is actually showing, not what's
|
||||||
|
// queued for the next poll.
|
||||||
const devicesStore = useDevicesStore()
|
const devicesStore = useDevicesStore()
|
||||||
devicesStore.devices = [makeDevice({ id: 1, lockedImageId: 99, currentImageId: 42 })]
|
devicesStore.devices = [makeDevice({ id: 1, lockedImageId: 99, currentImageId: 42 })]
|
||||||
vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
|
vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
|
||||||
const wrapper = mountView()
|
const wrapper = mountView()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBe('/api/devices/1/preview?v=99')
|
expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBe('/api/devices/1/preview?v=42')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('omits the thumbnail when the device has no currentImageId, even if a lock is queued', async () => {
|
||||||
|
const devicesStore = useDevicesStore()
|
||||||
|
devicesStore.devices = [makeDevice({ id: 1, lockedImageId: 99, currentImageId: null })]
|
||||||
|
vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates editWakeHour when the user picks a different hour chip', async () => {
|
it('updates editWakeHour when the user picks a different hour chip', async () => {
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ vi.mock('@/stores/toast', () => ({
|
|||||||
useToastStore: () => ({ show: toastShow }),
|
useToastStore: () => ({ show: toastShow }),
|
||||||
}))
|
}))
|
||||||
const uploadInitEdit = vi.fn()
|
const uploadInitEdit = vi.fn()
|
||||||
|
const uploadInit = vi.fn()
|
||||||
vi.mock('@/stores/upload', () => ({
|
vi.mock('@/stores/upload', () => ({
|
||||||
useUploadStore: () => ({ initEdit: uploadInitEdit }),
|
useUploadStore: () => ({ initEdit: uploadInitEdit, init: uploadInit }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const makeImage = (overrides: Partial<Image> = {}): Image => ({
|
const makeImage = (overrides: Partial<Image> = {}): Image => ({
|
||||||
@@ -100,6 +101,7 @@ describe('LibraryView', () => {
|
|||||||
mockRoute.query = {}
|
mockRoute.query = {}
|
||||||
toastShow.mockClear()
|
toastShow.mockClear()
|
||||||
uploadInitEdit.mockClear()
|
uploadInitEdit.mockClear()
|
||||||
|
uploadInit.mockClear()
|
||||||
routerPush.mockClear()
|
routerPush.mockClear()
|
||||||
|
|
||||||
// Default fetch stub — returns empty lists so onMounted doesn't error
|
// Default fetch stub — returns empty lists so onMounted doesn't error
|
||||||
@@ -117,6 +119,56 @@ describe('LibraryView', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LV-00: Add Photo button at the top
|
||||||
|
it('renders an Add Photo button at the top of the page', async () => {
|
||||||
|
const imagesStore = useImagesStore()
|
||||||
|
imagesStore.images = []
|
||||||
|
imagesStore.loading = false
|
||||||
|
vi.spyOn(imagesStore, 'fetchImages').mockResolvedValue()
|
||||||
|
vi.spyOn(imagesStore, 'fetchPendingCount').mockResolvedValue()
|
||||||
|
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||||
|
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const addBtn = wrapper.find('.library__add-btn')
|
||||||
|
expect(addBtn.exists()).toBe(true)
|
||||||
|
expect(addBtn.text()).toContain('Add Photo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clicking the Add Photo button primes the upload store and routes to /upload', async () => {
|
||||||
|
const imagesStore = useImagesStore()
|
||||||
|
imagesStore.images = []
|
||||||
|
imagesStore.loading = false
|
||||||
|
vi.spyOn(imagesStore, 'fetchImages').mockResolvedValue()
|
||||||
|
vi.spyOn(imagesStore, 'fetchPendingCount').mockResolvedValue()
|
||||||
|
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||||
|
|
||||||
|
// Spy on createElement so we can intercept the synthetic file input
|
||||||
|
const realCreate = document.createElement.bind(document)
|
||||||
|
let capturedInput: HTMLInputElement | null = null
|
||||||
|
vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
||||||
|
const el = realCreate(tag)
|
||||||
|
if (tag === 'input') {
|
||||||
|
capturedInput = el as HTMLInputElement
|
||||||
|
;(el as HTMLInputElement).click = vi.fn()
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('.library__add-btn').trigger('click')
|
||||||
|
|
||||||
|
const file = new File(['x'], 'photo.jpg', { type: 'image/jpeg' })
|
||||||
|
Object.defineProperty(capturedInput, 'files', { value: [file], configurable: true })
|
||||||
|
capturedInput!.onchange?.(new Event('change'))
|
||||||
|
|
||||||
|
expect(uploadInit).toHaveBeenCalledWith(file)
|
||||||
|
expect(routerPush).toHaveBeenCalledWith('/upload')
|
||||||
|
})
|
||||||
|
|
||||||
// LV-01: Default tab shows "All" tab active
|
// LV-01: Default tab shows "All" tab active
|
||||||
it('renders the All tab as active by default', async () => {
|
it('renders the All tab as active by default', async () => {
|
||||||
const imagesStore = useImagesStore()
|
const imagesStore = useImagesStore()
|
||||||
|
|||||||
@@ -177,9 +177,13 @@ function nextSyncLabel(device: Device): string | null {
|
|||||||
return `next sync in ${Math.round(fromNow / 3_600_000)}h`
|
return `next sync in ${Math.round(fromNow / 3_600_000)}h`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Home shows what's actually on the frame right now — the last image the
|
||||||
|
// device pulled. Lock/queue state is intentionally ignored; the preview
|
||||||
|
// won't change until the frame next polls and switches to the locked image.
|
||||||
function previewUrl(device: Device): string | undefined {
|
function previewUrl(device: Device): string | undefined {
|
||||||
const imageId = device.lockedImageId ?? device.currentImageId
|
return device.currentImageId
|
||||||
return imageId ? `/api/devices/${device.id}/preview?v=${imageId}` : undefined
|
? `/api/devices/${device.id}/preview?v=${device.currentImageId}`
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
import FrameCard from '@/components/FrameCard.vue'
|
import FrameCard from '@/components/FrameCard.vue'
|
||||||
import BaseBottomSheet from '@/components/BaseBottomSheet.vue'
|
import BaseBottomSheet from '@/components/BaseBottomSheet.vue'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="library">
|
<main class="library">
|
||||||
<PullToRefresh :is-at-top="isAtTop" :on-refresh="refreshLibrary">
|
<PullToRefresh :is-at-top="isAtTop" :on-refresh="refreshLibrary">
|
||||||
|
<!-- Top action bar -->
|
||||||
|
<div class="library__header">
|
||||||
|
<BaseButton variant="primary" class="library__add-btn" @click="onAddPhoto">
|
||||||
|
+ Add Photo
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="library__tabs" role="tablist">
|
<div class="library__tabs" role="tablist">
|
||||||
<button
|
<button
|
||||||
@@ -26,7 +32,7 @@
|
|||||||
<polyline points="21,15 16,10 5,21"/>
|
<polyline points="21,15 16,10 5,21"/>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="library__empty-title">No photos yet</p>
|
<p class="library__empty-title">No photos yet</p>
|
||||||
<p class="library__empty-sub">Tap "+ Add Photo" on the home screen to get started.</p>
|
<p class="library__empty-sub">Tap "+ Add Photo" above to upload your first one.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="library__grid">
|
<div v-else class="library__grid">
|
||||||
@@ -291,6 +297,23 @@ onMounted(() => {
|
|||||||
if (activeTab.value === 'shared') loadShared(sharedTab.value)
|
if (activeTab.value === 'shared') loadShared(sharedTab.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Add Photo ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function onAddPhoto() {
|
||||||
|
// File picker must be triggered in the user-gesture context (the click
|
||||||
|
// handler) before navigating, otherwise browsers block it as a popup.
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = 'image/jpeg,image/png,image/webp,image/gif'
|
||||||
|
input.onchange = () => {
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
uploadStore.init(file)
|
||||||
|
router.push('/upload')
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Pull-to-refresh ───────────────────────────────────────────────────────────
|
// ── Pull-to-refresh ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function isAtTop(): boolean {
|
function isAtTop(): boolean {
|
||||||
@@ -414,6 +437,14 @@ async function doDelete() {
|
|||||||
.library {
|
.library {
|
||||||
padding-bottom: calc(var(--bottom-nav-height) + var(--space-4));
|
padding-bottom: calc(var(--bottom-nav-height) + var(--space-4));
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: var(--space-4) var(--space-4) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__tabs {
|
&__tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{A as e,O as t,R as n,_ as r,d as i,ft as a,g as o,h as s,l as c,o as l,p as u,t as d,u as f}from"./_plugin-vue_export-helper-CeYnMxKK.js";import{n as p,t as m}from"./BaseBottomSheet-Baz8jxBn.js";var h={class:`device-picker__list`},g=[`checked`,`onChange`],_={class:`device-picker__name`},v={class:`device-picker__orientation`},y=d(r({__name:`DevicePicker`,props:{modelValue:{type:Boolean},devices:{},selected:{},uploading:{type:Boolean}},emits:[`update:modelValue`,`update:selected`,`confirm`],setup(r,{emit:d}){let y=r,b=d;function x(e){y.selected.includes(e)?b(`update:selected`,y.selected.filter(t=>t!==e)):b(`update:selected`,[...y.selected,e])}let S=c(()=>{let e=y.selected.length;return e===0?`Add to frame`:`Add to ${e} frame${e>1?`s`:``}`});return(c,d)=>(t(),i(m,{"model-value":r.modelValue,label:`Choose frames`,"onUpdate:modelValue":d[1]||=e=>c.$emit(`update:modelValue`,e)},{default:n(()=>[d[2]||=f(`h2`,{class:`device-picker__title`},`Add to frames`,-1),d[3]||=f(`p`,{class:`device-picker__sub`},`Choose which frames will show this photo.`,-1),f(`div`,h,[(t(!0),u(l,null,e(r.devices,e=>(t(),u(`label`,{key:e.id,class:`device-picker__row`},[f(`input`,{type:`checkbox`,class:`device-picker__check`,checked:r.selected.includes(e.id),onChange:t=>x(e.id)},null,40,g),f(`span`,_,a(e.name),1),f(`span`,v,a(e.orientation),1)]))),128))]),o(p,{variant:`primary`,class:`device-picker__confirm`,disabled:r.selected.length===0||r.uploading,onClick:d[0]||=e=>c.$emit(`confirm`)},{default:n(()=>[s(a(r.uploading?`Uploading…`:S.value),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-a6466fa5`]]);export{y as t};
|
import{A as e,O as t,R as n,_ as r,d as i,ft as a,g as o,h as s,l as c,o as l,p as u,t as d,u as f}from"./_plugin-vue_export-helper-CeYnMxKK.js";import{n as p,t as m}from"./BaseBottomSheet-vZ7hFF0Y.js";var h={class:`device-picker__list`},g=[`checked`,`onChange`],_={class:`device-picker__name`},v={class:`device-picker__orientation`},y=d(r({__name:`DevicePicker`,props:{modelValue:{type:Boolean},devices:{},selected:{},uploading:{type:Boolean}},emits:[`update:modelValue`,`update:selected`,`confirm`],setup(r,{emit:d}){let y=r,b=d;function x(e){y.selected.includes(e)?b(`update:selected`,y.selected.filter(t=>t!==e)):b(`update:selected`,[...y.selected,e])}let S=c(()=>{let e=y.selected.length;return e===0?`Add to frame`:`Add to ${e} frame${e>1?`s`:``}`});return(c,d)=>(t(),i(m,{"model-value":r.modelValue,label:`Choose frames`,"onUpdate:modelValue":d[1]||=e=>c.$emit(`update:modelValue`,e)},{default:n(()=>[d[2]||=f(`h2`,{class:`device-picker__title`},`Add to frames`,-1),d[3]||=f(`p`,{class:`device-picker__sub`},`Choose which frames will show this photo.`,-1),f(`div`,h,[(t(!0),u(l,null,e(r.devices,e=>(t(),u(`label`,{key:e.id,class:`device-picker__row`},[f(`input`,{type:`checkbox`,class:`device-picker__check`,checked:r.selected.includes(e.id),onChange:t=>x(e.id)},null,40,g),f(`span`,_,a(e.name),1),f(`span`,v,a(e.orientation),1)]))),128))]),o(p,{variant:`primary`,class:`device-picker__confirm`,disabled:r.selected.length===0||r.uploading,onClick:d[0]||=e=>c.$emit(`confirm`)},{default:n(()=>[s(a(r.uploading?`Uploading…`:S.value),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-a6466fa5`]]);export{y as t};
|
||||||
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
+1
-1
@@ -1 +1 @@
|
|||||||
import{A as e,G as t,O as n,_ as r,dt as i,f as a,ft as o,l as s,o as c,p as l,t as u,u as d,ut as f}from"./_plugin-vue_export-helper-CeYnMxKK.js";import{n as p,r as m,t as h}from"./index-BvMU-pbo.js";var g={class:`settings`},_={class:`settings__section`},v={class:`theme-grid`,role:`radiogroup`,"aria-label":`Choose theme`},y=[`aria-checked`,`aria-label`,`onClick`],b={class:`theme-swatch__label`},x={key:0,class:`theme-swatch__check`,"aria-hidden":`true`},S={class:`settings__section`},C={class:`settings__row`},w={class:`settings__row-value`},T=u(r({__name:`SettingsView`,setup(r){let u=m(),{saveTheme:T}=p(),E=s(()=>u.user?.theme??`warm-craft`);function D(e){T(e)}return(r,s)=>(n(),l(`main`,g,[s[5]||=d(`h1`,{class:`settings__title`},`Settings`,-1),d(`section`,_,[s[1]||=d(`h2`,{class:`settings__section-title`},`Theme`,-1),d(`div`,v,[(n(!0),l(c,null,e(t(h),e=>(n(),l(`button`,{key:e.id,type:`button`,role:`radio`,"aria-checked":E.value===e.id,"aria-label":e.label,class:f([`theme-swatch`,{"theme-swatch--active":E.value===e.id}]),style:i({"--swatch-bg":e.bg,"--swatch-primary":e.primary,"--swatch-text":e.text}),onClick:t=>D(e.id)},[s[0]||=d(`span`,{class:`theme-swatch__preview`,"aria-hidden":`true`},[d(`span`,{class:`theme-swatch__bar`}),d(`span`,{class:`theme-swatch__dot`})],-1),d(`span`,b,o(e.label),1),E.value===e.id?(n(),l(`span`,x,`✓`)):a(``,!0)],14,y))),128))])]),d(`section`,S,[s[3]||=d(`h2`,{class:`settings__section-title`},`Account`,-1),d(`div`,C,[s[2]||=d(`span`,{class:`settings__row-label`},`Signed in as`,-1),d(`span`,w,o(t(u).user?.email),1)]),s[4]||=d(`a`,{href:`/logout`,class:`settings__logout`},`Sign out`,-1)])]))}}),[[`__scopeId`,`data-v-76ec3881`]]);export{T as default};
|
import{A as e,G as t,O as n,_ as r,dt as i,f as a,ft as o,l as s,o as c,p as l,t as u,u as d,ut as f}from"./_plugin-vue_export-helper-CeYnMxKK.js";import{n as p,r as m,t as h}from"./index-BGc6BnG_.js";var g={class:`settings`},_={class:`settings__section`},v={class:`theme-grid`,role:`radiogroup`,"aria-label":`Choose theme`},y=[`aria-checked`,`aria-label`,`onClick`],b={class:`theme-swatch__label`},x={key:0,class:`theme-swatch__check`,"aria-hidden":`true`},S={class:`settings__section`},C={class:`settings__row`},w={class:`settings__row-value`},T=u(r({__name:`SettingsView`,setup(r){let u=m(),{saveTheme:T}=p(),E=s(()=>u.user?.theme??`warm-craft`);function D(e){T(e)}return(r,s)=>(n(),l(`main`,g,[s[5]||=d(`h1`,{class:`settings__title`},`Settings`,-1),d(`section`,_,[s[1]||=d(`h2`,{class:`settings__section-title`},`Theme`,-1),d(`div`,v,[(n(!0),l(c,null,e(t(h),e=>(n(),l(`button`,{key:e.id,type:`button`,role:`radio`,"aria-checked":E.value===e.id,"aria-label":e.label,class:f([`theme-swatch`,{"theme-swatch--active":E.value===e.id}]),style:i({"--swatch-bg":e.bg,"--swatch-primary":e.primary,"--swatch-text":e.text}),onClick:t=>D(e.id)},[s[0]||=d(`span`,{class:`theme-swatch__preview`,"aria-hidden":`true`},[d(`span`,{class:`theme-swatch__bar`}),d(`span`,{class:`theme-swatch__dot`})],-1),d(`span`,b,o(e.label),1),E.value===e.id?(n(),l(`span`,x,`✓`)):a(``,!0)],14,y))),128))])]),d(`section`,S,[s[3]||=d(`h2`,{class:`settings__section-title`},`Account`,-1),d(`div`,C,[s[2]||=d(`span`,{class:`settings__row-label`},`Signed in as`,-1),d(`span`,w,o(t(u).user?.email),1)]),s[4]||=d(`a`,{href:`/logout`,class:`settings__logout`},`Sign out`,-1)])]))}}),[[`__scopeId`,`data-v-76ec3881`]]);export{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
@@ -14,7 +14,7 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="pictureFrame" />
|
<meta name="apple-mobile-web-app-title" content="pictureFrame" />
|
||||||
<script type="module" crossorigin src="/build/assets/index-BvMU-pbo.js"></script>
|
<script type="module" crossorigin src="/build/assets/index-BGc6BnG_.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-CeYnMxKK.js">
|
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-CeYnMxKK.js">
|
||||||
<link rel="stylesheet" crossorigin href="/build/assets/index-BlLBHR1q.css">
|
<link rel="stylesheet" crossorigin href="/build/assets/index-BlLBHR1q.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -176,9 +176,14 @@ class DeviceApiController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve a PNG preview of the image currently shown on the frame, decoded
|
* Serve a PNG preview of the image **currently shown on the frame**,
|
||||||
* from the device's rendered 4bpp Spectra-6 .bin so the colors match what
|
* decoded from the device's rendered 4bpp Spectra-6 .bin so the colors
|
||||||
* the e-ink actually displays. The PNG is cached on disk next to the .bin.
|
* match what the e-ink actually displays.
|
||||||
|
*
|
||||||
|
* Always uses currentImage — the last image the device pulled — never
|
||||||
|
* lockedImage. A lock is a queued override that won't take effect until
|
||||||
|
* the device next polls; surfacing it on Home before the device has
|
||||||
|
* actually fetched it would lie about what's on the frame.
|
||||||
*/
|
*/
|
||||||
#[Route('/{id}/preview', name: 'api_device_preview', methods: ['GET'])]
|
#[Route('/{id}/preview', name: 'api_device_preview', methods: ['GET'])]
|
||||||
public function preview(int $id, EntityManagerInterface $em): Response
|
public function preview(int $id, EntityManagerInterface $em): Response
|
||||||
@@ -190,7 +195,7 @@ class DeviceApiController extends AbstractController
|
|||||||
return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
|
return $this->json(['error' => 'Device not found'], Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$image = $device->getLockedImage() ?? $device->getCurrentImage();
|
$image = $device->getCurrentImage();
|
||||||
if (!$image || $image->isDeleted()) {
|
if (!$image || $image->isDeleted()) {
|
||||||
return $this->json(['error' => 'No current image'], Response::HTTP_NOT_FOUND);
|
return $this->json(['error' => 'No current image'], Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user