ytptube/ui/app/composables/useBrowser.ts
2026-02-20 22:46:30 +03:00

410 lines
9.8 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,
});