import { computed, ref } from 'vue'; import { useNotification } from '~/composables/useNotification'; import type { WSEP } from '~/types/sockets'; import { useYtpConfig } from '~/composables/useYtpConfig'; import { parse_api_error, parse_list_response, request } from '~/utils'; import type { Pagination } from '~/types/responses'; import type { StoreItem } from '~/types/store'; type HistoryLoadOptions = { order?: 'ASC' | 'DESC'; status?: string; perPage?: number; }; const items = ref([]); const pagination = ref({ page: 1, per_page: 50, total: 0, total_pages: 0, has_next: false, has_prev: false, }); const isLoading = ref(false); const isLoaded = ref(false); const lastError = ref(null); const throwInstead = ref(false); const readJson = async (response: Response): Promise => { try { const clone = response.clone(); return await clone.json(); } catch { return null; } }; const ensureSuccess = async (response: Response): Promise => { if (response.ok) { return; } const payload = await readJson(response); const message = await parse_api_error(payload); throw new Error(message); }; const handleError = (error: unknown): void => { const message = error instanceof Error ? error.message : 'Unexpected error occurred.'; lastError.value = message; useNotification().error(message); }; const pageSize = computed(() => { const config = useYtpConfig(); return Number(config.app.default_pagination || 50); }); const buildQuery = ( page: number, perPage: number, order: 'ASC' | 'DESC' = 'DESC', status?: string, ): string => { const params = new URLSearchParams({ type: 'done', page: String(page), per_page: String(perPage), order, }); if (status) { params.set('status', status); } return params.toString(); }; const load = async (page: number = 1, options: HistoryLoadOptions = {}): Promise => { const { order = 'DESC', status, perPage = pageSize.value } = options; isLoading.value = true; try { const response = await request(`/api/history?${buildQuery(page, perPage, order, status)}`); await ensureSuccess(response); const data = await response.json(); const { items: loadedItems, pagination: paginationData } = await parse_list_response(data); items.value = loadedItems; pagination.value = paginationData; isLoaded.value = true; lastError.value = null; } catch (error) { handleError(error); if (throwInstead.value) { throw error; } } finally { isLoading.value = false; } }; const reload = async (options: HistoryLoadOptions = {}): Promise => { const targetPage = isLoaded.value ? pagination.value.page : 1; await load(targetPage, options); }; const remove = async ( options: { ids?: string[]; status?: string; removeFile?: boolean; } = {}, ): Promise => { const { ids, status, removeFile = true } = options; if (!ids && !status) { throw new Error('Either ids or status filter must be provided'); } try { const response = await request('/api/history', { method: 'DELETE', body: JSON.stringify({ type: 'done', ids, status, remove_file: removeFile, }), }); await ensureSuccess(response); const result = (await response.json()) as { deleted?: number; error?: string; message?: string; }; if (result.error || result.message) { throw new Error(result.error || result.message); } return Number(result.deleted ?? 0); } catch (error) { handleError(error); if (throwInstead.value) { throw error; } return 0; } }; const rename = async (item: StoreItem, newName: string): Promise => { const trimmedName = newName.trim(); if (!trimmedName || trimmedName === item.filename?.split('/').pop()) { return false; } try { const response = await request(`/api/history/${item._id}/rename`, { method: 'POST', body: JSON.stringify({ new_name: trimmedName }), }); await ensureSuccess(response); const updated = (await response.json()) as StoreItem; const index = items.value.findIndex((entry) => entry._id === updated._id); if (index !== -1) { items.value = [...items.value.slice(0, index), updated, ...items.value.slice(index + 1)]; } lastError.value = null; return true; } catch (error) { handleError(error); if (throwInstead.value) { throw error; } return false; } }; const reset = (): void => { items.value = []; pagination.value = { page: 1, per_page: pageSize.value, total: 0, total_pages: 0, has_next: false, has_prev: false, }; isLoaded.value = false; lastError.value = null; }; const upsert = (item: StoreItem): void => { const existingIndex = items.value.findIndex((existing) => existing._id === item._id); if (existingIndex !== -1) { items.value = [ item, ...items.value.slice(0, existingIndex), ...items.value.slice(existingIndex + 1), ]; return; } items.value = [item, ...items.value]; pagination.value.total++; if (items.value.length > pagination.value.per_page) { items.value = items.value.slice(0, pagination.value.per_page); } pagination.value.total_pages = Math.max( 1, Math.ceil(pagination.value.total / pagination.value.per_page), ); pagination.value.has_next = pagination.value.page < pagination.value.total_pages; }; const moveHandler = ( shouldHandle: () => boolean = () => isLoaded.value, ): ((payload: WSEP['item_moved']) => void) => { return (payload: WSEP['item_moved']): void => { if ('history' !== payload.data.to || !shouldHandle()) { return; } upsert(payload.data.item); }; }; export const useHistoryState = () => { return { items, pagination, isLoading, isLoaded, lastError, pageSize, load, reload, remove, rename, reset, upsert, moveHandler, }; };