Add per-kind stats, GIF hover playback, and static gif previews
This commit is contained in:
@@ -114,6 +114,17 @@
|
||||
color: var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.vk-media-gif-label {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 6px;
|
||||
font: 600 10px/1 ui-monospace, Menlo, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vk-media-play {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
|
||||
+45
-6
@@ -191,11 +191,31 @@
|
||||
return `${kind}${source}${channel}${author}${aspect}${resolution}${after}${before}`;
|
||||
}
|
||||
|
||||
function kindTotals(field) {
|
||||
const stats = state.stats ?? {};
|
||||
return {
|
||||
images: (stats.image?.[field] ?? 0) + (stats.gif?.[field] ?? 0),
|
||||
videos: stats.video?.[field] ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
function countLoaded() {
|
||||
let images = 0;
|
||||
let videos = 0;
|
||||
for (const item of state.items) {
|
||||
if (item.kind === "video") videos++;
|
||||
else if (item.kind === "image" || item.kind === "gif") images++;
|
||||
}
|
||||
return { images, videos };
|
||||
}
|
||||
|
||||
function renderStatus() {
|
||||
const stats = state.stats ?? { total: 0, downloaded: 0, cached: 0, notDownloaded: 0 };
|
||||
const downloaded = kindTotals("downloaded");
|
||||
const cached = kindTotals("cached");
|
||||
const loaded = countLoaded();
|
||||
setStatus(
|
||||
`(Images/Videos) | Downloaded ${stats.downloaded}/${stats.total} | ` +
|
||||
`Cached ${stats.cached}/${stats.total} (excl Downl) | Loaded ${state.items.length}/${stats.total}`
|
||||
`(Images/Videos) | Downloaded ${downloaded.images}/${downloaded.videos} | ` +
|
||||
`Cached ${cached.images}/${cached.videos} | Loaded ${loaded.images}/${loaded.videos}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -428,15 +448,22 @@
|
||||
const figure = el("figure", "vk-media-figure");
|
||||
const media = el("div", "vk-media-media");
|
||||
const source = gridSource(item);
|
||||
const playable = /\.(mp4|webm|m4v)(\?|$)/i.test(item.url ?? "");
|
||||
const videoSource = item.localUrl || item.playUrl || (item.source !== "embed" ? item.url : null);
|
||||
const playableVideo =
|
||||
!!videoSource &&
|
||||
(/\.(mp4|webm|m4v)(\?|$)/i.test(videoSource) || videoSource === item.playUrl);
|
||||
const animatedGif =
|
||||
item.kind === "gif" && item.remotePreviewUrl && /\.gif(\?|$)/i.test(item.remotePreviewUrl)
|
||||
? item.remotePreviewUrl
|
||||
: null;
|
||||
|
||||
if (item.kind === "video" && source && item.source !== "embed" && playable) {
|
||||
if (item.kind === "video" && videoSource && playableVideo) {
|
||||
// Discord has no server-side video thumbnails; the browser shows the first frame.
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.src = source;
|
||||
video.src = videoSource;
|
||||
video.addEventListener(
|
||||
"loadedmetadata",
|
||||
() => {
|
||||
@@ -452,11 +479,23 @@
|
||||
img.src = source;
|
||||
img.alt = item.kind;
|
||||
img.addEventListener("error", () => placeholder(media, item), { once: true });
|
||||
|
||||
// GIFs stay static in the grid and play while hovered.
|
||||
if (animatedGif) {
|
||||
media.addEventListener("mouseenter", () => {
|
||||
img.src = animatedGif;
|
||||
});
|
||||
media.addEventListener("mouseleave", () => {
|
||||
img.src = source;
|
||||
});
|
||||
}
|
||||
|
||||
media.append(img);
|
||||
} else {
|
||||
placeholder(media, item);
|
||||
}
|
||||
|
||||
if (item.kind === "gif") media.append(el("span", "vk-media-gif-label", "GIF"));
|
||||
if (item.kind === "video") media.append(playIcon(item));
|
||||
|
||||
media.addEventListener("click", () => openViewer(item, false));
|
||||
|
||||
+21
-6
@@ -118,6 +118,8 @@ export interface MediaItemResult {
|
||||
durationSecs: number | null;
|
||||
url: string;
|
||||
playUrl: string | null;
|
||||
/** The remote thumbnail URL from Discord's embed data (may be an animated GIF). */
|
||||
remotePreviewUrl: string | null;
|
||||
localUrl: string | null;
|
||||
thumbUrl: string | null;
|
||||
provider: string | null;
|
||||
@@ -539,9 +541,9 @@ export class MediaService {
|
||||
resolution?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
} = {}): { total: number; downloaded: number; cached: number; notDownloaded: number } {
|
||||
} = {}): Record<MediaKind, { total: number; downloaded: number; cached: number; notDownloaded: number }> {
|
||||
// Deduplicate by URL across scrapes, newest scrape wins, like queryMedia.
|
||||
const byUrl = new Map<string, { downloaded: boolean; cached: boolean }>();
|
||||
const byUrl = new Map<string, { kind: MediaKind; downloaded: boolean; cached: boolean }>();
|
||||
|
||||
for (const meta of this.engine.list()) {
|
||||
if (meta.state === "deleted") continue;
|
||||
@@ -559,6 +561,7 @@ export class MediaService {
|
||||
for (const row of rows) {
|
||||
if (byUrl.has(row.url)) continue;
|
||||
byUrl.set(row.url, {
|
||||
kind: row.kind,
|
||||
downloaded: row.downloaded === 1,
|
||||
cached: row.preview_hash !== null
|
||||
});
|
||||
@@ -571,13 +574,24 @@ export class MediaService {
|
||||
}
|
||||
}
|
||||
|
||||
const totals = { total: byUrl.size, downloaded: 0, cached: 0, notDownloaded: 0 };
|
||||
const totals: Record<
|
||||
MediaKind,
|
||||
{ total: number; downloaded: number; cached: number; notDownloaded: number }
|
||||
> = {
|
||||
image: { total: 0, downloaded: 0, cached: 0, notDownloaded: 0 },
|
||||
gif: { total: 0, downloaded: 0, cached: 0, notDownloaded: 0 },
|
||||
video: { total: 0, downloaded: 0, cached: 0, notDownloaded: 0 },
|
||||
audio: { total: 0, downloaded: 0, cached: 0, notDownloaded: 0 }
|
||||
};
|
||||
|
||||
for (const entry of byUrl.values()) {
|
||||
const bucket = totals[entry.kind];
|
||||
bucket.total++;
|
||||
if (entry.downloaded) {
|
||||
totals.downloaded++;
|
||||
bucket.downloaded++;
|
||||
} else {
|
||||
totals.notDownloaded++;
|
||||
if (entry.cached) totals.cached++;
|
||||
bucket.notDownloaded++;
|
||||
if (entry.cached) bucket.cached++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +708,7 @@ export class MediaService {
|
||||
durationSecs: row.duration_secs,
|
||||
url: row.url,
|
||||
playUrl: row.play_url,
|
||||
remotePreviewUrl: row.preview_url,
|
||||
localUrl:
|
||||
row.file_path && !meta.downloadDir
|
||||
? `/api/media/${meta.id}/${row.id}/file`
|
||||
|
||||
Reference in New Issue
Block a user