feat(media): pagination

This commit is contained in:
Florian Sylvain committed 2026-10-13 19:54:59 +02:00
1 parent 04dc151ed7
commit 63c1e7076e
65 files changed
+2960 -365

No files matched your search

+79 -12
View File
@@ -1,7 +1,14 @@
import { ref } from 'vue'
import { tryOnBeforeUnmount } from '@vueuse/core'
import { ApiError, useApi } from '@/composables/useApi'
import { toRequestQuery, type MediaControls } from '@/lib/mediaQuery'
import {
MEDIA_FULL_SET_PAGE_SIZE,
isFullSet,
toHashRequestQuery,
toRequestQuery,
type MediaControls,
} from '@/lib/mediaQuery'
import { pageCountFor } from '@/lib/pagination'
import { bumpMediaRevision } from '@/lib/mediaRevision'
export interface MediaItem {
@@ -44,12 +51,24 @@ export interface BulkDeleteResult {
handled: boolean
}
interface MediaListResponse {
export interface MediaListResponse {
items: MediaItem[]
next_cursor: string | null
total: number
page: number
size: number
counts: MediaCounts
}
export type MediaLoadOutcome =
| { kind: 'loaded'; page: number; redirected: boolean }
| { kind: 'failed'; handled: boolean }
| { kind: 'stale' }
interface MediaLoadOptions {
silent?: boolean
keepOnError?: boolean
}
export type MediaLoadStatus = 'loading' | 'ready' | 'error' | 'handled'
export type MediaFailure = 'handled' | 'not-found' | 'failed'
@@ -68,10 +87,19 @@ export function classifyMediaError(error: unknown): MediaFailure {
return 'failed'
}
function isPastLastPage(response: MediaListResponse, page: number, size: number) {
return (
response.items.length === 0 &&
response.total > 0 &&
page > pageCountFor(response.total, size)
)
}
export function useMedia() {
const { request } = useApi()
const items = ref<MediaItem[]>([])
const total = ref(0)
const counts = ref<MediaCounts>({ all: 0, unused: 0, duplicates: 0 })
const status = ref<MediaLoadStatus>('loading')
const showSkeleton = ref(false)
@@ -94,7 +122,39 @@ export function useMedia() {
}, MEDIA_SKELETON_DELAY_MS)
}
async function load(controls: MediaControls, options: { silent?: boolean } = {}) {
function requestPage(controls: MediaControls) {
return request<MediaListResponse>(`/v1/media/?${toRequestQuery(controls)}`)
}
async function requestFullSet(controls: MediaControls) {
const query = { ...controls, page: 1, size: MEDIA_FULL_SET_PAGE_SIZE }
const first = await requestPage(query)
const loaded = [...first.items]
let latest = first
while (loaded.length < first.total && latest.items.length > 0) {
query.page += 1
latest = await requestPage(query)
loaded.push(...latest.items)
}
return { ...latest, items: loaded, total: loaded.length, page: 1 }
}
async function requestList(controls: MediaControls) {
if (isFullSet(controls)) {
return { response: await requestFullSet(controls), page: 1, redirected: false }
}
const response = await requestPage(controls)
if (!isPastLastPage(response, controls.page, controls.size)) {
return { response, page: response.total === 0 ? 1 : controls.page, redirected: false }
}
const page = pageCountFor(response.total, controls.size)
return { response: await requestPage({ ...controls, page }), page, redirected: true }
}
async function load(
controls: MediaControls,
options: MediaLoadOptions = {},
): Promise<MediaLoadOutcome> {
const silent = options.silent === true
if (!silent) {
latestLoad += 1
@@ -102,20 +162,24 @@ export function useMedia() {
}
const ticket = latestLoad
try {
const response = await request<MediaListResponse>(
`/v1/media/?${toRequestQuery(controls)}`,
)
const { response, page, redirected } = await requestList(controls)
if (ticket !== latestLoad) {
return
return { kind: 'stale' }
}
items.value = response.items
total.value = response.total
counts.value = response.counts
status.value = 'ready'
return { kind: 'loaded', page, redirected }
} catch (error) {
if (silent || ticket !== latestLoad) {
return
if (ticket !== latestLoad) {
return { kind: 'stale' }
}
status.value = classifyMediaError(error) === 'handled' ? 'handled' : 'error'
const handled = classifyMediaError(error) === 'handled'
if (!silent) {
status.value = handled ? 'handled' : options.keepOnError ? 'ready' : 'error'
}
return { kind: 'failed', handled }
} finally {
if (!silent && ticket === latestLoad) {
clearSkeletonTimer()
@@ -129,7 +193,9 @@ export function useMedia() {
}
async function findCopies(item: MediaItem) {
const response = await request<MediaListResponse>(`/v1/media/?hash=${item.sha256}`)
const response = await request<MediaListResponse>(
`/v1/media/?${toHashRequestQuery(item.sha256)}`,
)
return response.items.filter((copy) => copy.id !== item.id)
}
@@ -204,6 +270,7 @@ export function useMedia() {
return {
items,
total,
counts,
status,
showSkeleton,