mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
Merge pull request #1194 from PR0M3TH3AN/feature/video-card-layout-update-14744375313479485048
Update Video Card layout with dedicated engagement section
This commit is contained in:
4 files changed
+310
-98
No files matched your search
@@ -175,7 +175,13 @@
|
||||
<div class="video-modal__meta-info flex flex-wrap items-center gap-3">
|
||||
<span id="videoTimestamp" class="modal-meta">Posted just now</span>
|
||||
<span id="videoEditedTimestamp" class="modal-meta hidden">Last edited just now</span>
|
||||
<span id="videoViewCount" class="modal-meta modal-view-count">– views</span>
|
||||
<span id="videoViewCount" class="modal-meta modal-view-count flex items-center gap-1.5" title="Views">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
<span data-view-count-text>–</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { normalizeDesignSystemContext } from "../../designSystem.js";
|
||||
import { formatShortNpub } from "../../utils/formatters.js";
|
||||
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
|
||||
|
||||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
|
||||
|
||||
export class SimilarContentCard {
|
||||
@@ -66,6 +67,7 @@ export class SimilarContentCard {
|
||||
this.authorNpubEl = null;
|
||||
this.timeEl = null;
|
||||
this.viewCountEl = null;
|
||||
this.discussionCountEl = null;
|
||||
|
||||
this.build();
|
||||
}
|
||||
@@ -82,6 +84,10 @@ export class SimilarContentCard {
|
||||
return this.viewCountEl;
|
||||
}
|
||||
|
||||
getDiscussionCountElement() {
|
||||
return this.discussionCountEl;
|
||||
}
|
||||
|
||||
closeMoreMenu() {}
|
||||
|
||||
closeSettingsMenu() {}
|
||||
@@ -469,9 +475,9 @@ export class SimilarContentCard {
|
||||
titleLink.title = this.video.title || "Untitled";
|
||||
titleLink.style.minWidth = "0";
|
||||
|
||||
// Modified order: Title -> Author -> Meta
|
||||
const authorStack = this.buildAuthorStack();
|
||||
const metaRow = this.buildMetaRow();
|
||||
const engagement = this.buildEngagementSection();
|
||||
|
||||
content.appendChild(titleLink);
|
||||
if (authorStack) {
|
||||
@@ -480,6 +486,9 @@ export class SimilarContentCard {
|
||||
if (metaRow) {
|
||||
content.appendChild(metaRow);
|
||||
}
|
||||
if (engagement) {
|
||||
content.appendChild(engagement);
|
||||
}
|
||||
|
||||
this.contentEl = content;
|
||||
this.titleEl = titleLink;
|
||||
@@ -556,24 +565,6 @@ export class SimilarContentCard {
|
||||
row.classList.add("player-modal__similar-card-meta");
|
||||
row.style.minWidth = "0";
|
||||
|
||||
// Reordered: Views -> Separator -> Time
|
||||
|
||||
const viewEl = this.document.createElement("span");
|
||||
viewEl.classList.add("player-modal__similar-card-views", "view-count-text");
|
||||
viewEl.dataset.viewCount = "";
|
||||
viewEl.textContent = "– views";
|
||||
if (this.pointerInfo?.key) {
|
||||
viewEl.dataset.viewPointer = this.pointerInfo.key;
|
||||
}
|
||||
row.appendChild(viewEl);
|
||||
this.viewCountEl = viewEl;
|
||||
|
||||
const separator = this.document.createElement("span");
|
||||
separator.classList.add("player-modal__similar-card-separator");
|
||||
separator.setAttribute("aria-hidden", "true");
|
||||
separator.textContent = "•";
|
||||
row.appendChild(separator);
|
||||
|
||||
const timeEl = this.document.createElement("time");
|
||||
timeEl.classList.add("player-modal__similar-card-timestamp");
|
||||
if (this.postedAt !== null) {
|
||||
@@ -592,6 +583,146 @@ export class SimilarContentCard {
|
||||
return row;
|
||||
}
|
||||
|
||||
createEyeIcon(classNames = []) {
|
||||
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.setAttribute("focusable", "false");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "2");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
|
||||
classNames.forEach((className) => {
|
||||
if (className) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
});
|
||||
|
||||
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
|
||||
path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");
|
||||
svg.appendChild(path);
|
||||
|
||||
const circle = this.document.createElementNS(SVG_NAMESPACE, "circle");
|
||||
circle.setAttribute("cx", "12");
|
||||
circle.setAttribute("cy", "12");
|
||||
circle.setAttribute("r", "3");
|
||||
svg.appendChild(circle);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
createMessageIcon(classNames = []) {
|
||||
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.setAttribute("focusable", "false");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "2");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
|
||||
classNames.forEach((className) => {
|
||||
if (className) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
});
|
||||
|
||||
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
|
||||
path.setAttribute(
|
||||
"d",
|
||||
"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
|
||||
);
|
||||
svg.appendChild(path);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
buildEngagementSection() {
|
||||
const hasPointer = this.pointerInfo && this.pointerInfo.key;
|
||||
const hasDiscussion =
|
||||
this.video.enableComments !== false &&
|
||||
(typeof this.video.discussionCount === "number" ||
|
||||
(typeof this.video.discussionCount === "string" &&
|
||||
this.video.discussionCount.trim()));
|
||||
|
||||
if (!hasPointer && !hasDiscussion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = this.document.createElement("div");
|
||||
container.classList.add(
|
||||
"flex",
|
||||
"items-center",
|
||||
"gap-4",
|
||||
"text-xs",
|
||||
"text-muted-strong",
|
||||
"mt-1.5",
|
||||
"video-card__engagement" // Reuse this class for consistent styling if global, otherwise it's just a marker
|
||||
);
|
||||
// Add specific class for scoping if needed, but Tailwind classes usually suffice.
|
||||
|
||||
// Views
|
||||
if (hasPointer) {
|
||||
const wrapper = this.document.createElement("div");
|
||||
wrapper.classList.add("flex", "items-center", "gap-1.5");
|
||||
wrapper.setAttribute("title", "Views");
|
||||
|
||||
const icon = this.createEyeIcon(["w-3.5", "h-3.5"]);
|
||||
wrapper.appendChild(icon);
|
||||
|
||||
const view = this.document.createElement("span");
|
||||
view.classList.add("view-count-text");
|
||||
view.textContent = "–";
|
||||
view.dataset.viewCount = "";
|
||||
view.dataset.viewPointer = this.pointerInfo.key;
|
||||
|
||||
wrapper.appendChild(view);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
this.viewCountEl = view;
|
||||
}
|
||||
|
||||
// Discussion
|
||||
if (hasDiscussion) {
|
||||
let initialCount = 0;
|
||||
if (typeof this.video.discussionCount === "number") {
|
||||
initialCount = this.video.discussionCount;
|
||||
} else if (typeof this.video.discussionCount === "string") {
|
||||
const parsed = Number.parseInt(this.video.discussionCount.trim(), 10);
|
||||
if (Number.isFinite(parsed)) {
|
||||
initialCount = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(initialCount) && initialCount >= 0) {
|
||||
const wrapper = this.document.createElement("div");
|
||||
wrapper.classList.add("flex", "items-center", "gap-1.5");
|
||||
wrapper.dataset.discussionCount = this.video.id;
|
||||
wrapper.dataset.countState = "ready";
|
||||
wrapper.setAttribute("title", "Comments");
|
||||
|
||||
const icon = this.createMessageIcon(["w-3.5", "h-3.5"]);
|
||||
wrapper.appendChild(icon);
|
||||
|
||||
const displayValue = initialCount.toLocaleString();
|
||||
|
||||
const valueEl = this.document.createElement("span");
|
||||
valueEl.textContent = displayValue;
|
||||
valueEl.dataset.discussionCountValue = "";
|
||||
|
||||
wrapper.appendChild(valueEl);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
this.discussionCountEl = wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
applyPointerDatasets() {
|
||||
if (!this.root || !this.pointerInfo) {
|
||||
return;
|
||||
|
||||
+139
-72
@@ -476,9 +476,9 @@ export class VideoCard {
|
||||
content.appendChild(badgesContainer);
|
||||
}
|
||||
|
||||
const discussion = this.buildDiscussionCount();
|
||||
if (discussion) {
|
||||
content.appendChild(discussion);
|
||||
const engagement = this.buildEngagementSection();
|
||||
if (engagement) {
|
||||
content.appendChild(engagement);
|
||||
}
|
||||
|
||||
if (this.showUnsupportedTorrentBadge) {
|
||||
@@ -926,30 +926,7 @@ export class VideoCard {
|
||||
metadata.appendChild(timeEl);
|
||||
this.timestampEl = timeEl;
|
||||
|
||||
if (this.pointerInfo && this.pointerInfo.key) {
|
||||
const dotClassNames = ["text-muted-strong"];
|
||||
if (!isCompact) {
|
||||
dotClassNames.push("mx-1");
|
||||
}
|
||||
|
||||
const dot = this.createElement("span", {
|
||||
classNames: dotClassNames,
|
||||
textContent: "•"
|
||||
});
|
||||
dot.setAttribute("aria-hidden", "true");
|
||||
|
||||
const view = this.createElement("span", {
|
||||
classNames: ["view-count-text"],
|
||||
textContent: "– views"
|
||||
});
|
||||
view.dataset.viewCount = "";
|
||||
view.dataset.viewPointer = this.pointerInfo.key;
|
||||
|
||||
metadata.appendChild(dot);
|
||||
metadata.appendChild(view);
|
||||
|
||||
this.viewCountEl = view;
|
||||
}
|
||||
// (View count moved to engagement section)
|
||||
|
||||
if (
|
||||
this.postedAt !== null &&
|
||||
@@ -2330,62 +2307,152 @@ export class VideoCard {
|
||||
}
|
||||
}
|
||||
|
||||
buildDiscussionCount() {
|
||||
if (this.variant === "compact") {
|
||||
createEyeIcon(classNames = []) {
|
||||
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.setAttribute("focusable", "false");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "2");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
|
||||
classNames.forEach((className) => {
|
||||
if (className) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
});
|
||||
|
||||
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
|
||||
path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");
|
||||
svg.appendChild(path);
|
||||
|
||||
const circle = this.document.createElementNS(SVG_NAMESPACE, "circle");
|
||||
circle.setAttribute("cx", "12");
|
||||
circle.setAttribute("cy", "12");
|
||||
circle.setAttribute("r", "3");
|
||||
svg.appendChild(circle);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
createMessageIcon(classNames = []) {
|
||||
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.setAttribute("focusable", "false");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "2");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
|
||||
classNames.forEach((className) => {
|
||||
if (className) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
});
|
||||
|
||||
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
|
||||
path.setAttribute(
|
||||
"d",
|
||||
"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
|
||||
);
|
||||
svg.appendChild(path);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
buildEngagementSection() {
|
||||
// Check availability
|
||||
const hasPointer = this.pointerInfo && this.pointerInfo.key;
|
||||
const hasDiscussion =
|
||||
this.variant !== "compact" &&
|
||||
this.video.enableComments !== false &&
|
||||
(typeof this.video.discussionCount === "number" ||
|
||||
(typeof this.video.discussionCount === "string" &&
|
||||
this.video.discussionCount.trim()));
|
||||
|
||||
if (!hasPointer && !hasDiscussion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.video.enableComments === false) {
|
||||
return null;
|
||||
const container = this.createElement("div", {
|
||||
classNames: [
|
||||
"flex",
|
||||
"items-center",
|
||||
"gap-4",
|
||||
"text-xs",
|
||||
"text-muted-strong",
|
||||
"mt-3",
|
||||
"video-card__engagement",
|
||||
],
|
||||
});
|
||||
|
||||
// Views
|
||||
if (hasPointer) {
|
||||
const wrapper = this.createElement("div", {
|
||||
classNames: ["flex", "items-center", "gap-1.5"],
|
||||
});
|
||||
wrapper.setAttribute("title", "Views");
|
||||
|
||||
const icon = this.createEyeIcon(["w-4", "h-4"]);
|
||||
wrapper.appendChild(icon);
|
||||
|
||||
const view = this.createElement("span", {
|
||||
classNames: ["view-count-text"],
|
||||
textContent: "–",
|
||||
});
|
||||
view.dataset.viewCount = "";
|
||||
view.dataset.viewPointer = this.pointerInfo.key;
|
||||
|
||||
wrapper.appendChild(view);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
this.viewCountEl = view;
|
||||
}
|
||||
|
||||
let initialCount = null;
|
||||
if (typeof this.video.discussionCount === "number") {
|
||||
initialCount = this.video.discussionCount;
|
||||
} else if (typeof this.video.discussionCount === "string") {
|
||||
const trimmed = this.video.discussionCount.trim();
|
||||
if (trimmed) {
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
// Discussion
|
||||
if (hasDiscussion) {
|
||||
let initialCount = 0;
|
||||
if (typeof this.video.discussionCount === "number") {
|
||||
initialCount = this.video.discussionCount;
|
||||
} else if (typeof this.video.discussionCount === "string") {
|
||||
const parsed = Number.parseInt(this.video.discussionCount.trim(), 10);
|
||||
if (Number.isFinite(parsed)) {
|
||||
initialCount = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(initialCount) && initialCount >= 0) {
|
||||
const wrapper = this.createElement("div", {
|
||||
classNames: ["flex", "items-center", "gap-1.5"],
|
||||
});
|
||||
wrapper.dataset.discussionCount = this.video.id;
|
||||
wrapper.dataset.countState = "ready";
|
||||
wrapper.setAttribute("title", "Comments");
|
||||
|
||||
const icon = this.createMessageIcon(["w-4", "h-4"]);
|
||||
wrapper.appendChild(icon);
|
||||
|
||||
const safeCount = Math.floor(initialCount);
|
||||
const displayValue = this.helpers.toLocaleString
|
||||
? this.helpers.toLocaleString(safeCount)
|
||||
: safeCount.toLocaleString();
|
||||
|
||||
const valueEl = this.createElement("span", {
|
||||
textContent: displayValue,
|
||||
});
|
||||
valueEl.dataset.discussionCountValue = "";
|
||||
|
||||
wrapper.appendChild(valueEl);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
this.discussionCountEl = wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
initialCount === null ||
|
||||
!Number.isFinite(initialCount) ||
|
||||
initialCount < 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const safeCount = Math.floor(initialCount);
|
||||
const displayValue = this.helpers.toLocaleString
|
||||
? this.helpers.toLocaleString(safeCount)
|
||||
: safeCount.toLocaleString();
|
||||
|
||||
const container = this.createElement("div", {
|
||||
classNames: ["flex", "items-center", "text-xs", "text-muted-strong"]
|
||||
});
|
||||
container.dataset.discussionCount = this.video.id;
|
||||
container.dataset.countState = "ready";
|
||||
|
||||
const valueEl = this.createElement("span", {
|
||||
textContent: displayValue
|
||||
});
|
||||
valueEl.dataset.discussionCountValue = "";
|
||||
|
||||
const labelEl = this.createElement("span", {
|
||||
classNames: ["ml-1"],
|
||||
textContent: "notes"
|
||||
});
|
||||
|
||||
container.appendChild(valueEl);
|
||||
container.appendChild(labelEl);
|
||||
|
||||
this.discussionCountEl = container;
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
@@ -3959,8 +3959,17 @@ export class VideoModal {
|
||||
}
|
||||
|
||||
updateViewCountLabel(text) {
|
||||
if (this.videoViewCountEl) {
|
||||
this.videoViewCountEl.textContent = text || "";
|
||||
if (!this.videoViewCountEl) {
|
||||
return;
|
||||
}
|
||||
const textEl = this.videoViewCountEl.querySelector(
|
||||
"[data-view-count-text]"
|
||||
);
|
||||
if (textEl) {
|
||||
textEl.textContent = text || "–";
|
||||
} else {
|
||||
// Fallback if span is missing
|
||||
this.videoViewCountEl.textContent = text || "–";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5438,15 +5447,14 @@ export class VideoModal {
|
||||
return this.formatViewCountLabel(Number(total));
|
||||
}
|
||||
if (status === "hydrating") {
|
||||
return "Loading views…";
|
||||
return "Loading…";
|
||||
}
|
||||
return "– views";
|
||||
return "–";
|
||||
}
|
||||
|
||||
formatViewCountLabel(total) {
|
||||
const numeric = Number.isFinite(total) ? Math.max(0, Number(total)) : 0;
|
||||
const label = numeric === 1 ? "view" : "views";
|
||||
return `${formatViewCount(numeric)} ${label}`;
|
||||
return formatViewCount(numeric);
|
||||
}
|
||||
|
||||
syncMoreMenuData({ currentVideo, canManageBlacklist }) {
|
||||
|
||||
Reference in new issue
Block a user