Merge pull request #359 from PR0M3TH3AN/codex/update-video-card-highlight-styles

Highlight owner-only private videos
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-08 11:31:55 -04:00
commit ccf65f7ccd
6 files changed
+383 -36

No files matched your search

+55
View File
@@ -199,6 +199,59 @@
</div>
</div>
<div class="space-y-1">
<span id="editVideoVisibilityLabel" class="block text-sm font-medium text-gray-200">
Visibility
</span>
<div
class="space-y-3 rounded-md border border-gray-800 bg-black/30 p-3"
role="group"
aria-labelledby="editVideoVisibilityLabel"
data-visibility-toggle
>
<div class="flex flex-wrap items-center gap-2 sm:justify-between">
<div
class="video-visibility-toggle"
data-visibility-options
>
<button
type="button"
class="video-visibility-toggle__option"
data-visibility-option="public"
aria-pressed="false"
>
Public
</button>
<button
type="button"
class="video-visibility-toggle__option"
data-visibility-option="private"
aria-pressed="false"
>
Private
</button>
</div>
<input
type="checkbox"
id="editVideoIsPrivate"
class="hidden"
data-default-checked="false"
aria-hidden="true"
/>
</div>
<p class="text-xs text-gray-400" data-visibility-helper>
Private notes stay visible only to you. Switch back to public when you're ready to share.
</p>
<button
type="button"
class="edit-field-button hidden"
data-edit-target="editVideoIsPrivate"
>
Edit field
</button>
</div>
</div>
<div class="space-y-1">
<label for="editEnableComments" class="block text-sm font-medium text-gray-200">
Enable comments
@@ -211,6 +264,8 @@
type="checkbox"
id="editEnableComments"
class="h-5 w-5 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500"
data-default-checked="true"
checked
/>
<span class="text-sm text-gray-300">
Allow replies once comment threads are available.
+61
View File
@@ -8,6 +8,7 @@
--color-surface: #0f172a;
--color-bg: var(--color-surface);
--color-card: #1e293b;
--color-card-private: #3b1d74;
--color-primary: #fe0032;
--color-secondary: #ff93a5;
--color-card-critical: #3f0b13;
@@ -167,6 +168,66 @@ header img {
box-shadow: var(--shadow-md);
}
.video-card.video-card--owner-private {
background-color: var(--color-card-private) !important;
box-shadow: 0 0 0 0.125rem rgba(168, 85, 247, 0.55), var(--shadow-md);
}
.video-card.video-card--owner-private:hover {
box-shadow: 0 0 0 0.125rem rgba(192, 132, 252, 0.65), var(--shadow-lg);
}
.video-visibility-toggle {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border-radius: 9999px;
border: 1px solid rgba(30, 41, 59, 0.7);
background-color: rgba(15, 23, 42, 0.55);
padding: 0.35rem;
}
.video-visibility-toggle__option {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid transparent;
background-color: rgba(15, 23, 42, 0.6);
color: rgba(203, 213, 225, 0.78);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.08em;
min-width: 5.5rem;
padding: 0.4rem 0.95rem;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.18s ease, color 0.18s ease,
border-color 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
}
.video-visibility-toggle__option:not(.is-disabled):hover {
background-color: rgba(148, 163, 184, 0.16);
color: rgba(226, 232, 240, 0.88);
}
.video-visibility-toggle__option[data-active="true"] {
background: linear-gradient(135deg, rgba(129, 140, 248, 0.4), rgba(192, 132, 252, 0.32));
color: #f8fafc;
border-color: rgba(129, 140, 248, 0.6);
box-shadow: 0 0 0 1px rgba(129, 140, 248, 0.4) inset,
0 6px 14px -6px rgba(129, 140, 248, 0.55);
}
.video-visibility-toggle__option.is-disabled {
opacity: 0.45;
cursor: not-allowed;
}
.video-visibility-toggle__option.is-disabled[data-active="true"] {
opacity: 0.65;
}
.video-card[data-owner-is-viewer="false"][data-url-health-state="offline"][data-stream-health-state="unhealthy"] {
display: none;
}
+4
View File
@@ -1819,6 +1819,9 @@ function renderChannelVideosFromList({
? app.formatTimeAgo(video.created_at)
: new Date(video.created_at * 1000).toLocaleString();
const highlightClass =
canEdit && video.isPrivate ? "video-card--owner-private" : "";
const videoCard = new VideoCard({
document,
video,
@@ -1826,6 +1829,7 @@ function renderChannelVideosFromList({
shareUrl,
pointerInfo,
timeAgo,
highlightClass,
capabilities: {
canEdit,
canDelete: canEdit,
+259 -32
View File
@@ -48,9 +48,15 @@ export class EditModal {
this.submitButton = null;
this.fieldButtons = [];
this.fields = {};
this.visibility = {
container: null,
buttons: [],
};
this.activeVideo = null;
this.isVisible = false;
this.eventsBound = false;
this.loadPromise = null;
}
addEventListener(type, listener, options) {
@@ -74,39 +80,54 @@ export class EditModal {
return this.root;
}
const targetContainer =
container || this.container || document.getElementById("modalContainer");
if (!targetContainer) {
throw new Error("Modal container element not found!");
if (this.loadPromise) {
return this.loadPromise;
}
let modal = targetContainer.querySelector("#editVideoModal");
if (!modal) {
const response = await fetch("components/edit-video-modal.html");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
this.loadPromise = (async () => {
const targetContainer =
container || this.container || document.getElementById("modalContainer");
if (!targetContainer) {
throw new Error("Modal container element not found!");
}
const html = await response.text();
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
this.removeTrackingScripts(wrapper);
targetContainer.appendChild(wrapper);
modal = wrapper.querySelector("#editVideoModal");
let modal = targetContainer.querySelector("#editVideoModal");
if (!modal) {
const response = await fetch("components/edit-video-modal.html");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
this.removeTrackingScripts(wrapper);
targetContainer.appendChild(wrapper);
modal = wrapper.querySelector("#editVideoModal");
}
if (!modal) {
throw new Error("Edit video modal markup missing after load.");
}
this.container = targetContainer;
this.root = modal;
this.cacheElements(modal);
if (!this.eventsBound) {
this.bindEvents();
this.eventsBound = true;
}
this.reset();
return this.root;
})();
try {
return await this.loadPromise;
} finally {
this.loadPromise = null;
}
if (!modal) {
throw new Error("Edit video modal markup missing after load.");
}
this.container = targetContainer;
this.root = modal;
this.cacheElements(modal);
this.bindEvents();
this.reset();
return this.root;
}
cacheElements(context) {
@@ -126,8 +147,22 @@ export class EditModal {
xs: context.querySelector("#editVideoXs") || null,
thumbnail: context.querySelector("#editVideoThumbnail") || null,
description: context.querySelector("#editVideoDescription") || null,
isPrivate: context.querySelector("#editVideoIsPrivate") || null,
enableComments: context.querySelector("#editEnableComments") || null,
};
this.visibility = {
container: context.querySelector("[data-visibility-toggle]") || null,
buttons: Array.from(
context.querySelectorAll("[data-visibility-option]") || []
),
helper: context.querySelector("[data-visibility-helper]") || null,
helperDefault: "",
};
if (this.visibility.helper) {
this.visibility.helperDefault = this.visibility.helper.textContent || "";
}
}
bindEvents() {
@@ -157,9 +192,31 @@ export class EditModal {
if (Array.isArray(this.fieldButtons)) {
this.fieldButtons.forEach((button) => {
if (!button || button.dataset.editListenerAttached === "true") {
return;
}
button.addEventListener("click", (event) => {
this.handleEditFieldToggle(event);
});
button.dataset.editListenerAttached = "true";
});
}
if (Array.isArray(this.visibility?.buttons)) {
this.visibility.buttons.forEach((button) => {
button.addEventListener("click", (event) => {
const option = event?.currentTarget?.dataset?.visibilityOption;
if (!option) {
return;
}
this.setVisibility(option);
});
});
}
if (this.fields.isPrivate) {
this.fields.isPrivate.addEventListener("change", () => {
this.handleIsPrivateChange({ emit: true });
});
}
@@ -189,12 +246,23 @@ export class EditModal {
return;
}
if (input.type === "checkbox") {
input.checked = true;
const defaultAttr = input.dataset?.defaultChecked;
const defaultChecked =
defaultAttr === "true"
? true
: defaultAttr === "false"
? false
: input.defaultChecked;
input.checked = defaultChecked;
input.disabled = false;
input.removeAttribute("disabled");
delete input.dataset.isEditing;
} else {
input.value = "";
input.readOnly = false;
input.removeAttribute("readonly");
input.classList.remove("locked-input");
delete input.dataset.isEditing;
}
delete input.dataset.originalValue;
});
@@ -207,6 +275,8 @@ export class EditModal {
});
}
this.handleIsPrivateChange({ emit: false });
this.activeVideo = null;
}
@@ -225,12 +295,14 @@ export class EditModal {
const effectiveXs = video.xs || magnetHints.xs || "";
const enableCommentsValue =
typeof video.enableComments === "boolean" ? video.enableComments : true;
const isPrivateValue = video.isPrivate === true;
const editContext = {
...video,
ws: effectiveWs,
xs: effectiveXs,
enableComments: enableCommentsValue,
isPrivate: isPrivateValue,
};
this.applyVideoToForm(editContext);
@@ -258,6 +330,7 @@ export class EditModal {
xs: editContext.xs || "",
thumbnail: editContext.thumbnail || "",
description: editContext.description || "",
isPrivate: editContext.isPrivate,
enableComments: editContext.enableComments,
};
@@ -287,7 +360,13 @@ export class EditModal {
const boolValue = rawValue === true;
input.checked = boolValue;
input.disabled = hasValue;
if (hasValue) {
input.setAttribute("disabled", "disabled");
} else {
input.removeAttribute("disabled");
}
input.dataset.originalValue = boolValue ? "true" : "false";
input.dataset.isEditing = hasValue ? "false" : "true";
if (button) {
if (hasValue) {
button.classList.remove("hidden");
@@ -307,11 +386,14 @@ export class EditModal {
input.value = value;
input.dataset.originalValue = value;
input.dataset.isEditing = hasValue ? "false" : "true";
if (hasValue) {
input.readOnly = true;
input.setAttribute("readonly", "readonly");
input.classList.add("locked-input");
} else {
input.readOnly = false;
input.removeAttribute("readonly");
input.classList.remove("locked-input");
}
@@ -327,6 +409,8 @@ export class EditModal {
}
}
});
this.handleIsPrivateChange({ emit: false });
}
fieldIdForKey(key) {
@@ -345,6 +429,8 @@ export class EditModal {
return "editVideoThumbnail";
case "description":
return "editVideoDescription";
case "isPrivate":
return "editVideoIsPrivate";
case "enableComments":
return "editEnableComments";
default:
@@ -385,15 +471,22 @@ export class EditModal {
return;
}
const mode = button.dataset.mode || "locked";
const isCheckbox = input.type === "checkbox";
const isLocked = isCheckbox
? input.disabled === true
: input.readOnly === true;
if (mode === "locked") {
if (isLocked) {
if (isCheckbox) {
input.disabled = false;
input.removeAttribute("disabled");
input.dataset.isEditing = "true";
} else {
input.disabled = false;
input.readOnly = false;
input.removeAttribute("readonly");
input.classList.remove("locked-input");
input.dataset.isEditing = "true";
}
button.dataset.mode = "editing";
button.textContent = "Restore original";
@@ -408,6 +501,9 @@ export class EditModal {
}
}
}
if (input === this.fields.isPrivate) {
this.updateVisibilityToggleUI();
}
return;
}
@@ -416,8 +512,13 @@ export class EditModal {
if (isCheckbox) {
input.checked = originalValue === "true";
input.disabled = true;
input.setAttribute("disabled", "disabled");
input.dataset.isEditing = "false";
button.dataset.mode = "locked";
button.textContent = "Edit field";
if (input === this.fields.isPrivate) {
this.updateVisibilityToggleUI();
}
return;
}
@@ -425,18 +526,121 @@ export class EditModal {
if (originalValue) {
input.readOnly = true;
input.setAttribute("readonly", "readonly");
input.classList.add("locked-input");
input.dataset.isEditing = "false";
button.dataset.mode = "locked";
button.textContent = "Edit field";
} else {
input.readOnly = false;
input.removeAttribute("readonly");
input.classList.remove("locked-input");
input.dataset.isEditing = "true";
button.classList.add("hidden");
button.dataset.mode = "locked";
button.textContent = "Edit field";
}
}
setVisibility(option, { emit = true } = {}) {
const checkbox = this.fields.isPrivate;
if (!checkbox) {
return;
}
const normalized = typeof option === "string" ? option.toLowerCase() : "";
if (normalized !== "public" && normalized !== "private") {
return;
}
if (checkbox.disabled) {
this.updateVisibilityToggleUI();
return;
}
const wantPrivate = normalized === "private";
if (checkbox.checked === wantPrivate) {
this.updateVisibilityToggleUI();
return;
}
checkbox.checked = wantPrivate;
this.handleIsPrivateChange({ emit });
}
updateVisibilityToggleUI() {
const checkbox = this.fields.isPrivate;
const buttons = Array.isArray(this.visibility?.buttons)
? this.visibility.buttons
: [];
if (!checkbox) {
return;
}
const isPrivate = checkbox.checked === true;
const disabled = checkbox.disabled === true;
buttons.forEach((button) => {
if (!button) {
return;
}
const option = button.dataset?.visibilityOption || "";
const normalized = option.toLowerCase();
const isActive =
(normalized === "private" && isPrivate) ||
(normalized === "public" && !isPrivate);
button.dataset.active = isActive ? "true" : "false";
button.setAttribute("aria-pressed", isActive ? "true" : "false");
if (disabled) {
button.classList.add("is-disabled");
button.setAttribute("aria-disabled", "true");
button.tabIndex = -1;
} else {
button.classList.remove("is-disabled");
button.removeAttribute("aria-disabled");
button.tabIndex = 0;
}
});
if (this.visibility?.container) {
this.visibility.container.dataset.state = isPrivate ? "private" : "public";
if (disabled) {
this.visibility.container.dataset.disabled = "true";
} else {
delete this.visibility.container.dataset.disabled;
}
}
const helper = this.visibility?.helper;
if (helper) {
const privateCopy = this.visibility.helperDefault || helper.textContent;
const publicCopy =
"Public notes appear in feeds for everyone who can view your channel.";
helper.textContent = isPrivate ? privateCopy : publicCopy;
}
}
handleIsPrivateChange({ emit = true } = {}) {
const checkbox = this.fields.isPrivate;
if (!checkbox) {
return;
}
this.updateVisibilityToggleUI();
if (!emit) {
return;
}
const value = this.sanitizers.checkbox(checkbox.checked);
const detail = {
field: "isPrivate",
value,
videoId: this.escapeHtml(this.activeVideo?.id || ""),
};
this.emit("video:edit-visibility-change", detail);
}
submit() {
if (!this.activeVideo || !this.root) {
this.showError("No video selected for editing.");
@@ -467,6 +671,7 @@ export class EditModal {
const thumbnailInput = this.fields.thumbnail;
const descriptionInput = this.fields.description;
const commentsInput = this.fields.enableComments;
const privateInput = this.fields.isPrivate;
const newTitle = fieldValue("title");
const newUrl = fieldValue("url");
@@ -476,7 +681,18 @@ export class EditModal {
const newThumbnail = fieldValue("thumbnail");
const newDescription = fieldValue("description");
const isEditing = (input) => !input || input.readOnly === false;
const isEditing = (input) => {
if (!input) {
return true;
}
if (input.dataset?.isEditing === "true") {
return true;
}
if (input.type === "checkbox") {
return input.disabled === false;
}
return input.readOnly === false;
};
const titleWasEdited = isEditing(titleInput);
const urlWasEdited = isEditing(urlInput);
@@ -497,6 +713,7 @@ export class EditModal {
: original.description || "";
const originalEnableComments =
typeof original.enableComments === "boolean" ? original.enableComments : true;
const originalIsPrivate = original.isPrivate === true;
let finalEnableComments = originalEnableComments;
if (commentsInput) {
@@ -507,6 +724,15 @@ export class EditModal {
}
}
let finalIsPrivate = originalIsPrivate;
if (privateInput) {
if (privateInput.disabled) {
finalIsPrivate = privateInput.dataset.originalValue === "true";
} else {
finalIsPrivate = this.sanitizers.checkbox(privateInput.checked);
}
}
if (!finalTitle || (!finalUrl && !finalMagnet)) {
this.showError("Title and at least one of URL or Magnet is required.");
return;
@@ -546,6 +772,7 @@ export class EditModal {
urlEdited: urlWasEdited,
magnetEdited: magnetWasEdited,
enableComments: finalEnableComments,
isPrivate: finalIsPrivate,
};
const originalEvent = {
+2 -1
View File
@@ -28,7 +28,8 @@ export class VideoCard {
this.video = video;
this.index = index;
this.shareUrl = shareUrl;
this.highlightClass = highlightClass || "";
this.highlightClass =
typeof highlightClass === "string" ? highlightClass : "";
this.animationClass = animationClass || "";
this.pointerInfo = pointerInfo;
this.capabilities = {
+2 -3
View File
@@ -321,9 +321,8 @@ export class VideoListView {
const shareUrl = this.buildShareUrl(video, shareBase);
const canEdit = this.utils.canEditVideo(video);
const canDelete = this.utils.canDeleteVideo(video);
const highlightClass = canEdit && video.isPrivate
? "border-2 border-yellow-500"
: "border-none";
const highlightClass =
canEdit && video.isPrivate ? "video-card--owner-private" : "";
const isNewlyRendered = !previouslyRenderedIds.has(video.id);
const animationClass = isNewlyRendered ? "video-card--enter" : "";
const timeAgo = this.formatters.formatTimeAgo(video.created_at);