feat(media): pagination
This commit is contained in:
1 parent
04dc151ed7
commit
63c1e7076e
65 files changed
+2960
-365
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user