Skip to content
2 changes: 1 addition & 1 deletion src/lib/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/lib/components/ActionSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
.action-sidebar {
position: absolute;
right: var(--space-lg);
bottom: calc(148px + env(safe-area-inset-bottom));
bottom: calc(var(--bottom-nav-height, 64px) + 68px);
display: flex;
flex-direction: column;
align-items: center;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/MusicDisc.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
.music-disc-area {
position: absolute;
right: var(--space-lg);
bottom: calc(90px + env(safe-area-inset-bottom));
bottom: calc(var(--bottom-nav-height, 64px) + 10px);
display: flex;
align-items: center;
gap: var(--space-sm);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ProgressBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
<style>
.progress-bar {
position: absolute;
bottom: calc(52px + env(safe-area-inset-bottom));
bottom: calc(var(--bottom-nav-height, 64px) - 8px);
left: 0;
right: 0;
z-index: 6;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ReelOverlay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
<style>
.reel-overlay {
position: absolute;
bottom: calc(94px + env(safe-area-inset-bottom));
bottom: calc(var(--bottom-nav-height, 64px) + 14px);
left: var(--space-lg);
right: var(--space-lg);
z-index: 5;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/SkeletonReel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
.sidebar-bones {
position: absolute;
right: var(--space-md);
bottom: calc(90px + env(safe-area-inset-bottom));
bottom: calc(var(--bottom-nav-height, 64px) + 10px);
display: flex;
flex-direction: column;
gap: var(--space-lg);
Expand All @@ -66,7 +66,7 @@

.overlay-bones {
position: absolute;
bottom: calc(80px + env(safe-area-inset-bottom));
bottom: var(--bottom-nav-height, 64px);
left: var(--space-lg);
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ToastStack.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
<style>
.toast-stack {
position: fixed;
bottom: calc(80px + env(safe-area-inset-bottom));
bottom: var(--bottom-nav-height, 64px);
left: 50%;
transform: translateX(-50%);
z-index: 300;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/settings/ClipsManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@
/* Selection bar */
.selection-bar {
position: fixed;
bottom: calc(72px + env(safe-area-inset-bottom, 0px));
bottom: calc(var(--bottom-nav-height, 64px) + 8px);
left: 50%;
transform: translateX(-50%);
display: flex;
Expand Down
130 changes: 130 additions & 0 deletions src/lib/components/settings/NotificationSettings.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NotificationPrefs } from '$lib/settingsApi';

const STORAGE_KEY = 'scrolly:push-test-sent-at';
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;

let {
pushSupported,
pushEnabled,
Expand All @@ -18,6 +22,72 @@
onTogglePush: () => void;
onUpdatePref: (key: keyof NotificationPrefs, value: boolean) => void;
} = $props();

let testState = $state<'idle' | 'counting' | 'sent'>('idle');
let countdown = $state(10);
let lastTestedAt = $state<number | null>(null);
let countdownTimer = $state<ReturnType<typeof setInterval> | null>(null);

const lastTestedLabel = $derived.by(() => {
if (!lastTestedAt) return null;
if (Date.now() - lastTestedAt > TWENTY_FOUR_HOURS) return null;
const d = new Date(lastTestedAt);
const h = d.getHours();
const m = d.getMinutes().toString().padStart(2, '0');
const s = d.getSeconds().toString().padStart(2, '0');
const ampm = h >= 12 ? 'PM' : 'AM';
const h12 = h % 12 || 12;
return `Last tested at ${h12}:${m}:${s} ${ampm}`;
});

onMount(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const ts = parseInt(stored, 10);
if (!isNaN(ts) && Date.now() - ts <= TWENTY_FOUR_HOURS) {
lastTestedAt = ts;
} else {
localStorage.removeItem(STORAGE_KEY);
}
}

return () => {
if (countdownTimer) clearInterval(countdownTimer);
};
});

async function sendTestNotification() {
testState = 'counting';
countdown = 10;

countdownTimer = setInterval(() => {
countdown--;
if (countdown <= 0 && countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}, 1000);

try {
const res = await fetch('/api/push/test', { method: 'POST' });
if (res.ok) {
const data = await res.json();
const sentAt = data.sentAt ?? Date.now();
lastTestedAt = sentAt;
localStorage.setItem(STORAGE_KEY, String(sentAt));
testState = 'sent';
} else {
testState = 'idle';
}
} catch {
testState = 'idle';
} finally {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}
}
</script>

{#if !pushSupported}
Expand All @@ -39,6 +109,23 @@
</button>
</div>

{#if pushEnabled}
<div class="test-row">
{#if testState === 'idle'}
<button class="test-btn" onclick={sendTestNotification}> Send test notification </button>
{#if lastTestedLabel}
<span class="last-tested">{lastTestedLabel}</span>
{/if}
{:else if testState === 'counting'}
<button class="test-btn counting" disabled>
Sending in {countdown}s...
</button>
{:else if testState === 'sent'}
<span class="sent-label">Sent to device!</span>
{/if}
</div>
{/if}

<div class="divider"></div>
<h4 class="sub-heading">Notify me about</h4>

Expand Down Expand Up @@ -216,4 +303,47 @@
.toggle.active .toggle-thumb {
transform: translateX(18px);
}

.test-row {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md) 0 0;
}

.test-btn {
border: none;
background: var(--bg-surface);
color: var(--accent-primary);
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
padding: var(--space-sm) var(--space-lg);
border-radius: var(--radius-full);
cursor: pointer;
transition: all 0.2s ease;
}
.test-btn:active {
transform: scale(0.97);
}
.test-btn.counting {
color: var(--text-secondary);
cursor: default;
}
.test-btn:disabled {
opacity: 0.7;
}

.sent-label {
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
color: var(--success);
}

.last-tested {
font-family: var(--font-body);
font-size: 0.75rem;
color: var(--text-muted);
}
</style>
Loading
Loading