import { ref, readonly, computed, toRaw } from 'vue' import { useNotification } from '~/composables/useNotification' import { useConfigStore } from '~/stores/ConfigStore' import { request, parse_api_error, sTrim, encodePath } from '~/utils' import type { FileItem, Pagination } from '~/types/filebrowser' const items = ref([]) const path = 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 lastError = ref(null) const selectedElms = ref([]) const masterSelectAll = ref(false) const sort_by = ref('name') const sort_order = ref('asc') const search = ref('') const throwInstead = ref(false) const notify = useNotification() 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 notify.error(message) } const buildQueryParams = (page?: number): string => { const config = useConfigStore() const params = new URLSearchParams() params.set('page', String(page ?? pagination.value.page)) params.set('per_page', String(config.app.default_pagination || 50)) params.set('sort_by', sort_by.value) params.set('sort_order', sort_order.value) if (search.value) { params.set('search', search.value) } return params.toString() } const loadContents = async (dir: string = '/', page: number = 1): Promise => { isLoading.value = true try { if (typeof dir !== 'string') { dir = '/' } dir = encodePath(sTrim(dir, '/')) const query = buildQueryParams(page) const response = await request(`/api/file/browser/${sTrim(dir, '/')}?${query}`) await ensureSuccess(response) const data = await response.json() items.value = data.contents || [] path.value = data.path || '/' if (data.pagination) { pagination.value = data.pagination } selectedElms.value = [] masterSelectAll.value = false lastError.value = null return true } catch (error) { handleError(error) if (throwInstead.value) throw error return false } finally { isLoading.value = false } } const changeSort = async (by: string): Promise => { if (!['name', 'size', 'date', 'type'].includes(by)) { return } if (by !== sort_by.value) { sort_by.value = by } else { sort_order.value = sort_order.value === 'asc' ? 'desc' : 'asc' } await loadContents(path.value, 1) } const setSearch = async (value: string): Promise => { search.value = value await loadContents(path.value, 1) } const setSortBy = (value: string): void => { sort_by.value = value } const setSortOrder = (value: string): void => { sort_order.value = value } const setSearchValue = (value: string): void => { search.value = value } const setPage = (value: number): void => { pagination.value.page = value } const changePage = async (page: number): Promise => { await loadContents(path.value, page) } const performAction = async ( item: FileItem, action: string, payload: Record, callback?: (item: FileItem, action: string, data: Record, source: FileItem) => void, multiple: boolean = false ): Promise => { try { const response = await request('/api/file/actions', { method: 'POST', body: JSON.stringify([{ path: item.path, action, ...payload }]), }) await ensureSuccess(response) const results = await response.json() as Array<{ path: string, status: boolean, error?: string, [key: string]: unknown }> for (const result of results) { if (!multiple && result.path !== item.path) { continue } if (!multiple && !result.status) { notify.error(`Failed to perform action: ${result.error || 'Unknown error'}`) return false } if (callback && typeof callback === 'function') { if (!multiple) { callback(item, action, payload as Record, item) } else { const matchedItem = items.value.find(it => it.path === result.path) if (matchedItem) { callback(matchedItem, action, result as Record, toRaw(item)) } } } } return true } catch (error) { handleError(error) if (throwInstead.value) throw error return false } } const performMassAction = async ( actions: Array<{ path: string, action: string, [key: string]: unknown }>, callback?: (result: { path: string, status: boolean, error?: string }) => void ): Promise => { try { const response = await request('/api/file/actions', { method: 'POST', body: JSON.stringify(actions), }) await ensureSuccess(response) const results = await response.json() as Array<{ path: string, status: boolean, error?: string }> for (const result of results) { if (!result.status) { notify.error(`Failed to perform action on '${result.path}': ${result.error || 'Unknown error'}`) continue } if (callback && typeof callback === 'function') { callback(result) } } return true } catch (error) { handleError(error) if (throwInstead.value) throw error return false } } const createDirectory = async (dir: string, newDir: string): Promise => { const trimmedDir = sTrim(newDir, '/') if (!trimmedDir || trimmedDir === dir) { return false } const success = await performAction( { path: dir || '/' } as FileItem, 'directory', { new_dir: trimmedDir }, () => { notify.success(`Successfully created '${trimmedDir}'.`) } ) return success } const renameItem = async (item: FileItem, newName: string): Promise => { const trimmedName = newName.trim() if (!trimmedName || trimmedName === item.name) { return false } return await performAction( item, 'rename', { new_name: trimmedName }, (it, _, data) => { const source = data as { new_path?: string } if (source.new_path) { it.name = source.new_path.split('/').pop() || trimmedName it.path = source.new_path } notify.success(`Renamed '${item.name}'.`) }, true ) } const deleteItem = async (item: FileItem): Promise => { return await performAction( item, 'delete', {}, () => { items.value = items.value.filter(i => i.path !== item.path) notify.warning(`Deleted '${item.name}'.`) } ) } const moveItem = async (item: FileItem, newPath: string): Promise => { const trimmedPath = sTrim(newPath, '/') || '/' if (!trimmedPath || trimmedPath === item.path) { return false } return await performAction( item, 'move', { new_path: trimmedPath }, (it, _, data) => { const source = data as { new_path?: string; path?: string } if (source.path) { items.value = items.value.filter(i => i.path !== source.path) } notify.success(`Moved '${item.name}' to '${trimmedPath}'.`) }, true ) } const deleteSelected = async (): Promise => { if (selectedElms.value.length < 1) { notify.error('No items selected.') return false } const actions = selectedElms.value.map(p => { return { path: p, action: 'delete' } }) const success = await performMassAction(actions, (result) => { const item = items.value.find(it => it.path === result.path) if (item) { items.value = items.value.filter(it => it.path !== result.path) notify.warning(`Deleted '${item.name}'.`) } }) if (success) { selectedElms.value = [] } return success } const moveSelected = async (newPath: string): Promise => { if (selectedElms.value.length < 1) { notify.error('No items selected.') return false } const trimmedPath = sTrim(newPath, '/') || '/' const actions = selectedElms.value.map(p => ({ path: p, action: 'move', new_path: trimmedPath, })) const success = await performMassAction(actions, (result) => { items.value = items.value.filter(it => it.path !== result.path) notify.success(`Moved '${result.path}' to '${trimmedPath}'.`) }) if (success) { selectedElms.value = [] } return success } const clearError = (): void => { lastError.value = null } const reset = (): void => { items.value = [] path.value = '/' pagination.value = { page: 1, per_page: 50, total: 0, total_pages: 0, has_next: false, has_prev: false, } selectedElms.value = [] masterSelectAll.value = false search.value = '' lastError.value = null } const filteredItems = computed(() => { return items.value }) export const useBrowser = () => ({ items: readonly(items), path: readonly(path), pagination: readonly(pagination), isLoading: readonly(isLoading), lastError: readonly(lastError), selectedElms, masterSelectAll, sort_by, sort_order, search, filteredItems, loadContents, changeSort, setSearch, setSortBy, setSortOrder, setSearchValue, setPage, changePage, createDirectory, renameItem, deleteItem, moveItem, deleteSelected, moveSelected, performAction, performMassAction, clearError, reset, throwInstead, })