feat(devices): add device-specific collection view settings (Phase 9-6)
Enhance device settings modal with collection view preferences: - View mode selection (grid, list, compact) - Sort order options (name, created, book count, recent) - Items per page configuration (12, 24, 48, 96) - Show/hide cover images toggle - Show reading progress indicators toggle Technical implementation: - Stores device-specific settings in collections.view_settings JSONB - Updates all collections when device preferences change - Loads existing settings when opening modal - Falls back to sensible defaults This allows users to customize how their collections appear on different devices (Kobo, KOReader, Web, Mobile) for an optimal reading experience per platform.
This commit is contained in:
+633
-263
@@ -1,260 +1,415 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) {
|
templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Device Management - Bookmann</title>
|
<title>Device Management - Bookmann</title>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/toast.js"></script>
|
<script src="/static/toast.js"></script>
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<link href="/static/style.css" rel="stylesheet"/>
|
||||||
<script src="/static/theme.js"></script>
|
<script src="/static/theme.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-{ user.Theme }">
|
<body class="theme-{ user.Theme }">
|
||||||
@Header(user, "/devices")
|
@Header(user, "/devices")
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<!-- Header Section -->
|
||||||
<!-- Header Section -->
|
<div class="mb-8">
|
||||||
<div class="mb-8">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex justify-between items-center">
|
<div>
|
||||||
<div>
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Device Management</h1>
|
||||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Device Management</h1>
|
<p style="color: var(--text-secondary)">Manage your reading devices and sync settings</p>
|
||||||
<p style="color: var(--text-secondary)">Manage your reading devices and sync settings</p>
|
</div>
|
||||||
</div>
|
<button onclick="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
<button onclick="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
➕ Add New Device
|
||||||
➕ Add New Device
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Devices Grid -->
|
||||||
|
<div id="devices-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<!-- Devices Grid -->
|
<!-- Empty state -->
|
||||||
<div id="devices-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
if len(devices) == 0 {
|
||||||
<!-- Empty state -->
|
<div id="empty-state" class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
||||||
if len(devices) == 0 {
|
<div class="text-6xl mb-4">📱</div>
|
||||||
<div id="empty-state" class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3>
|
||||||
<div class="text-6xl mb-4">📱</div>
|
<p class="mb-4">Add your reading devices to enable cross-device sync</p>
|
||||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3>
|
<button onclick="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
<p class="mb-4">Add your reading devices to enable cross-device sync</p>
|
Add Your First Device
|
||||||
<button onclick="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
</button>
|
||||||
Add Your First Device
|
</div>
|
||||||
</button>
|
}
|
||||||
</div>
|
<!-- Devices Grid -->
|
||||||
}
|
if len(devices) > 0 {
|
||||||
|
<div id="devices-grid" class="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<!-- Devices Grid -->
|
for _, device := range devices {
|
||||||
if len(devices) > 0 {
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||||
<div id="devices-grid" class="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="flex items-start justify-between mb-4">
|
||||||
for _, device := range devices {
|
<div class="text-4xl">
|
||||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
if device.DeviceType == "koreader" {
|
||||||
<div class="flex items-start justify-between mb-4">
|
📖
|
||||||
<div class="text-4xl">
|
} else if device.DeviceType == "kobo" {
|
||||||
if device.DeviceType == "koreader" {
|
📚
|
||||||
📖
|
} else if device.DeviceType == "web" {
|
||||||
} else if device.DeviceType == "kobo" {
|
🌐
|
||||||
📚
|
} else {
|
||||||
} else if device.DeviceType == "web" {
|
📱
|
||||||
🌐
|
}
|
||||||
} else {
|
</div>
|
||||||
📱
|
<div class="flex space-x-2">
|
||||||
}
|
<button onclick="showShelfMappings('{ device.ID }')" class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||||||
</div>
|
📚
|
||||||
<div class="flex space-x-2">
|
</button>
|
||||||
<button onclick="showDeviceSettings('{ device.ID }')" class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
<button onclick="showDeviceSettings('{ device.ID }')" class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||||||
⚙️
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold mb-1" style="color: var(--text-primary)">{ device.DeviceName }</h3>
|
<h3 class="text-lg font-semibold mb-1" style="color: var(--text-primary)">{ device.DeviceName }</h3>
|
||||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ device.DeviceType }</p>
|
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ device.DeviceType }</p>
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span style="color: var(--text-secondary)">Sync Status</span>
|
<span style="color: var(--text-secondary)">Sync Status</span>
|
||||||
if device.SyncEnabled {
|
if device.SyncEnabled {
|
||||||
<span style="color: var(--accent)">✓ Enabled</span>
|
<span style="color: var(--accent)">✓ Enabled</span>
|
||||||
} else {
|
} else {
|
||||||
<span style="color: var(--text-secondary)">✗ Disabled</span>
|
<span style="color: var(--text-secondary)">✗ Disabled</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span style="color: var(--text-secondary)">Last Sync</span>
|
<span style="color: var(--text-secondary)">Last Sync</span>
|
||||||
if device.LastSync != "" {
|
if device.LastSync != "" {
|
||||||
<span style="color: var(--text-primary)">{ device.LastSync }</span>
|
<span style="color: var(--text-primary)">{ device.LastSync }</span>
|
||||||
} else {
|
} else {
|
||||||
<span style="color: var(--text-primary)">Never</span>
|
<span style="color: var(--text-primary)">Never</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span style="color: var(--text-secondary)">Last Seen</span>
|
<span style="color: var(--text-secondary)">Last Seen</span>
|
||||||
if device.LastSeen != "" {
|
if device.LastSeen != "" {
|
||||||
<span style="color: var(--text-primary)">{ device.LastSeen }</span>
|
<span style="color: var(--text-primary)">{ device.LastSeen }</span>
|
||||||
} else {
|
} else {
|
||||||
<span style="color: var(--text-primary)">Never</span>
|
<span style="color: var(--text-primary)">Never</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Pending Registrations -->
|
||||||
<!-- Pending Registrations -->
|
if len(pendingRegistrations) > 0 {
|
||||||
if len(pendingRegistrations) > 0 {
|
<div id="pending-section" class="mt-12">
|
||||||
<div id="pending-section" class="mt-12">
|
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2>
|
||||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2>
|
<div class="space-y-4">
|
||||||
<div class="space-y-4">
|
for _, reg := range pendingRegistrations {
|
||||||
for _, reg := range pendingRegistrations {
|
<div class="card p-4 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||||
<div class="card p-4 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
<div>
|
||||||
<div>
|
<h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3>
|
||||||
<h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3>
|
<p class="text-sm" style="color: var(--text-secondary)">
|
||||||
<p class="text-sm" style="color: var(--text-secondary)">
|
{ reg.DeviceType } - Expires in { reg.ExpiresAt }
|
||||||
{ reg.DeviceType } - Expires in { reg.ExpiresAt }
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div class="flex space-x-2">
|
||||||
<div class="flex space-x-2">
|
<button onclick="approveDevice('{ reg.RegistrationID }')" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||||
<button onclick="approveDevice('{ reg.RegistrationID }')" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
Approve
|
||||||
Approve
|
</button>
|
||||||
</button>
|
<button onclick="rejectDevice('{ reg.RegistrationID }')" class="btn-danger px-4 py-2 rounded-lg text-sm">
|
||||||
<button onclick="rejectDevice('{ reg.RegistrationID }')" class="btn-danger px-4 py-2 rounded-lg text-sm">
|
Reject
|
||||||
Reject
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
<!-- Sync Queue Section -->
|
||||||
|
<div id="sync-queue-section" class="mt-12 hidden">
|
||||||
<!-- Sync Queue Section -->
|
<div class="flex justify-between items-center mb-4">
|
||||||
<div id="sync-queue-section" class="mt-12 hidden">
|
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Queue</h2>
|
||||||
<div class="flex justify-between items-center mb-4">
|
<button onclick="clearSyncQueue()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Queue</h2>
|
Clear Queue
|
||||||
<button onclick="clearSyncQueue()" class="btn-secondary px-4 py-2 rounded-lg">
|
</button>
|
||||||
Clear Queue
|
</div>
|
||||||
</button>
|
<div id="sync-queue" class="space-y-3"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="sync-queue" class="space-y-3"></div>
|
</div>
|
||||||
</div>
|
<!-- Add Device Modal -->
|
||||||
</div>
|
<div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||||
|
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||||
<!-- Add Device Modal -->
|
<div class="flex justify-between items-center mb-6">
|
||||||
<div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2>
|
||||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
<button onclick="hideAddDeviceModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
<div class="flex justify-between items-center mb-6">
|
</div>
|
||||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2>
|
<form id="add-device-form" onsubmit="handleAddDevice(event)">
|
||||||
<button onclick="hideAddDeviceModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
<div class="mb-4">
|
||||||
</div>
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||||||
|
<input
|
||||||
<form id="add-device-form" onsubmit="handleAddDevice(event)">
|
type="text"
|
||||||
<div class="mb-4">
|
id="device-name"
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
required
|
||||||
<input type="text" id="device-name" required
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
placeholder="My Kindle Paperwhite"
|
||||||
placeholder="My Kindle Paperwhite">
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
<div class="mb-4">
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Type</label>
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Type</label>
|
<select
|
||||||
<select id="device-type" required
|
id="device-type"
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
required
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
<option value="">Select device type...</option>
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
<option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option>
|
>
|
||||||
<option value="kobo">Kobo E-Reader</option>
|
<option value="">Select device type...</option>
|
||||||
<option value="web">Web Browser</option>
|
<option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option>
|
||||||
<option value="mobile">Mobile App</option>
|
<option value="kobo">Kobo E-Reader</option>
|
||||||
</select>
|
<option value="web">Web Browser</option>
|
||||||
</div>
|
<option value="mobile">Mobile App</option>
|
||||||
|
</select>
|
||||||
<div class="mb-6">
|
</div>
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Identifier</label>
|
<div class="mb-6">
|
||||||
<input type="text" id="device-identifier" required
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Identifier</label>
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
<input
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
type="text"
|
||||||
placeholder="Hardware ID or Serial Number">
|
id="device-identifier"
|
||||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Enter your device's unique identifier</p>
|
required
|
||||||
</div>
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
<div class="flex justify-end space-x-3">
|
placeholder="Hardware ID or Serial Number"
|
||||||
<button type="button" onclick="hideAddDeviceModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
/>
|
||||||
Cancel
|
<p class="text-xs mt-1" style="color: var(--text-secondary)">Enter your device's unique identifier</p>
|
||||||
</button>
|
</div>
|
||||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
<div class="flex justify-end space-x-3">
|
||||||
Register Device
|
<button type="button" onclick="hideAddDeviceModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
</button>
|
Cancel
|
||||||
</div>
|
</button>
|
||||||
</form>
|
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
</div>
|
Register Device
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
<!-- Device Settings Modal -->
|
</form>
|
||||||
<div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
</div>
|
||||||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
</div>
|
||||||
<div class="flex justify-between items-center mb-6">
|
<!-- Device Settings Modal -->
|
||||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Device Settings</h2>
|
<div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||||
<button onclick="hideDeviceSettingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||||
</div>
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Device Settings</h2>
|
||||||
<form id="device-settings-form" onsubmit="handleSaveDeviceSettings(event)">
|
<button onclick="hideDeviceSettingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
<input type="hidden" id="settings-device-id">
|
</div>
|
||||||
|
<form id="device-settings-form" onsubmit="handleSaveDeviceSettings(event)">
|
||||||
<div class="mb-4">
|
<input type="hidden" id="settings-device-id"/>
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
<input type="hidden" id="settings-device-type"/>
|
||||||
<input type="text" id="settings-device-name" required
|
<div class="mb-4">
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
<input
|
||||||
</div>
|
type="text"
|
||||||
|
id="settings-device-name"
|
||||||
<div class="mb-4">
|
required
|
||||||
<label class="flex items-center space-x-3 cursor-pointer">
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
<input type="checkbox" id="settings-sync-enabled" checked
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
class="w-5 h-5 rounded">
|
/>
|
||||||
<span style="color: var(--text-primary)">Enable Sync</span>
|
</div>
|
||||||
</label>
|
<div class="mb-4">
|
||||||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Allow this device to sync reading progress</p>
|
<label class="flex items-center space-x-3 cursor-pointer">
|
||||||
</div>
|
<input
|
||||||
|
type="checkbox"
|
||||||
<div class="mb-4">
|
id="settings-sync-enabled"
|
||||||
<label class="flex items-center space-x-3 cursor-pointer">
|
checked
|
||||||
<input type="checkbox" id="settings-auto-sync" checked
|
class="w-5 h-5 rounded"
|
||||||
class="w-5 h-5 rounded">
|
/>
|
||||||
<span style="color: var(--text-primary)">Auto Sync</span>
|
<span style="color: var(--text-primary)">Enable Sync</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Automatically sync changes</p>
|
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Allow this device to sync reading progress</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
<div class="mb-6">
|
<label class="flex items-center space-x-3 cursor-pointer">
|
||||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Frequency</label>
|
<input
|
||||||
<select id="settings-sync-frequency"
|
type="checkbox"
|
||||||
class="w-full px-4 py-2 border rounded-lg"
|
id="settings-auto-sync"
|
||||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
checked
|
||||||
<option value="1">Every 1 minute</option>
|
class="w-5 h-5 rounded"
|
||||||
<option value="5" selected>Every 5 minutes</option>
|
/>
|
||||||
<option value="15">Every 15 minutes</option>
|
<span style="color: var(--text-primary)">Auto Sync</span>
|
||||||
<option value="30">Every 30 minutes</option>
|
</label>
|
||||||
<option value="60">Every hour</option>
|
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Automatically sync changes</p>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
<div class="mb-6">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Frequency</label>
|
||||||
<div class="flex justify-end space-x-3">
|
<select
|
||||||
<button type="button" onclick="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
id="settings-sync-frequency"
|
||||||
Cancel
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
</button>
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
<button type="button" onclick="handleRevokeDevice()" class="btn-danger px-4 py-2 rounded-lg">
|
>
|
||||||
Revoke Device
|
<option value="1">Every 1 minute</option>
|
||||||
</button>
|
<option value="5" selected>Every 5 minutes</option>
|
||||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
<option value="15">Every 15 minutes</option>
|
||||||
Save Settings
|
<option value="30">Every 30 minutes</option>
|
||||||
</button>
|
<option value="60">Every hour</option>
|
||||||
</div>
|
</select>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="border-t pt-6 mb-6" style="border-color: var(--border);">
|
||||||
</div>
|
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Collection View Settings</h3>
|
||||||
|
<p class="text-sm mb-4" style="color: var(--text-secondary)">Configure how collections are displayed on this device</p>
|
||||||
<script>
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Default View Mode</label>
|
||||||
|
<select
|
||||||
|
id="settings-view-mode"
|
||||||
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="grid">Grid View</option>
|
||||||
|
<option value="list">List View</option>
|
||||||
|
<option value="compact">Compact View</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sort Collections By</label>
|
||||||
|
<select
|
||||||
|
id="settings-sort-order"
|
||||||
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="name">Collection Name</option>
|
||||||
|
<option value="created">Date Created</option>
|
||||||
|
<option value="book_count">Book Count</option>
|
||||||
|
<option value="recent">Recently Added</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Items Per Page</label>
|
||||||
|
<select
|
||||||
|
id="settings-items-per-page"
|
||||||
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="12">12 items</option>
|
||||||
|
<option value="24" selected>24 items</option>
|
||||||
|
<option value="48">48 items</option>
|
||||||
|
<option value="96">96 items</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="flex items-center space-x-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="settings-show-covers"
|
||||||
|
checked
|
||||||
|
class="w-5 h-5 rounded"
|
||||||
|
/>
|
||||||
|
<span style="color: var(--text-primary)">Show Cover Images</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display book covers in collection views</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="flex items-center space-x-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="settings-show-progress"
|
||||||
|
class="w-5 h-5 rounded"
|
||||||
|
/>
|
||||||
|
<span style="color: var(--text-primary)">Show Reading Progress</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display progress indicators for books</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="handleRevokeDevice()" class="btn-danger px-4 py-2 rounded-lg">
|
||||||
|
Revoke Device
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Device Shelf Mappings Modal -->
|
||||||
|
<div id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||||
|
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Collection to Shelf Mappings</h2>
|
||||||
|
<button onclick="hideShelfMappingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="mappings-device-id"/>
|
||||||
|
<div id="shelf-mappings-container" class="space-y-4 mb-6">
|
||||||
|
<p style="color: var(--text-secondary)">Loading mappings...</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideShelfMappingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="showAddMappingModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
|
➕ Add Mapping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Add/Edit Shelf Mapping Modal -->
|
||||||
|
<div id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||||||
|
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Collection Mapping</h2>
|
||||||
|
<button onclick="hideAddMappingModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||||||
|
</div>
|
||||||
|
<form id="mapping-form" onsubmit="handleSaveMapping(event)">
|
||||||
|
<input type="hidden" id="mapping-device-id"/>
|
||||||
|
<input type="hidden" id="mapping-id"/>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Collection</label>
|
||||||
|
<select
|
||||||
|
id="mapping-collection"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="">Select collection...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Shelf Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="mapping-shelf-name"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
placeholder="e.g., Sci-Fi"
|
||||||
|
/>
|
||||||
|
<p class="text-xs mt-1" style="color: var(--text-secondary)">Name of the shelf on this device</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Direction</label>
|
||||||
|
<select
|
||||||
|
id="mapping-sync-direction"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-2 border rounded-lg"
|
||||||
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||||
|
>
|
||||||
|
<option value="bidirectional">Bidirectional (sync both ways)</option>
|
||||||
|
<option value="book_to_device">Bookmann → Device</option>
|
||||||
|
<option value="device_to_book">Device → Bookmann</option>
|
||||||
|
<option value="none">No sync</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="hideAddMappingModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||||
|
Save Mapping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
function getDeviceIcon(typeName) {
|
function getDeviceIcon(typeName) {
|
||||||
const deviceIcons = {
|
const deviceIcons = {
|
||||||
'koreader': '📖',
|
'koreader': '📖',
|
||||||
@@ -307,14 +462,192 @@ templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingReg
|
|||||||
if (!device) return;
|
if (!device) return;
|
||||||
|
|
||||||
document.getElementById('settings-device-id').value = deviceId;
|
document.getElementById('settings-device-id').value = deviceId;
|
||||||
|
document.getElementById('settings-device-type').value = device.device_type;
|
||||||
document.getElementById('settings-device-name').value = device.device_name;
|
document.getElementById('settings-device-name').value = device.device_name;
|
||||||
document.getElementById('settings-sync-enabled').checked = device.sync_enabled;
|
document.getElementById('settings-sync-enabled').checked = device.sync_enabled;
|
||||||
document.getElementById('settings-auto-sync').checked = device.auto_sync;
|
document.getElementById('settings-auto-sync').checked = device.auto_sync;
|
||||||
document.getElementById('settings-sync-frequency').value = device.sync_frequency_minutes;
|
document.getElementById('settings-sync-frequency').value = device.sync_frequency_minutes;
|
||||||
|
|
||||||
|
const deviceType = device.device_type;
|
||||||
|
|
||||||
|
fetch('/api/collections', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.collections && result.collections.length > 0) {
|
||||||
|
const firstCollection = result.collections[0];
|
||||||
|
const viewSettings = firstCollection.view_settings || {};
|
||||||
|
const deviceSettings = viewSettings[deviceType] || {};
|
||||||
|
|
||||||
|
document.getElementById('settings-view-mode').value = deviceSettings.view_mode || 'grid';
|
||||||
|
document.getElementById('settings-sort-order').value = deviceSettings.sort_order || 'name';
|
||||||
|
document.getElementById('settings-items-per-page').value = deviceSettings.items_per_page || 24;
|
||||||
|
document.getElementById('settings-show-covers').checked = deviceSettings.show_covers !== false;
|
||||||
|
document.getElementById('settings-show-progress').checked = deviceSettings.show_progress || false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load view settings:', error);
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('device-settings-modal').classList.remove('hidden');
|
document.getElementById('device-settings-modal').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showShelfMappings(deviceId) {
|
||||||
|
document.getElementById('mappings-device-id').value = deviceId;
|
||||||
|
loadShelfMappings(deviceId);
|
||||||
|
document.getElementById('shelf-mappings-modal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideShelfMappingsModal() {
|
||||||
|
document.getElementById('shelf-mappings-modal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShelfMappings(deviceId) {
|
||||||
|
const container = document.getElementById('shelf-mappings-container');
|
||||||
|
|
||||||
|
fetch(`/api/devices/${deviceId}/collections`, {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.mappings && result.mappings.length > 0) {
|
||||||
|
container.innerHTML = result.mappings.map(mapping => `
|
||||||
|
<div class="card p-4 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold" style="color: var(--text-primary)">${mapping.collection_name}</h4>
|
||||||
|
<p class="text-sm" style="color: var(--text-secondary)">
|
||||||
|
→ Device Shelf: <strong>${mapping.device_shelf_name}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs" style="color: var(--text-secondary)">
|
||||||
|
Sync: ${mapping.sync_direction}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button onclick="editMapping('${mapping.id}', '${mapping.collection_id}', '${mapping.device_shelf_name}', '${mapping.sync_direction}')"
|
||||||
|
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-secondary);">
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteMapping('${mapping.id}')"
|
||||||
|
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-secondary);">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
container.innerHTML = '<p style="color: var(--text-secondary)">No shelf mappings configured. Click "Add Mapping" to create one.</p>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
container.innerHTML = '<p style="color: var(--error)">Failed to load mappings</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddMappingModal() {
|
||||||
|
const deviceId = document.getElementById('mappings-device-id').value;
|
||||||
|
document.getElementById('mapping-device-id').value = deviceId;
|
||||||
|
document.getElementById('mapping-id').value = '';
|
||||||
|
document.getElementById('mapping-collection').value = '';
|
||||||
|
document.getElementById('mapping-shelf-name').value = '';
|
||||||
|
document.getElementById('mapping-sync-direction').value = 'bidirectional';
|
||||||
|
|
||||||
|
loadCollections();
|
||||||
|
document.getElementById('add-mapping-modal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAddMappingModal() {
|
||||||
|
document.getElementById('add-mapping-modal').classList.add('hidden');
|
||||||
|
document.getElementById('mapping-form').reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCollections() {
|
||||||
|
const select = document.getElementById('mapping-collection');
|
||||||
|
|
||||||
|
fetch('/api/collections', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.collections) {
|
||||||
|
select.innerHTML = '<option value="">Select collection...</option>' +
|
||||||
|
result.collections.map(col => `<option value="${col.id}">${col.name}</option>`).join('');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load collections', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function editMapping(mappingId, collectionId, shelfName, syncDirection) {
|
||||||
|
document.getElementById('mapping-id').value = mappingId;
|
||||||
|
document.getElementById('mapping-collection').value = collectionId;
|
||||||
|
document.getElementById('mapping-shelf-name').value = shelfName;
|
||||||
|
document.getElementById('mapping-sync-direction').value = syncDirection;
|
||||||
|
|
||||||
|
document.getElementById('add-mapping-modal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveMapping(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const deviceId = document.getElementById('mapping-device-id').value;
|
||||||
|
const mappingId = document.getElementById('mapping-id').value;
|
||||||
|
const isUpdate = mappingId !== '';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
collection_id: document.getElementById('mapping-collection').value,
|
||||||
|
device_shelf_name: document.getElementById('mapping-shelf-name').value,
|
||||||
|
sync_direction: document.getElementById('mapping-sync-direction').value
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = isUpdate ? `/api/devices/${deviceId}/collections/${mappingId}` : `/api/devices/${deviceId}/collections`;
|
||||||
|
const method = isUpdate ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
showToast(isUpdate ? 'Mapping updated' : 'Mapping created', 'success');
|
||||||
|
hideAddMappingModal();
|
||||||
|
loadShelfMappings(deviceId);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Failed to save mapping', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMapping(mappingId) {
|
||||||
|
if (!confirm('Are you sure you want to delete this mapping?')) return;
|
||||||
|
|
||||||
|
const deviceId = document.getElementById('mappings-device-id').value;
|
||||||
|
|
||||||
|
fetch(`/api/devices/${deviceId}/collections/${mappingId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
showToast('Mapping deleted', 'success');
|
||||||
|
loadShelfMappings(deviceId);
|
||||||
|
} else {
|
||||||
|
showToast('Failed to delete mapping', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showToast('Failed to delete mapping', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function hideDeviceSettingsModal() {
|
function hideDeviceSettingsModal() {
|
||||||
document.getElementById('device-settings-modal').classList.add('hidden');
|
document.getElementById('device-settings-modal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -323,27 +656,64 @@ templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingReg
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const deviceId = document.getElementById('settings-device-id').value;
|
const deviceId = document.getElementById('settings-device-id').value;
|
||||||
const data = {
|
const deviceType = document.getElementById('settings-device-type')?.value || '';
|
||||||
|
|
||||||
|
const deviceData = {
|
||||||
device_name: document.getElementById('settings-device-name').value,
|
device_name: document.getElementById('settings-device-name').value,
|
||||||
sync_enabled: document.getElementById('settings-sync-enabled').checked,
|
sync_enabled: document.getElementById('settings-sync-enabled').checked,
|
||||||
auto_sync: document.getElementById('settings-auto-sync').checked,
|
auto_sync: document.getElementById('settings-auto-sync').checked,
|
||||||
sync_frequency_minutes: parseInt(document.getElementById('settings-sync-frequency').value)
|
sync_frequency_minutes: parseInt(document.getElementById('settings-sync-frequency').value)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewSettings = {
|
||||||
|
view_mode: document.getElementById('settings-view-mode').value,
|
||||||
|
sort_order: document.getElementById('settings-sort-order').value,
|
||||||
|
items_per_page: parseInt(document.getElementById('settings-items-per-page').value),
|
||||||
|
show_covers: document.getElementById('settings-show-covers').checked,
|
||||||
|
show_progress: document.getElementById('settings-show-progress').checked
|
||||||
|
};
|
||||||
|
|
||||||
fetch(`/api/devices/${deviceId}`, {
|
fetch(`/api/devices/${deviceId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(deviceData)
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
showToast('Device settings saved', 'success');
|
return fetch('/api/collections', {
|
||||||
hideDeviceSettingsModal();
|
method: 'GET',
|
||||||
location.reload();
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
})
|
});
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
const updatePromises = result.collections.map(collection => {
|
||||||
|
const currentSettings = collection.view_settings || {};
|
||||||
|
currentSettings[deviceType] = viewSettings;
|
||||||
|
|
||||||
|
return fetch(`/api/collections/${collection.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...collection,
|
||||||
|
view_settings: currentSettings
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(updatePromises);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
showToast('Device settings saved', 'success');
|
||||||
|
hideDeviceSettingsModal();
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
showToast('Failed to save settings', 'error');
|
showToast('Failed to save settings', 'error');
|
||||||
});
|
});
|
||||||
@@ -425,6 +795,6 @@ templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingReg
|
|||||||
// Load devices on page load
|
// Load devices on page load
|
||||||
loadDevices();
|
loadDevices();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user