From f9730dc95c59295f18640494b53f467d99c75751 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Fri, 13 Mar 2026 12:50:35 +1300 Subject: [PATCH] Started thumbnail actions --- materialious/src/lib/api/backend/history.ts | 9 +- materialious/src/lib/api/historyPool.ts | 2 +- materialious/src/lib/api/index.ts | 9 +- materialious/src/lib/components/Share.svelte | 18 +- .../lib/components/layout/ItemsList.svelte | 10 +- .../thumbnail/VideoThumbnail.svelte | 177 +++++++++++++----- materialious/src/lib/i18n/locales/en.json | 4 + materialious/src/lib/thumbnail.ts | 16 +- 8 files changed, 182 insertions(+), 63 deletions(-) diff --git a/materialious/src/lib/api/backend/history.ts b/materialious/src/lib/api/backend/history.ts index 2bdf6566..af39ace3 100644 --- a/materialious/src/lib/api/backend/history.ts +++ b/materialious/src/lib/api/backend/history.ts @@ -1,7 +1,8 @@ import sodium from 'libsodium-wrappers-sumo'; import { decryptWithMasterKey, encryptWithMasterKey, getRawKey, getSecureHash } from './encryption'; -import type { VideoPlay, VideoWatchHistory } from '../model'; +import type { VideoWatchHistory } from '../model'; import { getBestThumbnail } from '$lib/images'; +import type { ThumbnailVideo } from '$lib/thumbnail'; export async function updateWatchHistoryBackend(videoId: string, progress: number) { await sodium.ready; @@ -128,7 +129,7 @@ export async function deleteWatchHistoryItemBackend(videoId: string) { await fetch(`/api/user/history/${videoHash}`, { method: 'DELETE' }); } -export async function saveWatchHistoryBackend(video: VideoPlay, progress: number = 0) { +export async function saveWatchHistoryBackend(video: ThumbnailVideo, progress: number = 0) { await sodium.ready; const rawKey = await getRawKey(); if (!rawKey) return; @@ -137,7 +138,9 @@ export async function saveWatchHistoryBackend(video: VideoPlay, progress: number const title = await encryptWithMasterKey(video.title); const author = await encryptWithMasterKey(video.author); - const thumbnail = await encryptWithMasterKey(getBestThumbnail(video.videoThumbnails)); + const thumbnail = await encryptWithMasterKey( + 'videoThumbnails' in video ? getBestThumbnail(video.videoThumbnails) : video.thumbnail + ); const videoId = await encryptWithMasterKey(video.videoId); await fetch('/api/user/history', { diff --git a/materialious/src/lib/api/historyPool.ts b/materialious/src/lib/api/historyPool.ts index e588f3dc..2a05c267 100644 --- a/materialious/src/lib/api/historyPool.ts +++ b/materialious/src/lib/api/historyPool.ts @@ -5,7 +5,7 @@ const videoIds: string[] = []; const pendingResolves = new Map void>(); let timeout: ReturnType | null = null; -const DEBOUNCE_MS = 2000; +const DEBOUNCE_MS = 1000; const BATCH_SIZE = 100; async function processBatches(): Promise { diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index 6039278c..a0053cb9 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -21,7 +21,8 @@ import type { CommentsOptions, ChannelOptions, ChannelContent, - VideoWatchHistory + VideoWatchHistory, + VideoBase } from './model'; import { commentsSetDefaults, searchSetDefaults, useEngineFallback } from './misc'; import { getSearchYTjs } from './youtubejs/search'; @@ -82,6 +83,7 @@ import { } from './backend/history'; import { localDb } from '$lib/dexie'; import { getBestThumbnail } from '$lib/images'; +import type { ThumbnailVideo } from '$lib/thumbnail'; export async function getPopular(fetchOptions?: RequestInit): Promise { // Doesn't exist in YTjs. @@ -368,7 +370,7 @@ export async function updateWatchHistory( await localDb.watchHistory.update({ videoId }, { progress, watched: new Date() }); } -export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { +export async function saveWatchHistory(video: ThumbnailVideo, progress: number = 0) { if (!get(watchHistoryEnabledStore)) return; if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) { @@ -384,7 +386,8 @@ export async function saveWatchHistory(video: VideoPlay, progress: number = 0) { progress, id: video.videoId, title: video.title, - thumbnail: getBestThumbnail(video.videoThumbnails), + thumbnail: + 'videoThumbnails' in video ? getBestThumbnail(video.videoThumbnails) : video.thumbnail, videoId: video.videoId, type: 'historyVideo' }); diff --git a/materialious/src/lib/components/Share.svelte b/materialious/src/lib/components/Share.svelte index b88b4d0e..c8d80e82 100644 --- a/materialious/src/lib/components/Share.svelte +++ b/materialious/src/lib/components/Share.svelte @@ -17,11 +17,17 @@ let { shares, includePromptText = undefined, - iconOnly = true + iconOnly = true, + classes = 'surface-container-highest', + menuClasses = 'mobile', + onShare = undefined }: { shares: ShareLink[]; includePromptText?: string; iconOnly: boolean; + classes?: string; + menuClasses?: string; + onShare?: (value: string) => void; } = $props(); const shareBase = { @@ -34,19 +40,21 @@ let shareButtonElement: HTMLElement | undefined = $state(); let includePrompt = $state(false); - async function onShare(share: ShareLink) { + async function onShareValue(share: ShareLink) { const url = new URL(`${shareBase[share.type]}${share.path}`); if (share.param && includePrompt) url.searchParams.append(share.param.key, share.param.value().toString()); + onShare?.(url.toString()); + await shareURL(url.toString()); shareButtonElement?.click(); } - + {#if thumbnailActionsVisible} + +
  • + (thumbnailActionsVisible = false)} + /> +
  • +
  • + +
  • +
    + {/if} + {/if} + @@ -291,6 +367,7 @@ word-wrap: break-word; overflow: hidden; border-radius: 0px; + width: 100%; } .thumbnail-details { @@ -320,6 +397,10 @@ align-items: end; } + menu li:hover { + background-color: transparent; + } + @media screen and (max-width: 1800px) { .sideways-root .thumbnail { width: 100%; diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 6df66699..b7c538fe 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -38,6 +38,10 @@ "sortBy": "Sort By", "features": "Features" }, + "markAs": { + "watched": "Mark as watched", + "unwatched": "Mark as unwatched" + }, "initalSetup": { "useInvidious": "I want to use Invidious", "useLocalFallback": "I want to use local video fallback", diff --git a/materialious/src/lib/thumbnail.ts b/materialious/src/lib/thumbnail.ts index 8e7a5ebf..c8b5fc19 100644 --- a/materialious/src/lib/thumbnail.ts +++ b/materialious/src/lib/thumbnail.ts @@ -1,9 +1,23 @@ -import type { Image } from './api/model'; +import type { + Image, + Notification, + PlaylistPageVideo, + Video, + VideoBase, + VideoWatchHistory +} from './api/model'; import { localDb } from './dexie'; import { getBestThumbnail } from './images'; let isInitial = false; +export type ThumbnailVideo = + | VideoBase + | Video + | Notification + | PlaylistPageVideo + | VideoWatchHistory; + export async function associateAvatar(channelId: string, avatars: Image[]) { if (avatars.length === 0) return;