added fallback thumbnail image

this is in case the videos thumbnail image cant be loaded in.
This commit is contained in:
2025-01-26 14:21:01 -05:00
parent ee3bb2144a
commit 6d9d34fa24
2 changed files with 76 additions and 93 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,5 +1,11 @@
// js/app.js // js/app.js
// <ai_context>
// DO NOT REMOVE OR EDIT. This context is for internal build tooling.
// File: js/app.js
// Project: bitvid
// </ai_context>
import { nostrClient } from "./nostr.js"; import { nostrClient } from "./nostr.js";
import { torrentClient } from "./webtorrent.js"; import { torrentClient } from "./webtorrent.js";
import { isDevMode } from "./config.js"; import { isDevMode } from "./config.js";
@@ -553,18 +559,14 @@ class bitvidApp {
// Sort by creation date // Sort by creation date
videoArray.sort((a, b) => b.created_at - a.created_at); videoArray.sort((a, b) => b.created_at - a.created_at);
// Prepare to fetch user profiles
const userProfiles = new Map(); const userProfiles = new Map();
const uniquePubkeys = [...new Set(videoArray.map((v) => v.pubkey))]; const uniquePubkeys = [...new Set(videoArray.map((v) => v.pubkey))];
// Fetch user profiles
for (const pubkey of uniquePubkeys) { for (const pubkey of uniquePubkeys) {
try { try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [ const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{ { kinds: [0], authors: [pubkey], limit: 1 },
kinds: [0],
authors: [pubkey],
limit: 1,
},
]); ]);
if (userEvents[0]?.content) { if (userEvents[0]?.content) {
@@ -588,7 +590,7 @@ class bitvidApp {
} }
} }
// Build HTML for each video // Build video cards
const renderedVideos = videoArray const renderedVideos = videoArray
.map((video, index) => { .map((video, index) => {
try { try {
@@ -597,30 +599,24 @@ class bitvidApp {
return ""; return "";
} }
// First, create a ?v=... link for middle-click / ctrl+click // Create share URL
const nevent = window.NostrTools.nip19.neventEncode({ const nevent = window.NostrTools.nip19.neventEncode({ id: video.id });
id: video.id, const shareUrl = `${window.location.pathname}?v=${encodeURIComponent(nevent)}`;
});
const shareUrl = `${
window.location.pathname
}?v=${encodeURIComponent(nevent)}`;
// Get profile info
const profile = userProfiles.get(video.pubkey) || { const profile = userProfiles.get(video.pubkey) || {
name: "Unknown", name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`, picture: `https://robohash.org/${video.pubkey}`,
}; };
const timeAgo = this.formatTimeAgo(video.created_at); const timeAgo = this.formatTimeAgo(video.created_at);
// If user is the owner // Determine edit capability
const canEdit = video.pubkey === this.pubkey; const canEdit = video.pubkey === this.pubkey;
const highlightClass = video.isPrivate && canEdit
? "border-2 border-yellow-500"
: "border-none";
// If it's private + user owns it => highlight with a special border // If user can edit, show gear menu
const highlightClass =
video.isPrivate && canEdit
? "border-2 border-yellow-500"
: "border-none"; // normal case
// Gear menu if canEdit
const gearMenu = canEdit const gearMenu = canEdit
? ` ? `
<div class="relative inline-block ml-3 overflow-visible"> <div class="relative inline-block ml-3 overflow-visible">
@@ -635,7 +631,6 @@ class bitvidApp {
class="w-5 h-5" class="w-5 h-5"
/> />
</button> </button>
<!-- The dropdown appears above the gear (bottom-full) -->
<div <div
id="settingsDropdown-${index}" id="settingsDropdown-${index}"
class="hidden absolute right-0 bottom-full mb-2 w-32 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50" class="hidden absolute right-0 bottom-full mb-2 w-32 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
@@ -659,13 +654,10 @@ class bitvidApp {
` `
: ""; : "";
// Instead of a <div onclick="..."> for the thumbnail, we use <a> // Main video card
// This allows middle-click or ctrl+click to open shareUrl in a new tab,
// while left-click is prevented => opens modal
return ` return `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}"> <div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
<!-- VIDEO THUMBNAIL via <a> -->
<a <a
href="${shareUrl}" href="${shareUrl}"
target="_blank" target="_blank"
@@ -677,60 +669,51 @@ class bitvidApp {
app.playVideo('${encodeURIComponent(video.magnet)}'); app.playVideo('${encodeURIComponent(video.magnet)}');
}" }"
> >
${ <img
video.thumbnail src="assets/jpg/video-thumbnail-fallback.jpg"
? `<img data-real-src="${this.escapeHTML(video.thumbnail)}"
src="${this.escapeHTML(video.thumbnail)}" alt="${this.escapeHTML(video.title)}"
alt="${this.escapeHTML(video.title)}" class="w-full h-full object-cover"
class="w-full h-full object-cover" onload="
>` const realSrc = this.getAttribute('data-real-src');
: `<div class="flex items-center justify-center h-full bg-gray-800"> if (realSrc) {
<svg class="w-16 h-16 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> const testImg = new Image();
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" testImg.onload = () => { this.src = realSrc; };
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /> testImg.src = realSrc;
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" }
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> "
</svg> />
</div>`
}
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity duration-300"></div> <div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity duration-300"></div>
</a> </a>
<!-- CARD INFO -->
<div class="p-4"> <div class="p-4">
<!-- TITLE --> <h3
<h3 class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3" onclick="app.playVideo('${encodeURIComponent(video.magnet)}')"
onclick="app.playVideo('${encodeURIComponent( >
video.magnet ${this.escapeHTML(video.title)}
)}')" </h3>
>
${this.escapeHTML(video.title)}
</h3>
<!-- CREATOR info + gear icon --> <div class="flex items-center justify-between">
<div class="flex items-center justify-between"> <div class="flex items-center space-x-3">
<!-- Left: Avatar & user/time --> <div class="w-8 h-8 rounded-full bg-gray-700 overflow-hidden">
<div class="flex items-center space-x-3"> <img
<div class="w-8 h-8 rounded-full bg-gray-700 overflow-hidden"> src="${this.escapeHTML(profile.picture)}"
<img alt="${profile.name}"
src="${this.escapeHTML(profile.picture)}" class="w-full h-full object-cover"
alt="${profile.name}" >
class="w-full h-full object-cover" </div>
> <div class="min-w-0">
</div> <p class="text-sm text-gray-400 hover:text-gray-300 cursor-pointer">
<div class="min-w-0"> ${this.escapeHTML(profile.name)}
<p class="text-sm text-gray-400 hover:text-gray-300 cursor-pointer"> </p>
${this.escapeHTML(profile.name)} <div class="flex items-center text-xs text-gray-500 mt-1">
</p> <span>${timeAgo}</span>
<div class="flex items-center text-xs text-gray-500 mt-1">
<span>${timeAgo}</span>
</div>
</div>
</div> </div>
<!-- Right: gearMenu if user owns the video --> </div>
${gearMenu}
</div> </div>
${gearMenu}
</div>
</div> </div>
</div> </div>
`; `;