chore(frontend): slim admin.ts, remove dead library.ts
- Remove dead functions from admin.ts: loadSystemStats, renderSystemStats, triggerLibraryScan, triggerQuickScan, all WebSocket functions - Remove library.ts (695 lines of innerHTML string-building replaced by HTMX server-rendered partials) - Remove library import from main.ts
This commit is contained in:
+3
-140
@@ -1,134 +1,5 @@
|
||||
import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
import { createWebSocket } from "./websocket";
|
||||
|
||||
function initializeScanWebSocket(): void {
|
||||
createWebSocket({
|
||||
onMessage: (message) => {
|
||||
switch (message.type) {
|
||||
case "scan_progress":
|
||||
updateScanProgress(message.data);
|
||||
break;
|
||||
case "scan_complete":
|
||||
showScanComplete(message.data);
|
||||
break;
|
||||
case "scan_error":
|
||||
showScanError(message.data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
enableReconnect: true,
|
||||
reconnectDelay: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
function updateScanProgress(data: { progress: number; files_scanned: number; new_items: number }): void {
|
||||
const progressBar = document.getElementById("scan-progress-bar");
|
||||
if (progressBar) {
|
||||
progressBar.style.width = (data.progress * 100) + "%";
|
||||
}
|
||||
|
||||
const progressText = document.getElementById("scan-progress-text");
|
||||
if (progressText) {
|
||||
progressText.textContent = `${data.files_scanned} files scanned (${data.new_items} new)`;
|
||||
}
|
||||
}
|
||||
|
||||
function showScanComplete(_data: unknown): void {
|
||||
console.log("Scan complete:", _data);
|
||||
}
|
||||
|
||||
function showScanError(_data: unknown): void {
|
||||
console.error("Scan error:", _data);
|
||||
}
|
||||
|
||||
async function triggerLibraryScan(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/libraries/scan", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Library scan started", "success");
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.error || "Failed to start scan", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Scan error:", error);
|
||||
showToast("Failed to start library scan", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerQuickScan(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/libraries/quick-scan", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Quick scan started", "success");
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.error || "Failed to start quick scan", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Quick scan error:", error);
|
||||
showToast("Failed to start quick scan", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemStats(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/admin/stats", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const stats = await response.json();
|
||||
renderSystemStats(stats);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load stats:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSystemStats(stats: Record<string, unknown>): void {
|
||||
const container = document.getElementById("system-stats");
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_books || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Total Books</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_users || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Users</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_devices || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Devices</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_libraries || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Libraries</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function scanAllLibraries(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -227,6 +98,8 @@ function showScanProgress(
|
||||
pollScanProgress(jobIds, libraryNames);
|
||||
}
|
||||
|
||||
let scanPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
function pollScanProgress(
|
||||
jobIds: string[],
|
||||
_libraryNames: Record<string, string>,
|
||||
@@ -283,7 +156,7 @@ function pollScanProgress(
|
||||
}
|
||||
|
||||
if (allComplete) {
|
||||
clearInterval(scanPollInterval);
|
||||
clearInterval(scanPollInterval!);
|
||||
showScanResults(
|
||||
jobIds.length,
|
||||
totalFiles,
|
||||
@@ -369,8 +242,6 @@ async function loadWatchStatus(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let scanPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
function stopScanStatusPolling(): void {
|
||||
if (scanPollInterval !== undefined) {
|
||||
clearInterval(scanPollInterval);
|
||||
@@ -380,22 +251,14 @@ function stopScanStatusPolling(): void {
|
||||
|
||||
export {
|
||||
hideScanProgress,
|
||||
initializeScanWebSocket,
|
||||
loadSystemStats,
|
||||
loadWatchStatus,
|
||||
scanAllLibraries,
|
||||
stopScanStatusPolling,
|
||||
triggerLibraryScan,
|
||||
triggerQuickScan,
|
||||
};
|
||||
|
||||
Alpine.data("admin", () => ({
|
||||
hideScanProgress,
|
||||
initializeScanWebSocket,
|
||||
loadSystemStats,
|
||||
loadWatchStatus,
|
||||
scanAllLibraries,
|
||||
stopScanStatusPolling,
|
||||
triggerLibraryScan,
|
||||
triggerQuickScan,
|
||||
}));
|
||||
|
||||
@@ -1,695 +0,0 @@
|
||||
// Library management functionality for admin/library page
|
||||
// Procedural/imperative style t
|
||||
// nt
|
||||
// OOP)
|
||||
|
||||
import { Alpine } from "./alpine";
|
||||
import {
|
||||
apiPut,
|
||||
apiPost,
|
||||
apiGet,
|
||||
handleResponse,
|
||||
handleError,
|
||||
handleVoidResponse,
|
||||
apiDelete,
|
||||
} from "./api";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
library_type_id: string;
|
||||
type_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface LibraryFolder {
|
||||
id: string;
|
||||
library_id: string;
|
||||
folder_path: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface LibrariesResponse {
|
||||
data: Library[];
|
||||
}
|
||||
|
||||
// State (already SSR'd, used for updates)
|
||||
let libraries: Library[] = [];
|
||||
|
||||
// Reload libraries from API (called after create/delete/update)
|
||||
async function reloadLibraries(): Promise<void> {
|
||||
try {
|
||||
const response = await apiGet("/libraries");
|
||||
const result = (await handleResponse(
|
||||
response,
|
||||
)) as unknown as LibrariesResponse;
|
||||
libraries = result.data;
|
||||
renderLibraries();
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load libraries");
|
||||
}
|
||||
}
|
||||
|
||||
// Render libraries list (replaces SSR content after updates)
|
||||
function renderLibraries(): void {
|
||||
const container = document.getElementById("libraries-list");
|
||||
if (!container) return;
|
||||
if (libraries.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p style="color: var(--text-secondary)" class="text-center py-8">No libraries yet. Create your first library to get started.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = libraries
|
||||
.map(
|
||||
(library) =>
|
||||
'<div class="p-4 border rounded-lg" style="background-color: var(--bg-primary); border-color: var(--border)">' +
|
||||
'<div class="flex justify-between items-start mb-2">' +
|
||||
"<div>" +
|
||||
`<h4 class="font-semibold" style="color: var(--text-primary)">${escapeHtmlLocal(library.name)}</h4>` +
|
||||
(library.description
|
||||
? `<p class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(library.description)}</p>`
|
||||
: "") +
|
||||
`<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${escapeHtmlLocal(library.type_name)}</span>` +
|
||||
"</div>" +
|
||||
'<div class="flex space-x-2">' +
|
||||
`<button data-library-id="${library.id}" data-action="show-folders" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button>` +
|
||||
`<button data-library-id="${library.id}" data-action="edit" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button>` +
|
||||
`<button data-library-id="${library.id}" data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>` +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
`<div id="library-folders-${library.id}" class="hidden mt-3 space-y-2"></div>` +
|
||||
"</div>",
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Load user's visible libraries for visibility management
|
||||
async function loadUserVisibility(): Promise<void> {
|
||||
const select = document.getElementById("user-select") as HTMLSelectElement;
|
||||
const userId = select?.value;
|
||||
if (!userId) {
|
||||
const container = document.getElementById("user-libraries");
|
||||
if (container) {
|
||||
container.innerHTML =
|
||||
'<p style="color: var(--text-secondary)">Please select a user</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiGet("/libraries/visible");
|
||||
const visibleLibraries = (await handleResponse(
|
||||
response,
|
||||
)) as unknown as Library[];
|
||||
const container = document.getElementById("user-libraries");
|
||||
if (!container) return;
|
||||
|
||||
const visibleIds = new Set(visibleLibraries.map((lib: Library) => lib.id));
|
||||
|
||||
container.innerHTML = libraries
|
||||
.map((library) => {
|
||||
const isVisible = visibleIds.has(library.id);
|
||||
return (
|
||||
'<label class="flex items-center space-x-3 p-2 rounded" style="background-color: var(--bg-primary);">' +
|
||||
`<input type="checkbox" ${isVisible ? "checked" : ""} ` +
|
||||
`data-user-id="${userId}" data-library-id="${library.id}" ` +
|
||||
`onchange="setLibraryVisibility('${userId}', '${library.id}', this.checked)" ` +
|
||||
'class="w-4 h-4">' +
|
||||
`<span style="color: var(--text-primary)">${escapeHtmlLocal(library.name)} (${escapeHtmlLocal(library.type_name)})</span>` +
|
||||
"</label>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load user libraries");
|
||||
}
|
||||
}
|
||||
|
||||
// Set library visibility for a user
|
||||
async function setLibraryVisibility(
|
||||
userId: string,
|
||||
libraryId: string,
|
||||
isVisible: boolean,
|
||||
): Promise<void> {
|
||||
// userId is used in the HTML onchange handler but the API gets user from JWT context
|
||||
console.debug(
|
||||
"Setting visibility for user:",
|
||||
userId,
|
||||
"library:",
|
||||
libraryId,
|
||||
"visible:",
|
||||
isVisible,
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await apiPost("/libraries/visibility", {
|
||||
library_id: libraryId,
|
||||
is_visible: isVisible,
|
||||
});
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Library visibility updated", "success");
|
||||
|
||||
// Refresh visibility controls
|
||||
void loadUserVisibility();
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to update library visibility");
|
||||
}
|
||||
}
|
||||
|
||||
// Create library form handler
|
||||
async function handleCreateLibrarySubmit(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const libraryId = (document.getElementById("library-id") as HTMLInputElement)
|
||||
?.value;
|
||||
const isEdit = !!libraryId;
|
||||
|
||||
const libraryData = {
|
||||
name: formData.get("name") as string,
|
||||
description: formData.get("description") as string,
|
||||
type: formData.get("type") as string,
|
||||
};
|
||||
|
||||
try {
|
||||
const url = isEdit ? `/libraries/${libraryId}` : "/libraries";
|
||||
const response = isEdit
|
||||
? await apiPut(url, libraryData)
|
||||
: await apiPost(url, libraryData);
|
||||
|
||||
if (isEdit) {
|
||||
await handleVoidResponse(response);
|
||||
} else {
|
||||
(await handleResponse(response)) as unknown as { data: Library };
|
||||
}
|
||||
|
||||
showToast(
|
||||
isEdit ? "Library updated successfully" : "Library created successfully",
|
||||
"success",
|
||||
);
|
||||
|
||||
hideCreateLibraryModal();
|
||||
form.reset();
|
||||
|
||||
const libraryIdInput = document.getElementById(
|
||||
"library-id",
|
||||
) as HTMLInputElement;
|
||||
if (libraryIdInput) {
|
||||
libraryIdInput.value = "";
|
||||
}
|
||||
|
||||
void reloadLibraries();
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
isEdit ? "Failed to update library" : "Failed to create library",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete library
|
||||
async function deleteLibrary(libraryId: string): Promise<void> {
|
||||
const library = libraries.find((l) => l.id === libraryId);
|
||||
if (!library) return;
|
||||
|
||||
showDeleteModal(library);
|
||||
}
|
||||
|
||||
// Show library folders
|
||||
async function showLibraryFolders(libraryId: string): Promise<void> {
|
||||
const container = document.getElementById(`library-folders-${libraryId}`);
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const response = await apiGet(`/libraries/${libraryId}/folders`);
|
||||
const folders = (await handleResponse(
|
||||
response,
|
||||
)) as unknown as LibraryFolder[];
|
||||
|
||||
container.innerHTML = folders
|
||||
.map(
|
||||
(folder: LibraryFolder) =>
|
||||
'<div class="flex justify-between items-center p-2 rounded" style="background-color: var(--bg-secondary); border-color: var(--border)">' +
|
||||
`<span class="text-sm" style="color: var(--text-primary)">${escapeHtmlLocal(folder.folder_path)}</span>` +
|
||||
`<button data-library-id="${libraryId}" data-folder-path="${escapeHtmlLocal(folder.folder_path)}" data-action="remove-folder" ` +
|
||||
'class="text-xs text-red-500">Remove</button>' +
|
||||
"</div>",
|
||||
)
|
||||
.join("");
|
||||
|
||||
container.innerHTML +=
|
||||
'<div class="mt-2 flex space-x-2">' +
|
||||
`<input type="text" id="folder-path-${libraryId}" placeholder="Add folder path" ` +
|
||||
'class="flex-1 px-2 py-1 text-sm border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">' +
|
||||
`<button data-action="browse-folder" data-input-id="folder-path-${libraryId}" ` +
|
||||
'class="btn-secondary px-2 py-1 text-xs rounded">Browse</button>' +
|
||||
`<button data-library-id="${libraryId}" data-action="add-folder" ` +
|
||||
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
|
||||
"</div>";
|
||||
|
||||
container.classList.remove("hidden");
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load folders");
|
||||
}
|
||||
}
|
||||
|
||||
// Add library folder
|
||||
async function addLibraryFolder(libraryId: string): Promise<void> {
|
||||
const input = document.getElementById(
|
||||
`folder-path-${libraryId}`,
|
||||
) as HTMLInputElement;
|
||||
const folderPath = input?.value.trim();
|
||||
|
||||
if (!folderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiPost(`/libraries/${libraryId}/folders`, {
|
||||
folder_path: folderPath,
|
||||
});
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Folder added successfully", "success");
|
||||
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
void showLibraryFolders(libraryId); // Refresh
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to add folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove library folder
|
||||
async function removeLibraryFolder(
|
||||
libraryId: string,
|
||||
folderPath: string,
|
||||
): Promise<void> {
|
||||
if (!confirm(`Remove folder "${folderPath}" from the library?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiDelete(`/libraries/${libraryId}/folders`, {
|
||||
folder_path: folderPath,
|
||||
});
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Folder removed successfully", "success");
|
||||
|
||||
void showLibraryFolders(libraryId); // Refresh
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to remove folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Edit library (placeholder - opens modal or navigates to edit page)
|
||||
function editLibrary(libraryId: string): void {
|
||||
const library = libraries.find((l) => l.id === libraryId);
|
||||
if (!library) {
|
||||
showToast("Library not found", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById(
|
||||
"create-library-form",
|
||||
) as HTMLFormElement;
|
||||
if (form) {
|
||||
const nameInput = form.querySelector('[name="name"]') as HTMLInputElement;
|
||||
const descInput = form.querySelector(
|
||||
'[name="description"]',
|
||||
) as HTMLTextAreaElement;
|
||||
const typeInput = form.querySelector('[name="type"]') as HTMLSelectElement;
|
||||
|
||||
if (nameInput) nameInput.value = library.name;
|
||||
if (descInput) descInput.value = library.description || "";
|
||||
if (typeInput) typeInput.value = library.type_name;
|
||||
}
|
||||
|
||||
const modalTitle = document.querySelector("#create-library-modal h2");
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = "Edit Library";
|
||||
}
|
||||
|
||||
const libraryIdInput = document.getElementById(
|
||||
"library-id",
|
||||
) as HTMLInputElement;
|
||||
if (libraryIdInput) {
|
||||
libraryIdInput.value = libraryId;
|
||||
}
|
||||
|
||||
showCreateLibraryModal();
|
||||
}
|
||||
|
||||
// Modal controls
|
||||
function showCreateLibraryModal(): void {
|
||||
const modal = document.getElementById("create-library-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
const modalTitle = document.querySelector("#create-library-modal h2");
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = "Create Library";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideCreateLibraryModal(): void {
|
||||
const modal = document.getElementById("create-library-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete modal state
|
||||
let libraryToDelete: Library | null = null;
|
||||
|
||||
function showDeleteModal(library: Library): void {
|
||||
libraryToDelete = library;
|
||||
|
||||
const modal = document.getElementById("delete-library-modal") as HTMLElement;
|
||||
const content = document.getElementById(
|
||||
"delete-modal-content",
|
||||
) as HTMLElement;
|
||||
|
||||
if (modal && content) {
|
||||
const message = `Are you sure you want to delete "<strong>${escapeHtmlLocal(library.name)}</strong>"?
|
||||
|
||||
This will remove:
|
||||
• Library metadata from the database
|
||||
• All folder references
|
||||
• All book records from the database
|
||||
|
||||
⚠️ Book files on disk will NOT be deleted.
|
||||
|
||||
This action cannot be undone.`;
|
||||
|
||||
content.innerHTML = message.replace(/\n/g, "<br>");
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function hideDeleteModal(): void {
|
||||
const modal = document.getElementById("delete-library-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
libraryToDelete = null;
|
||||
}
|
||||
|
||||
async function confirmDeleteLibrary(): Promise<void> {
|
||||
if (!libraryToDelete) return;
|
||||
|
||||
const libraryId = libraryToDelete.id;
|
||||
hideDeleteModal();
|
||||
|
||||
try {
|
||||
const response = await apiDelete(`/libraries/${libraryId}`);
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Library deleted successfully", "success");
|
||||
|
||||
await reloadLibraries();
|
||||
|
||||
const libraryIdInput = document.getElementById(
|
||||
"library-id",
|
||||
) as HTMLInputElement | null;
|
||||
if (libraryIdInput) {
|
||||
libraryIdInput.value = "";
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to delete library");
|
||||
}
|
||||
}
|
||||
|
||||
// Local escape HTML helper
|
||||
function escapeHtmlLocal(text: string): string {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Event delegation for handling dynamic button clicks
|
||||
function handleLibraryListClick(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest("button") as HTMLElement;
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const libraryId = button.dataset.libraryId;
|
||||
|
||||
switch (action) {
|
||||
case "show-folders":
|
||||
if (libraryId) showLibraryFolders(libraryId);
|
||||
break;
|
||||
case "delete":
|
||||
if (libraryId) deleteLibrary(libraryId);
|
||||
break;
|
||||
case "edit":
|
||||
if (libraryId) editLibrary(libraryId);
|
||||
break;
|
||||
case "add-folder":
|
||||
if (libraryId) addLibraryFolder(libraryId);
|
||||
break;
|
||||
case "remove-folder":
|
||||
if (libraryId && button.dataset.folderPath) {
|
||||
removeLibraryFolder(libraryId, button.dataset.folderPath);
|
||||
}
|
||||
break;
|
||||
case "browse-folder":
|
||||
if (button.dataset.inputId) showFolderBrowser(button.dataset.inputId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFolderBrowserClick(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest("button") as HTMLElement;
|
||||
const div = target.closest("div[data-action]") as HTMLElement;
|
||||
|
||||
if (button) {
|
||||
const action = button.dataset.action;
|
||||
const path = button.dataset.path;
|
||||
|
||||
switch (action) {
|
||||
case "browse-parent":
|
||||
if (path) navigateFolderBrowser(path);
|
||||
break;
|
||||
case "browse-cancel":
|
||||
hideFolderBrowser();
|
||||
break;
|
||||
case "browse-select":
|
||||
if (path) selectBrowseFolder(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (div && div.dataset.action === "browse-navigate") {
|
||||
const path = div.dataset.path;
|
||||
if (path) navigateFolderBrowser(path);
|
||||
}
|
||||
}
|
||||
|
||||
function handleGlobalClick(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest("button") as HTMLElement;
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
|
||||
switch (action) {
|
||||
case "show-create-modal":
|
||||
showCreateLibraryModal();
|
||||
break;
|
||||
case "hide-create-modal":
|
||||
hideCreateLibraryModal();
|
||||
break;
|
||||
case "hide-delete-modal":
|
||||
hideDeleteModal();
|
||||
break;
|
||||
case "confirm-delete":
|
||||
void confirmDeleteLibrary();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Folder browser state
|
||||
let currentBrowsePath = "";
|
||||
let currentBrowseInputId = "";
|
||||
|
||||
// Show folder browser modal
|
||||
function showFolderBrowser(inputId: string): void {
|
||||
currentBrowseInputId = inputId;
|
||||
currentBrowsePath = "/";
|
||||
|
||||
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.remove("hidden");
|
||||
void loadBrowseDirectories(currentBrowsePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Load directories for browsing
|
||||
async function loadBrowseDirectories(path: string): Promise<void> {
|
||||
try {
|
||||
const response = await apiGet(
|
||||
`/libraries/browse?path=${encodeURIComponent(path)}`,
|
||||
);
|
||||
const data = (await handleResponse(response)) as unknown as {
|
||||
current_path: string;
|
||||
parent_path: string;
|
||||
directories: string[];
|
||||
};
|
||||
|
||||
currentBrowsePath = data.current_path;
|
||||
renderBrowseDirectories(data);
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load directories");
|
||||
}
|
||||
}
|
||||
|
||||
// Render browse directories (uses event delegation via data-action attributes)
|
||||
function renderBrowseDirectories(data: {
|
||||
current_path: string;
|
||||
parent_path: string;
|
||||
directories: string[];
|
||||
}): void {
|
||||
const container = document.getElementById("folder-browser-content");
|
||||
if (!container) return;
|
||||
|
||||
let html = `
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
${
|
||||
data.parent_path
|
||||
? `<button type="button" data-action="browse-parent" data-path="${escapeHtmlLocal(data.parent_path)}" class="btn-secondary px-3 py-1 rounded">↑ Parent</button>`
|
||||
: ""
|
||||
}
|
||||
<span class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(data.current_path)}</span>
|
||||
</div>
|
||||
<div class="max-h-64 overflow-y-auto space-y-1">
|
||||
`;
|
||||
|
||||
if (data.directories.length === 0) {
|
||||
html +=
|
||||
'<p style="color: var(--text-secondary)" class="text-center py-4">No subdirectories</p>';
|
||||
} else {
|
||||
data.directories.forEach((dir) => {
|
||||
const fullPath =
|
||||
data.current_path === "/" ? `/${dir}` : `${data.current_path}/${dir}`;
|
||||
html += `
|
||||
<div class="p-2 rounded cursor-pointer hover:opacity-80"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary)"
|
||||
data-action="browse-navigate"
|
||||
data-path="${escapeHtmlLocal(fullPath)}">
|
||||
📁 ${escapeHtmlLocal(dir)}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" data-action="browse-cancel" class="btn-secondary px-4 py-2 rounded">Cancel</button>
|
||||
<button type="button" data-action="browse-select" data-path="${escapeHtmlLocal(data.current_path)}" class="btn-primary px-4 py-2 rounded">Select This Folder</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Navigate to subdirectory
|
||||
function navigateFolderBrowser(path: string): void {
|
||||
void loadBrowseDirectories(path);
|
||||
}
|
||||
|
||||
// Select folder and close browser
|
||||
function selectBrowseFolder(path: string): void {
|
||||
const input = document.getElementById(
|
||||
currentBrowseInputId,
|
||||
) as HTMLInputElement;
|
||||
if (input) {
|
||||
input.value = path;
|
||||
}
|
||||
hideFolderBrowser();
|
||||
}
|
||||
|
||||
// Hide folder browser modal
|
||||
function hideFolderBrowser(): void {
|
||||
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize page
|
||||
function initializeLibraryAdmin(): void {
|
||||
// Setup event listeners
|
||||
const librariesList = document.getElementById("libraries-list");
|
||||
if (librariesList) {
|
||||
librariesList.addEventListener("click", handleLibraryListClick);
|
||||
}
|
||||
|
||||
const folderBrowserModal = document.getElementById("folder-browser-modal");
|
||||
if (folderBrowserModal) {
|
||||
folderBrowserModal.addEventListener("click", handleFolderBrowserClick);
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleGlobalClick);
|
||||
|
||||
// Setup form submission
|
||||
const createLibraryForm = document.getElementById("create-library-form");
|
||||
if (createLibraryForm) {
|
||||
createLibraryForm.addEventListener("submit", handleCreateLibrarySubmit);
|
||||
}
|
||||
|
||||
// SSR provides initial library list - no need to fetch on page load
|
||||
// reloadLibraries() is called after create/delete/update operations
|
||||
}
|
||||
|
||||
// Export functions for global access
|
||||
export {
|
||||
addLibraryFolder,
|
||||
confirmDeleteLibrary,
|
||||
deleteLibrary,
|
||||
editLibrary,
|
||||
handleCreateLibrarySubmit,
|
||||
hideDeleteModal,
|
||||
hideFolderBrowser,
|
||||
initializeLibraryAdmin,
|
||||
loadUserVisibility,
|
||||
navigateFolderBrowser,
|
||||
removeLibraryFolder,
|
||||
selectBrowseFolder,
|
||||
setLibraryVisibility,
|
||||
showDeleteModal,
|
||||
showFolderBrowser,
|
||||
showLibraryFolders,
|
||||
};
|
||||
|
||||
Alpine.data("library", () => ({
|
||||
addLibraryFolder,
|
||||
confirmDeleteLibrary,
|
||||
deleteLibrary,
|
||||
editLibrary,
|
||||
handleCreateLibrarySubmit,
|
||||
hideDeleteModal,
|
||||
hideFolderBrowser,
|
||||
initializeLibraryAdmin,
|
||||
loadUserVisibility,
|
||||
navigateFolderBrowser,
|
||||
removeLibraryFolder,
|
||||
selectBrowseFolder,
|
||||
setLibraryVisibility,
|
||||
showDeleteModal,
|
||||
showFolderBrowser,
|
||||
showLibraryFolders,
|
||||
}));
|
||||
@@ -20,7 +20,6 @@ import "./dom";
|
||||
import "./events";
|
||||
import "./header";
|
||||
import "./index";
|
||||
import "./library";
|
||||
import "./library-switcher";
|
||||
import "./linking";
|
||||
import "./login";
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user