diff --git a/src/lib/assets/icon.svg b/src/lib/assets/icon.svg index fbc58c6..4936657 100644 --- a/src/lib/assets/icon.svg +++ b/src/lib/assets/icon.svg @@ -1,5 +1,5 @@ - + diff --git a/src/lib/components/ActionSidebar.svelte b/src/lib/components/ActionSidebar.svelte index 97be29a..0db02fe 100644 --- a/src/lib/components/ActionSidebar.svelte +++ b/src/lib/components/ActionSidebar.svelte @@ -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; diff --git a/src/lib/components/MusicDisc.svelte b/src/lib/components/MusicDisc.svelte index 1dc5591..75cccac 100644 --- a/src/lib/components/MusicDisc.svelte +++ b/src/lib/components/MusicDisc.svelte @@ -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); diff --git a/src/lib/components/ProgressBar.svelte b/src/lib/components/ProgressBar.svelte index 54298e9..4bec6b4 100644 --- a/src/lib/components/ProgressBar.svelte +++ b/src/lib/components/ProgressBar.svelte @@ -108,7 +108,7 @@
+ {#if testState === 'idle'} + + {#if lastTestedLabel} + {lastTestedLabel} + {/if} + {:else if testState === 'counting'} + + {:else if testState === 'sent'} + Sent to device! + {/if} +
+ {/if} +

Notify me about

@@ -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); + } diff --git a/src/lib/components/settings/ShortcutManager.svelte b/src/lib/components/settings/ShortcutManager.svelte index 891107e..64a9697 100644 --- a/src/lib/components/settings/ShortcutManager.svelte +++ b/src/lib/components/settings/ShortcutManager.svelte @@ -1,16 +1,9 @@
@@ -172,70 +54,11 @@
{#if showSheet} - (showSheet = false)}> -
- - - - - - -
-
iCloud Shortcut Link
-

- Paste the iCloud link so iOS members see a "Get Shortcut" button. -

-
- - -
- {#if validationError} -

{validationError}

- {/if} -
- - {#if token} -
-
Shortcut Token
-

Authenticates shortcut requests. Rotate if compromised.

-
- {token.slice(0, 8)}…{token.slice(-4)} - -
-
- {/if} -
-
+ (showSheet = false)} + /> {/if} diff --git a/src/lib/components/settings/ShortcutSheet.svelte b/src/lib/components/settings/ShortcutSheet.svelte new file mode 100644 index 0000000..47830ae --- /dev/null +++ b/src/lib/components/settings/ShortcutSheet.svelte @@ -0,0 +1,416 @@ + + + +
+ + + + + + +
+
iCloud Shortcut Link
+

+ Paste the iCloud link so iOS members see a "Get Shortcut" button. +

+
+ + +
+ {#if validationError} +

{validationError}

+ {/if} + + {#if validated && validationWarnings.length > 0} + doSave(shortcutUrl.trim())} + /> + {/if} +
+ + {#if token} +
+
Shortcut Token
+

Authenticates shortcut requests. Rotate if compromised.

+
+ {token.slice(0, 8)}…{token.slice(-4)} + +
+
+ {/if} +
+
+ + diff --git a/src/lib/components/settings/ValidationResults.svelte b/src/lib/components/settings/ValidationResults.svelte new file mode 100644 index 0000000..880aebf --- /dev/null +++ b/src/lib/components/settings/ValidationResults.svelte @@ -0,0 +1,154 @@ + + +
+ {#if isUnreachable} +
+ + Could not reach iCloud to validate. You can save without checking. +
+
+ + +
+ {:else if hasBlockers} + {#each blockers as warning (warning.code + warning.message)} +
+ + + {@html warning.message} +
+ {/each} +
+ +
+ {:else if hasSoftOnly} + {#each softWarnings as warning (warning.code + warning.message)} +
+ + + {@html warning.message} +
+ {/each} +
+ +
+ {/if} +
+ + diff --git a/src/lib/feed.ts b/src/lib/feed.ts index 92d39f6..4a3f2ad 100644 --- a/src/lib/feed.ts +++ b/src/lib/feed.ts @@ -2,14 +2,17 @@ import type { FeedClip } from '$lib/types'; import { fetchUnwatchedCount } from '$lib/stores/notifications'; export type FeedFilter = 'all' | 'unwatched' | 'watched' | 'favorites'; +export type FeedSort = 'oldest' | 'round-robin'; export function buildClipParams( filter: FeedFilter, offset: number, - pageSize: number + pageSize: number, + sort?: FeedSort ): URLSearchParams { const params = new URLSearchParams(); if (filter !== 'all') params.set('filter', filter); + if (sort && sort !== 'oldest') params.set('sort', sort); params.set('limit', String(pageSize)); params.set('offset', String(offset)); return params; @@ -17,10 +20,11 @@ export function buildClipParams( export async function fetchClips( filter: FeedFilter, - pageSize: number + pageSize: number, + sort?: FeedSort ): Promise<{ clips: FeedClip[]; hasMore: boolean } | null> { try { - const params = buildClipParams(filter, 0, pageSize); + const params = buildClipParams(filter, 0, pageSize, sort); const res = await fetch(`/api/clips?${params}`); if (res.ok) return res.json(); return null; @@ -33,10 +37,11 @@ export async function fetchClips( export async function fetchMoreClips( filter: FeedFilter, offset: number, - pageSize: number + pageSize: number, + sort?: FeedSort ): Promise<{ clips: FeedClip[]; hasMore: boolean } | null> { try { - const params = buildClipParams(filter, offset, pageSize); + const params = buildClipParams(filter, offset, pageSize, sort); const res = await fetch(`/api/clips?${params}`); if (res.ok) return res.json(); return null; diff --git a/src/lib/server/db/migrations/0021_flaky_stature.sql b/src/lib/server/db/migrations/0021_flaky_stature.sql new file mode 100644 index 0000000..f0933d1 --- /dev/null +++ b/src/lib/server/db/migrations/0021_flaky_stature.sql @@ -0,0 +1 @@ +ALTER TABLE `users` ADD `feed_sort_order` text DEFAULT 'oldest' NOT NULL; \ No newline at end of file diff --git a/src/lib/server/db/migrations/meta/0021_snapshot.json b/src/lib/server/db/migrations/meta/0021_snapshot.json new file mode 100644 index 0000000..569497a --- /dev/null +++ b/src/lib/server/db/migrations/meta/0021_snapshot.json @@ -0,0 +1,1219 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ad304cb3-15cb-4a07-8380-0390f33d1389", + "prevId": "9beecb53-a092-49a5-b7bf-caddbb66474c", + "tables": { + "clips": { + "name": "clips", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_url": { + "name": "original_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "video_path": { + "name": "video_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "thumbnail_path": { + "name": "thumbnail_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'downloading'" + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'video'" + }, + "audio_path": { + "name": "audio_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "spotify_url": { + "name": "spotify_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "apple_music_url": { + "name": "apple_music_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "youtube_music_url": { + "name": "youtube_music_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_size_bytes": { + "name": "file_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "clips_group_url": { + "name": "clips_group_url", + "columns": [ + "group_id", + "original_url" + ], + "isUnique": true + } + }, + "foreignKeys": { + "clips_group_id_groups_id_fk": { + "name": "clips_group_id_groups_id_fk", + "tableFrom": "clips", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "clips_added_by_users_id_fk": { + "name": "clips_added_by_users_id_fk", + "tableFrom": "clips", + "tableTo": "users", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment_hearts": { + "name": "comment_hearts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "comment_id": { + "name": "comment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "comment_hearts_unique": { + "name": "comment_hearts_unique", + "columns": [ + "comment_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comment_hearts_comment_id_comments_id_fk": { + "name": "comment_hearts_comment_id_comments_id_fk", + "tableFrom": "comment_hearts", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comment_hearts_user_id_users_id_fk": { + "name": "comment_hearts_user_id_users_id_fk", + "tableFrom": "comment_hearts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comment_views": { + "name": "comment_views", + "columns": { + "clip_id": { + "name": "clip_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "comment_views_unique": { + "name": "comment_views_unique", + "columns": [ + "clip_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comment_views_clip_id_clips_id_fk": { + "name": "comment_views_clip_id_clips_id_fk", + "tableFrom": "comment_views", + "tableTo": "clips", + "columnsFrom": [ + "clip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comment_views_user_id_users_id_fk": { + "name": "comment_views_user_id_users_id_fk", + "tableFrom": "comment_views", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "comments": { + "name": "comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clip_id": { + "name": "clip_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gif_url": { + "name": "gif_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "comments_clip_id_clips_id_fk": { + "name": "comments_clip_id_clips_id_fk", + "tableFrom": "comments", + "tableTo": "clips", + "columnsFrom": [ + "clip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "favorites": { + "name": "favorites", + "columns": { + "clip_id": { + "name": "clip_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "favorites_clip_id_clips_id_fk": { + "name": "favorites_clip_id_clips_id_fk", + "tableFrom": "favorites", + "tableTo": "clips", + "columnsFrom": [ + "clip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "favorites_user_id_users_id_fk": { + "name": "favorites_user_id_users_id_fk", + "tableFrom": "favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invite_code": { + "name": "invite_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_days": { + "name": "retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_storage_mb": { + "name": "max_storage_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_file_size_mb": { + "name": "max_file_size_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 500 + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'coral'" + }, + "download_provider": { + "name": "download_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "platform_filter_mode": { + "name": "platform_filter_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'all'" + }, + "platform_filter_list": { + "name": "platform_filter_list", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shortcut_token": { + "name": "shortcut_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shortcut_url": { + "name": "shortcut_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "groups_invite_code_unique": { + "name": "groups_invite_code_unique", + "columns": [ + "invite_code" + ], + "isUnique": true + }, + "groups_shortcut_token_unique": { + "name": "groups_shortcut_token_unique", + "columns": [ + "shortcut_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_preferences": { + "name": "notification_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "new_adds": { + "name": "new_adds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "reactions": { + "name": "reactions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "comments": { + "name": "comments", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "mentions": { + "name": "mentions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "daily_reminder": { + "name": "daily_reminder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_preferences_user_id_users_id_fk": { + "name": "notification_preferences_user_id_users_id_fk", + "tableFrom": "notification_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clip_id": { + "name": "clip_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment_preview": { + "name": "comment_preview", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notifications_user_created": { + "name": "notifications_user_created", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_clip_id_clips_id_fk": { + "name": "notifications_clip_id_clips_id_fk", + "tableFrom": "notifications", + "tableTo": "clips", + "columnsFrom": [ + "clip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "push_subscriptions": { + "name": "push_subscriptions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keys_p256dh": { + "name": "keys_p256dh", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keys_auth": { + "name": "keys_auth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "push_subscriptions_user_id_users_id_fk": { + "name": "push_subscriptions_user_id_users_id_fk", + "tableFrom": "push_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reactions": { + "name": "reactions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clip_id": { + "name": "clip_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "reactions_unique": { + "name": "reactions_unique", + "columns": [ + "clip_id", + "user_id", + "emoji" + ], + "isUnique": true + } + }, + "foreignKeys": { + "reactions_clip_id_clips_id_fk": { + "name": "reactions_clip_id_clips_id_fk", + "tableFrom": "reactions", + "tableTo": "clips", + "columnsFrom": [ + "clip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_user_id_users_id_fk": { + "name": "reactions_user_id_users_id_fk", + "tableFrom": "reactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "theme_preference": { + "name": "theme_preference", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "auto_scroll": { + "name": "auto_scroll", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "muted_by_default": { + "name": "muted_by_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "feed_sort_order": { + "name": "feed_sort_order", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'oldest'" + }, + "avatar_path": { + "name": "avatar_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "removed_at": { + "name": "removed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_phone_unique": { + "name": "users_phone_unique", + "columns": [ + "phone" + ], + "isUnique": true + } + }, + "foreignKeys": { + "users_group_id_groups_id_fk": { + "name": "users_group_id_groups_id_fk", + "tableFrom": "users", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification_codes": { + "name": "verification_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verified_at": { + "name": "verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "verification_codes_user_id_users_id_fk": { + "name": "verification_codes_user_id_users_id_fk", + "tableFrom": "verification_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "watched": { + "name": "watched", + "columns": { + "clip_id": { + "name": "clip_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "watch_percent": { + "name": "watch_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "watched_at": { + "name": "watched_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "watched_clip_user": { + "name": "watched_clip_user", + "columns": [ + "clip_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "watched_clip_id_clips_id_fk": { + "name": "watched_clip_id_clips_id_fk", + "tableFrom": "watched", + "tableTo": "clips", + "columnsFrom": [ + "clip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "watched_user_id_users_id_fk": { + "name": "watched_user_id_users_id_fk", + "tableFrom": "watched", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/lib/server/db/migrations/meta/_journal.json b/src/lib/server/db/migrations/meta/_journal.json index ed74af1..2d36c23 100644 --- a/src/lib/server/db/migrations/meta/_journal.json +++ b/src/lib/server/db/migrations/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1772347099686, "tag": "0020_concerned_toad_men", "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1772404817972, + "tag": "0021_flaky_stature", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 1cbfe98..67013ad 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -27,6 +27,7 @@ export const users = sqliteTable('users', { themePreference: text('theme_preference').notNull().default('system'), autoScroll: integer('auto_scroll', { mode: 'boolean' }).notNull().default(false), mutedByDefault: integer('muted_by_default', { mode: 'boolean' }).notNull().default(true), + feedSortOrder: text('feed_sort_order').notNull().default('oldest'), avatarPath: text('avatar_path'), removedAt: integer('removed_at', { mode: 'timestamp' }), createdAt: integer('created_at', { mode: 'timestamp' }).notNull() diff --git a/src/lib/settingsApi.ts b/src/lib/settingsApi.ts index bcc57ce..1353c0f 100644 --- a/src/lib/settingsApi.ts +++ b/src/lib/settingsApi.ts @@ -64,6 +64,12 @@ export async function saveMutedByDefault(value: boolean): Promise { await savePreference({ mutedByDefault: value }); } +// --- Feed sort --- + +export async function saveFeedSortOrder(value: 'oldest' | 'round-robin'): Promise { + await savePreference({ feedSortOrder: value }); +} + // --- Accent color --- export function applyAccentColor(key: AccentColorKey): void { diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 72d05d9..b1f26ba 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -17,6 +17,7 @@ import GearSixIcon from 'phosphor-svelte/lib/GearSixIcon'; const { children }: { children: Snippet } = $props(); + let bottomTabsEl: HTMLElement | undefined = $state(); const isFeed = $derived(page.url.pathname === '/'); const isSettings = $derived(page.url.pathname === '/settings'); @@ -29,8 +30,21 @@ startPolling(); fetchGroupMembers(); - // Tell fixed-position components (e.g. InstallBanner) about the bottom nav height - document.documentElement.style.setProperty('--bottom-nav-height', '80px'); + // Measure actual bottom nav height and expose as CSS variable. + // This adapts to the real safe-area insets on each device instead of + // hardcoding 80px + env(safe-area-inset-bottom) everywhere. + const updateNavHeight = () => { + if (bottomTabsEl) { + const h = bottomTabsEl.offsetHeight; + document.documentElement.style.setProperty('--bottom-nav-height', `${h}px`); + } + }; + const navObserver = new ResizeObserver(updateNavHeight); + if (bottomTabsEl) { + navObserver.observe(bottomTabsEl); + updateNavHeight(); + } + // Initialize mute state from user preference const user = page.data?.user; if (user) { @@ -57,6 +71,7 @@ return () => { stopPolling(); + navObserver.disconnect(); themeObserver.disconnect(); darkMq.removeEventListener('change', syncThemeColor); document.documentElement.style.removeProperty('--bottom-nav-height'); @@ -110,7 +125,12 @@
{@render children()}
-