ytptube/ui/app/composables/useBrowser.ts

406 lines
9.6 KiB
TypeScript

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<FileItem[]>([])
const path = ref<string>('/')
const pagination = ref<Pagination>({
page: 1,
per_page: 50,
total: 0,
total_pages: 0,
has_next: false,
has_prev: false,
})
const isLoading = ref<boolean>(false)
const lastError = ref<string | null>(null)
const selectedElms = ref<string[]>([])
const masterSelectAll = ref<boolean>(false)
const sort_by = ref<string>('name')
const sort_order = ref<string>('asc')
const search = ref<string>('')
const throwInstead = ref(false)
const notify = useNotification()
const readJson = async (response: Response): Promise<unknown> => {
try {
const clone = response.clone()
return await clone.json()
}
catch {
return null
}
}
const ensureSuccess = async (response: Response): Promise<void> => {
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<boolean> => {
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<void> => {
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<void> => {
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<void> => {
await loadContents(path.value, page)
}
const performAction = async (
item: FileItem,
action: string,
payload: Record<string, unknown>,
callback?: (item: FileItem, action: string, data: Record<string, unknown>, source: FileItem) => void,
multiple: boolean = false
): Promise<boolean> => {
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<string, unknown>, item)
}
else {
const matchedItem = items.value.find(it => it.path === result.path)
if (matchedItem) {
callback(matchedItem, action, result as Record<string, unknown>, 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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<FileItem[]>(() => {
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,
})