From 8ce0e65ccd12edc38fe891057655e91cf11a8d9f Mon Sep 17 00:00:00 2001 From: Eichenholz Date: Thu, 6 Aug 2026 16:57:29 +0200 Subject: [PATCH] ENH - paginate Gallery in batches of 30 --- frontend/src/utils/media.test.ts | 7 +++++++ frontend/src/utils/media.ts | 6 ++++++ frontend/src/views/apps/GalleryApp.vue | 12 +++++++---- frontend/testserver/index.cjs | 28 ++++++++++++-------------- sky_phone/config/media.lua | 2 +- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/frontend/src/utils/media.test.ts b/frontend/src/utils/media.test.ts index aa65b08..d444aec 100644 --- a/frontend/src/utils/media.test.ts +++ b/frontend/src/utils/media.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest' import { filterMedia, formatRecordingDuration, + hasNextMediaPage, mediaErrorKey, mergeMedia, } from './media' @@ -32,6 +33,12 @@ describe('media utilities', () => { ]) }) + it('loads another gallery page only after a full 30-item batch', () => { + expect(hasNextMediaPage(30)).toBe(true) + expect(hasNextMediaPage(29)).toBe(false) + expect(hasNextMediaPage(0)).toBe(false) + }) + it('formats unlimited recording durations', () => { expect(formatRecordingDuration(0)).toBe('00:00') expect(formatRecordingDuration(3_725_000)).toBe('62:05') diff --git a/frontend/src/utils/media.ts b/frontend/src/utils/media.ts index 90f8c49..1bc21e3 100644 --- a/frontend/src/utils/media.ts +++ b/frontend/src/utils/media.ts @@ -1,5 +1,7 @@ import type { GalleryFilter, MediaType, PhoneMedia } from '@/types/media' +export const MEDIA_PAGE_SIZE = 30 + export function isMediaType(value: unknown): value is MediaType { return value === 'photo' || value === 'video' } @@ -24,6 +26,10 @@ export function mergeMedia( ) } +export function hasNextMediaPage(pageLength: number): boolean { + return pageLength === MEDIA_PAGE_SIZE +} + export function formatRecordingDuration(elapsedMs: number): string { const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000)) const minutes = Math.floor(totalSeconds / 60) diff --git a/frontend/src/views/apps/GalleryApp.vue b/frontend/src/views/apps/GalleryApp.vue index 387f59e..862ad17 100644 --- a/frontend/src/views/apps/GalleryApp.vue +++ b/frontend/src/views/apps/GalleryApp.vue @@ -17,14 +17,18 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { usePhoneStore } from '@/stores/phone' import type { DeleteResult, GalleryFilter, PhoneMedia } from '@/types/media' -import { mediaErrorKey, mergeMedia } from '@/utils/media' +import { + hasNextMediaPage, + MEDIA_PAGE_SIZE, + mediaErrorKey, + mergeMedia, +} from '@/utils/media' import { nuiCall } from '@/utils/nui' const isDevelopment = import.meta.env.DEV const developmentGalleryState = isDevelopment ? new URLSearchParams(window.location.search).get('galleryMock') : null -const pageSize = 36 const filterItems = [ { id: 'all', label: 'all' }, { id: 'photo', label: 'photos' }, @@ -135,14 +139,14 @@ async function fetchMore(): Promise { fetching.value = true const offset = media.value.length const response = await nuiCall('gallery:list', { - limit: pageSize, + limit: MEDIA_PAGE_SIZE, mediaType: filter.value === 'all' ? undefined : filter.value, mockState: developmentGalleryState ?? undefined, offset, }) if (response.success && Array.isArray(response.data)) { media.value = mergeMedia(media.value, response.data) - hasMore.value = response.data.length === pageSize + hasMore.value = hasNextMediaPage(response.data.length) } else if ( isDevelopment && developmentGalleryState !== 'error' && diff --git a/frontend/testserver/index.cjs b/frontend/testserver/index.cjs index c1abc16..ae4502c 100644 --- a/frontend/testserver/index.cjs +++ b/frontend/testserver/index.cjs @@ -11,20 +11,18 @@ let authenticated = false let draft = null let linkedAccount = null let mockNotes = [] -let mockMedia = [ - { - createdAt: Date.now() - 60_000, - id: 1, - mediaType: 'photo', - url: 'https://picsum.photos/seed/sky-phone-1/600/800', - }, - { - createdAt: Date.now() - 120_000, - id: 2, - mediaType: 'video', - url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4', - }, -] +let mockMedia = Array.from({ length: 65 }, (_, index) => { + const id = index + 1 + const video = id % 10 === 0 + return { + createdAt: Date.now() - id * 60_000, + id, + mediaType: video ? 'video' : 'photo', + url: video + ? 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' + : `https://picsum.photos/seed/sky-phone-${id}/600/800`, + } +}) const deviceData = {} const accountDevices = [ { @@ -145,7 +143,7 @@ app.post('/api/:endpoint', (request, response) => { ? mockMedia.filter((item) => item.mediaType === request.body.mediaType) : mockMedia const offset = Number(request.body.offset) || 0 - const limit = Number(request.body.limit) || 36 + const limit = Number(request.body.limit) || 30 response.json({ success: true, data: filtered.slice(offset, offset + limit), diff --git a/sky_phone/config/media.lua b/sky_phone/config/media.lua index 45fde44..1fa5442 100644 --- a/sky_phone/config/media.lua +++ b/sky_phone/config/media.lua @@ -13,5 +13,5 @@ Config.Media = { BitrateKbps = 1500, }, UploadSessionTimeoutMs = 60000, - PageSize = 36, + PageSize = 30, }