import { Alpine } from "./alpine"; import { showToast } from "./toast"; import { getToken } from "./storage"; function getDeviceIcon(typeName: string): string { const deviceIcons: Record = { koreader: "📖", kobo: "📚", web: "🌐", mobile: "📱", }; return deviceIcons[typeName] || "📱"; } function showAddDeviceModal(): void { const modal = document.getElementById("add-device-modal"); if (modal) modal.classList.remove("hidden"); } function hideAddDeviceModal(): void { const modal = document.getElementById("add-device-modal"); const form = document.getElementById("add-device-form"); if (modal) modal.classList.add("hidden"); if (form && form instanceof HTMLFormElement) form.reset(); } async function handleAddDevice(event: Event): Promise { event.preventDefault(); const token = getToken(); if (!token) return; const deviceNameInput = document.getElementById( "device-name", ) as HTMLInputElement; const deviceTypeInput = document.getElementById( "device-type", ) as HTMLSelectElement; const deviceIdentifierInput = document.getElementById( "device-identifier", ) as HTMLInputElement; if (!deviceNameInput || !deviceTypeInput || !deviceIdentifierInput) return; const data = { device_name: deviceNameInput.value, device_type: deviceTypeInput.value, device_identifier: deviceIdentifierInput.value, }; try { const response = await fetch("/api/devices/register", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(data), }); if (response.ok) { showToast( "Device registered! Check your device for sync instructions.", "success", ); hideAddDeviceModal(); window.location.reload(); } else { showToast("Failed to register device", "error"); } } catch (error) { console.error("Failed to register device", error); showToast("Failed to register device", "error"); } } function showDeviceSettings(deviceId: string): void { const modal = document.getElementById("device-settings-modal"); const deviceIdInput = document.getElementById( "settings-device-id", ) as HTMLInputElement; const deviceTypeInput = document.getElementById( "settings-device-type", ) as HTMLInputElement; const deviceNameInput = document.getElementById( "settings-device-name", ) as HTMLInputElement; const syncEnabledInput = document.getElementById( "settings-sync-enabled", ) as HTMLInputElement; const autoSyncInput = document.getElementById( "settings-auto-sync", ) as HTMLInputElement; const syncFrequencyInput = document.getElementById( "settings-sync-frequency", ) as HTMLInputElement; if (!modal || !deviceIdInput) return; deviceIdInput.value = deviceId; const token = getToken(); if (!token) return; let deviceType: any; fetch(`/api/devices/${deviceId}`, { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => res.json()) .then((device) => { if (deviceTypeInput) deviceTypeInput.value = device.device_type || ""; if (deviceNameInput) deviceNameInput.value = device.device_name || ""; if (syncEnabledInput) syncEnabledInput.checked = device.sync_enabled || false; if (autoSyncInput) autoSyncInput.checked = device.auto_sync || false; if (syncFrequencyInput) syncFrequencyInput.value = String(device.sync_frequency_minutes || 60); deviceType = device.device_type; return fetch("/api/collections", { headers: { Authorization: `Bearer ${token}` }, }); }) .then((res) => res.json()) .then((result) => { if (result.collections && result.collections.length > 0) { const firstCollection = result.collections[0]; const viewSettings = firstCollection.view_settings || {}; const deviceSettings = viewSettings[deviceType] || {}; const viewModeInput = document.getElementById( "settings-view-mode", ) as HTMLInputElement; const sortOrderInput = document.getElementById( "settings-sort-order", ) as HTMLInputElement; const itemsPerPageInput = document.getElementById( "settings-items-per-page", ) as HTMLInputElement; const showCoversInput = document.getElementById( "settings-show-covers", ) as HTMLInputElement; const showProgressInput = document.getElementById( "settings-show-progress", ) as HTMLInputElement; if (viewModeInput) viewModeInput.value = deviceSettings.view_mode || "grid"; if (sortOrderInput) sortOrderInput.value = deviceSettings.sort_order || "name"; if (itemsPerPageInput) itemsPerPageInput.value = String(deviceSettings.items_per_page || 24); if (showCoversInput) showCoversInput.checked = deviceSettings.show_covers !== false; if (showProgressInput) showProgressInput.checked = deviceSettings.show_progress || false; } }) .catch((error) => { console.error("Failed to load view settings:", error); }); if (modal) modal.classList.remove("hidden"); } function showShelfMappings(deviceId: string): void { const mappingsDeviceIdInput = document.getElementById( "mappings-device-id", ) as HTMLInputElement; const modal = document.getElementById("shelf-mappings-modal"); if (mappingsDeviceIdInput) mappingsDeviceIdInput.value = deviceId; loadShelfMappings(deviceId); if (modal) modal.classList.remove("hidden"); } function hideShelfMappingsModal(): void { const modal = document.getElementById("shelf-mappings-modal"); if (modal) modal.classList.add("hidden"); } async function loadShelfMappings(deviceId: string): Promise { const container = document.getElementById("shelf-mappings-container"); const token = getToken(); if (!container || !token) return; try { const response = await fetch(`/api/devices/${deviceId}/collections`, { headers: { Authorization: `Bearer ${token}` }, }); const result = await response.json(); if (result.mappings && result.mappings.length > 0) { container.innerHTML = result.mappings .map( (mapping: any) => `

${mapping.collection_name}

→ Device Shelf: ${mapping.device_shelf_name}

Sync: ${mapping.sync_direction}

`, ) .join(""); } else { container.innerHTML = '

No shelf mappings configured. Click "Add Mapping" to create one.

'; } } catch (error) { container.innerHTML = '

Failed to load mappings

'; } } function showAddMappingModal(): void { const mappingsDeviceIdInput = document.getElementById( "mappings-device-id", ) as HTMLInputElement; const mappingIdInput = document.getElementById( "mapping-id", ) as HTMLInputElement; const mappingCollectionInput = document.getElementById( "mapping-collection", ) as HTMLSelectElement; const mappingShelfNameInput = document.getElementById( "mapping-shelf-name", ) as HTMLInputElement; const mappingSyncDirectionInput = document.getElementById( "mapping-sync-direction", ) as HTMLSelectElement; const modal = document.getElementById("add-mapping-modal"); if (mappingsDeviceIdInput) { const mappingDeviceIdInput = document.getElementById( "mapping-device-id", ) as HTMLInputElement; if (mappingDeviceIdInput) mappingDeviceIdInput.value = mappingsDeviceIdInput.value; } if (mappingIdInput) mappingIdInput.value = ""; if (mappingCollectionInput) mappingCollectionInput.value = ""; if (mappingShelfNameInput) mappingShelfNameInput.value = ""; if (mappingSyncDirectionInput) mappingSyncDirectionInput.value = "bidirectional"; loadCollections(); if (modal) modal.classList.remove("hidden"); } function hideAddMappingModal(): void { const modal = document.getElementById("add-mapping-modal"); const form = document.getElementById("mapping-form"); if (modal) modal.classList.add("hidden"); if (form && form instanceof HTMLFormElement) form.reset(); } async function loadCollections(): Promise { const select = document.getElementById( "mapping-collection", ) as HTMLSelectElement; const token = getToken(); if (!select || !token) return; try { const response = await fetch("/api/collections", { headers: { Authorization: `Bearer ${token}` }, }); const result = await response.json(); if (result.collections) { select.innerHTML = '' + result.collections .map((col: any) => ``) .join(""); } } catch (error) { console.error("Failed to load collections", error); } } function editMapping( mappingId: string, collectionId: string, shelfName: string, syncDirection: string, ): void { const mappingIdInput = document.getElementById( "mapping-id", ) as HTMLInputElement; const mappingCollectionInput = document.getElementById( "mapping-collection", ) as HTMLSelectElement; const mappingShelfNameInput = document.getElementById( "mapping-shelf-name", ) as HTMLInputElement; const mappingSyncDirectionInput = document.getElementById( "mapping-sync-direction", ) as HTMLSelectElement; const modal = document.getElementById("add-mapping-modal"); if (mappingIdInput) mappingIdInput.value = mappingId; if (mappingCollectionInput) mappingCollectionInput.value = collectionId; if (mappingShelfNameInput) mappingShelfNameInput.value = shelfName; if (mappingSyncDirectionInput) mappingSyncDirectionInput.value = syncDirection; if (modal) modal.classList.remove("hidden"); } async function handleSaveMapping(event: Event): Promise { event.preventDefault(); const token = getToken(); const mappingsDeviceIdInput = document.getElementById( "mapping-device-id", ) as HTMLInputElement; const mappingIdInput = document.getElementById( "mapping-id", ) as HTMLInputElement; const mappingCollectionInput = document.getElementById( "mapping-collection", ) as HTMLSelectElement; const mappingShelfNameInput = document.getElementById( "mapping-shelf-name", ) as HTMLInputElement; const mappingSyncDirectionInput = document.getElementById( "mapping-sync-direction", ) as HTMLSelectElement; if ( !token || !mappingsDeviceIdInput || !mappingCollectionInput || !mappingShelfNameInput || !mappingSyncDirectionInput ) return; const deviceId = mappingsDeviceIdInput.value; const mappingId = mappingIdInput.value; const isUpdate = mappingId !== ""; const data = { collection_id: mappingCollectionInput.value, device_shelf_name: mappingShelfNameInput.value, sync_direction: mappingSyncDirectionInput.value, }; const url = isUpdate ? `/api/devices/${deviceId}/collections/${mappingId}` : `/api/devices/${deviceId}/collections`; const method = isUpdate ? "PUT" : "POST"; try { const response = await fetch(url, { method: method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(data), }); if (response.ok) { showToast(isUpdate ? "Mapping updated" : "Mapping created", "success"); hideAddMappingModal(); loadShelfMappings(deviceId); } else { showToast("Failed to save mapping", "error"); } } catch (error) { console.error("Failed to save mapping", error); showToast("Failed to save mapping", "error"); } } async function deleteMapping(mappingId: string): Promise { if (!confirm("Are you sure you want to delete this mapping?")) return; const token = getToken(); const mappingsDeviceIdInput = document.getElementById( "mappings-device-id", ) as HTMLInputElement; if (!token || !mappingsDeviceIdInput) return; const deviceId = mappingsDeviceIdInput.value; try { const response = await fetch( `/api/devices/${deviceId}/collections/${mappingId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }, ); if (response.ok) { showToast("Mapping deleted", "success"); loadShelfMappings(deviceId); } else { showToast("Failed to delete mapping", "error"); } } catch (error) { console.error("Failed to delete mapping", error); showToast("Failed to delete mapping", "error"); } } function hideDeviceSettingsModal(): void { const modal = document.getElementById("device-settings-modal"); if (modal) modal.classList.add("hidden"); } async function handleSaveDeviceSettings(event: Event): Promise { event.preventDefault(); const token = getToken(); const settingsDeviceIdInput = document.getElementById( "settings-device-id", ) as HTMLInputElement; const settingsDeviceTypeInput = document.getElementById( "settings-device-type", ) as HTMLInputElement; const settingsDeviceNameInput = document.getElementById( "settings-device-name", ) as HTMLInputElement; const settingsSyncEnabledInput = document.getElementById( "settings-sync-enabled", ) as HTMLInputElement; const settingsAutoSyncInput = document.getElementById( "settings-auto-sync", ) as HTMLInputElement; const settingsSyncFrequencyInput = document.getElementById( "settings-sync-frequency", ) as HTMLInputElement; if (!token || !settingsDeviceIdInput || !settingsDeviceNameInput) return; const deviceId = settingsDeviceIdInput.value; const deviceType = settingsDeviceTypeInput?.value || ""; const deviceData = { device_name: settingsDeviceNameInput.value, sync_enabled: settingsSyncEnabledInput?.checked || false, auto_sync: settingsAutoSyncInput?.checked || false, sync_frequency_minutes: parseInt( settingsSyncFrequencyInput?.value || "60", 10, ), }; const viewModeInput = document.getElementById( "settings-view-mode", ) as HTMLSelectElement; const sortOrderInput = document.getElementById( "settings-sort-order", ) as HTMLSelectElement; const itemsPerPageInput = document.getElementById( "settings-items-per-page", ) as HTMLInputElement; const showCoversInput = document.getElementById( "settings-show-covers", ) as HTMLInputElement; const showProgressInput = document.getElementById( "settings-show-progress", ) as HTMLInputElement; const viewSettings = { view_mode: viewModeInput?.value || "grid", sort_order: sortOrderInput?.value || "name", items_per_page: parseInt(itemsPerPageInput?.value || "24", 10), show_covers: showCoversInput?.checked !== false, show_progress: showProgressInput?.checked || false, }; try { await fetch(`/api/devices/${deviceId}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(deviceData), }); const collectionsRes = await fetch("/api/collections", { method: "GET", headers: { Authorization: `Bearer ${token}` }, }); const collections = await collectionsRes.json(); const updatePromises = collections.collections.map((collection: any) => { const currentSettings = collection.view_settings || {}; currentSettings[deviceType] = viewSettings; return fetch(`/api/collections/${collection.id}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ ...collection, view_settings: currentSettings, }), }); }); await Promise.all(updatePromises); showToast("Device settings saved", "success"); hideDeviceSettingsModal(); window.location.reload(); } catch (error) { console.error("Failed to save settings", error); showToast("Failed to save settings", "error"); } } async function copyToClipboard( text: string, description: string, ): Promise { try { await navigator.clipboard.writeText(text); showToast(`${description} copied!`, "success"); } catch (error) { console.error("Failed to copy to clipboard", error); showToast("Failed to copy to clipboard", "error"); } } async function handleRevokeDevice(): Promise { const token = getToken(); const settingsDeviceIdInput = document.getElementById( "settings-device-id", ) as HTMLInputElement; if (!token || !settingsDeviceIdInput) return; if ( !confirm( "Are you sure you want to revoke this device? It will no longer be able to sync.", ) ) return; const deviceId = settingsDeviceIdInput.value; try { const response = await fetch(`/api/devices/${deviceId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { showToast("Device revoked successfully", "success"); hideDeviceSettingsModal(); window.location.reload(); } else { showToast("Failed to revoke device", "error"); } } catch (error) { console.error("Failed to revoke device", error); showToast("Failed to revoke device", "error"); } } async function approveDevice(registrationId: string): Promise { const token = getToken(); if (!token) return; try { const response = await fetch(`/api/devices/approve/${registrationId}`, { method: "GET", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { showToast("Device approved successfully", "success"); window.location.reload(); } else { showToast("Failed to approve device", "error"); } } catch (error) { console.error("Failed to approve device", error); showToast("Failed to approve device", "error"); } } async function rejectDevice(registrationId: string): Promise { const token = getToken(); if (!token) return; try { const response = await fetch(`/api/devices/reject/${registrationId}`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { showToast("Device registration rejected", "info"); window.location.reload(); } else { showToast("Failed to reject device", "error"); } } catch (error) { console.error("Failed to reject device", error); showToast("Failed to reject device", "error"); } } async function clearSyncQueue(): Promise { if (!confirm("Are you sure you want to clear all sync queue items?")) return; const token = getToken(); if (!token) return; try { const response = await fetch("/api/queue/clear", { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { showToast("Sync queue cleared", "success"); } else { showToast("Failed to clear queue", "error"); } } catch (error) { console.error("Failed to clear queue", error); showToast("Failed to clear queue", "error"); } } function setupEventDelegation(): void { document.addEventListener("click", (e) => { const target = e.target as HTMLElement; const button = target.closest("button") as HTMLButtonElement; if (!button) return; const action = button.dataset.action; if (action === "edit-mapping") { editMapping( button.dataset.mappingId || "", button.dataset.collectionId || "", button.dataset.shelfName || "", button.dataset.syncDirection || "", ); } else if (action === "delete-mapping") { deleteMapping(button.dataset.mappingId || ""); } }); } export { approveDevice, clearSyncQueue, copyToClipboard, deleteMapping, editMapping, getDeviceIcon, handleAddDevice, handleRevokeDevice, handleSaveDeviceSettings, handleSaveMapping, hideAddDeviceModal, hideAddMappingModal, hideDeviceSettingsModal, hideShelfMappingsModal, loadCollections, loadShelfMappings, rejectDevice, setupEventDelegation, showAddDeviceModal, showAddMappingModal, showDeviceSettings, showShelfMappings, }; Alpine.data("devices", () => ({ approveDevice, clearSyncQueue, copyToClipboard, deleteMapping, editMapping, getDeviceIcon, handleAddDevice, handleRevokeDevice, handleSaveDeviceSettings, handleSaveMapping, hideAddDeviceModal, hideAddMappingModal, hideDeviceSettingsModal, hideShelfMappingsModal, loadCollections, loadShelfMappings, rejectDevice, setupEventDelegation, showAddDeviceModal, showAddMappingModal, showDeviceSettings, showShelfMappings, }));