|
|
|
|
@@ -236,8 +236,10 @@
|
|
|
|
|
let pushError = $state('');
|
|
|
|
|
|
|
|
|
|
// ── In-app notifications ──────────────────────────────────────────────────────
|
|
|
|
|
let notifyNewChapters = $state(data.notifyNewChapters ?? true);
|
|
|
|
|
let notifyNewChaptersSaving = $state(false);
|
|
|
|
|
let notifyNewChapters = $state(data.notifyNewChapters ?? true);
|
|
|
|
|
let notifyNewChaptersPush = $state(data.notifyNewChaptersPush ?? true);
|
|
|
|
|
let notifyNewChaptersSaving = $state(false);
|
|
|
|
|
let notifyNewChaptersPushSaving = $state(false);
|
|
|
|
|
|
|
|
|
|
async function toggleNotifyNewChapters() {
|
|
|
|
|
notifyNewChaptersSaving = true;
|
|
|
|
|
@@ -248,14 +250,27 @@
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ notify_new_chapters: next })
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
notifyNewChapters = next;
|
|
|
|
|
}
|
|
|
|
|
if (res.ok) notifyNewChapters = next;
|
|
|
|
|
} catch { /* ignore */ } finally {
|
|
|
|
|
notifyNewChaptersSaving = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function toggleNotifyNewChaptersPush() {
|
|
|
|
|
notifyNewChaptersPushSaving = true;
|
|
|
|
|
const next = !notifyNewChaptersPush;
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/profile', {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ notify_new_chapters_push: next })
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) notifyNewChaptersPush = next;
|
|
|
|
|
} catch { /* ignore */ } finally {
|
|
|
|
|
notifyNewChaptersPushSaving = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!browser) return;
|
|
|
|
|
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
|
|
|
@@ -802,8 +817,16 @@
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="flex-1 text-sm font-medium text-(--color-text)">Notifications</span>
|
|
|
|
|
<span class="text-xs mr-2 hidden sm:inline {notifyNewChapters ? 'text-(--color-brand)' : 'text-(--color-muted)'}">
|
|
|
|
|
{notifyNewChapters ? 'On' : 'Off'}
|
|
|
|
|
<span class="text-xs mr-2 hidden sm:inline text-(--color-muted)">
|
|
|
|
|
{#if notifyNewChapters && pushState === 'subscribed'}
|
|
|
|
|
<span class="text-(--color-brand)">In-app · Push</span>
|
|
|
|
|
{:else if notifyNewChapters}
|
|
|
|
|
<span class="text-(--color-brand)">In-app</span>
|
|
|
|
|
{:else if pushState === 'subscribed'}
|
|
|
|
|
<span class="text-(--color-brand)">Push</span>
|
|
|
|
|
{:else}
|
|
|
|
|
Off
|
|
|
|
|
{/if}
|
|
|
|
|
</span>
|
|
|
|
|
<svg class={cn(chevronClass, expanded === 'notifications' ? 'rotate-90' : '')} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
|
|
|
@@ -814,50 +837,24 @@
|
|
|
|
|
<div class="px-5 py-5 space-y-5 bg-(--color-surface-3)/30">
|
|
|
|
|
<span class="text-xs font-semibold text-(--color-muted) uppercase tracking-wider">Notifications</span>
|
|
|
|
|
|
|
|
|
|
<!-- In-app -->
|
|
|
|
|
<div class="flex items-start justify-between gap-4">
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
<p class="text-sm font-medium text-(--color-text)">In-app notifications</p>
|
|
|
|
|
<p class="text-sm text-(--color-muted) mt-0.5">
|
|
|
|
|
{#if notifyNewChapters}
|
|
|
|
|
Notified when new chapters arrive in your library.
|
|
|
|
|
{:else}
|
|
|
|
|
In-app new-chapter notifications are disabled.
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={toggleNotifyNewChapters}
|
|
|
|
|
disabled={notifyNewChaptersSaving}
|
|
|
|
|
class={cn(
|
|
|
|
|
'shrink-0 relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none disabled:opacity-50',
|
|
|
|
|
notifyNewChapters ? 'bg-(--color-brand)' : 'bg-(--color-surface-3)'
|
|
|
|
|
)}
|
|
|
|
|
role="switch"
|
|
|
|
|
aria-checked={notifyNewChapters}
|
|
|
|
|
title={notifyNewChapters ? 'Turn off in-app notifications' : 'Turn on in-app notifications'}
|
|
|
|
|
>
|
|
|
|
|
<span class={cn('inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform', notifyNewChapters ? 'translate-x-6' : 'translate-x-1')}></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Push -->
|
|
|
|
|
<!-- Browser push master toggle -->
|
|
|
|
|
{#if pushState !== 'unsupported'}
|
|
|
|
|
<div class="flex items-start justify-between gap-4">
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
<p class="text-sm font-medium text-(--color-text)">Push notifications</p>
|
|
|
|
|
<p class="text-sm text-(--color-muted) mt-0.5">
|
|
|
|
|
<p class="text-sm font-medium text-(--color-text)">Browser push</p>
|
|
|
|
|
<p class="text-xs text-(--color-muted) mt-0.5">
|
|
|
|
|
{#if pushState === 'subscribed'}
|
|
|
|
|
Push enabled for new chapters in your library.
|
|
|
|
|
This browser is subscribed to push notifications.
|
|
|
|
|
{:else if pushState === 'denied'}
|
|
|
|
|
Blocked by your browser. Change in browser settings.
|
|
|
|
|
Blocked by your browser — change in browser settings.
|
|
|
|
|
{:else if pushState === 'loading'}
|
|
|
|
|
Updating…
|
|
|
|
|
{:else}
|
|
|
|
|
Get notified when new chapters arrive.
|
|
|
|
|
Subscribe to receive push notifications in this browser.
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
{#if pushError}
|
|
|
|
|
<p class="text-sm text-(--color-danger) mt-1.5">{pushError}</p>
|
|
|
|
|
<p class="text-xs text-(--color-danger) mt-1">{pushError}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
@@ -890,6 +887,63 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Per-category table -->
|
|
|
|
|
<div class="rounded-lg border border-(--color-border) overflow-hidden">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="grid grid-cols-[1fr_auto_auto] items-center gap-4 px-4 py-2 bg-(--color-surface-3)/60 border-b border-(--color-border)">
|
|
|
|
|
<span class="text-xs font-semibold text-(--color-muted) uppercase tracking-wider">Category</span>
|
|
|
|
|
<span class="text-xs font-semibold text-(--color-muted) uppercase tracking-wider w-12 text-center">In-app</span>
|
|
|
|
|
<span class="text-xs font-semibold text-(--color-muted) uppercase tracking-wider w-12 text-center">Push</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- New chapters row -->
|
|
|
|
|
<div class="grid grid-cols-[1fr_auto_auto] items-center gap-4 px-4 py-3">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-sm font-medium text-(--color-text)">New chapters</p>
|
|
|
|
|
<p class="text-xs text-(--color-muted) mt-0.5">When a book in your library gets new chapters</p>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- In-app toggle -->
|
|
|
|
|
<div class="w-12 flex justify-center">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="switch"
|
|
|
|
|
aria-checked={notifyNewChapters}
|
|
|
|
|
aria-label="In-app notifications for new chapters"
|
|
|
|
|
onclick={toggleNotifyNewChapters}
|
|
|
|
|
disabled={notifyNewChaptersSaving}
|
|
|
|
|
class={cn(
|
|
|
|
|
'shrink-0 relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-(--color-brand) focus:ring-offset-2 focus:ring-offset-(--color-surface) disabled:opacity-50',
|
|
|
|
|
notifyNewChapters ? 'bg-(--color-brand)' : 'bg-(--color-surface-3) border border-(--color-border)'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span class={cn('inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform', notifyNewChapters ? 'translate-x-6' : 'translate-x-1')}></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Push toggle -->
|
|
|
|
|
<div class="w-12 flex justify-center">
|
|
|
|
|
{#if pushState === 'unsupported'}
|
|
|
|
|
<span class="text-xs text-(--color-muted)" title="Push not supported in this browser">—</span>
|
|
|
|
|
{:else if pushState === 'denied'}
|
|
|
|
|
<span class="text-xs text-(--color-muted)" title="Push blocked by browser">—</span>
|
|
|
|
|
{:else}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="switch"
|
|
|
|
|
aria-checked={notifyNewChaptersPush && pushState === 'subscribed'}
|
|
|
|
|
aria-label="Push notifications for new chapters"
|
|
|
|
|
onclick={toggleNotifyNewChaptersPush}
|
|
|
|
|
disabled={notifyNewChaptersPushSaving || pushState !== 'subscribed'}
|
|
|
|
|
class={cn(
|
|
|
|
|
'shrink-0 relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-(--color-brand) focus:ring-offset-2 focus:ring-offset-(--color-surface) disabled:opacity-40',
|
|
|
|
|
notifyNewChaptersPush && pushState === 'subscribed' ? 'bg-(--color-brand)' : 'bg-(--color-surface-3) border border-(--color-border)'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span class={cn('inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform', notifyNewChaptersPush && pushState === 'subscribed' ? 'translate-x-6' : 'translate-x-1')}></span>
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|