Add modal-driven video edit flow

This commit is contained in:
thePR0M3TH3AN committed 2025-09-27 21:28:20 -04:00
1 parent b5987c5923
commit 1b4bf696b7
3 files changed
+534 -79

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
<!-- components/edit-video-modal.html -->
<div
id="editVideoModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
>
<div
id="editVideoModalOverlay"
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div
class="relative modal-container h-full w-full flex items-start md:items-center justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-4xl my-0 rounded-lg overflow-y-auto relative"
style="max-height: 90vh"
>
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent transition-transform duration-150 p-4 flex items-center justify-between"
>
<h2 class="text-xl font-bold text-white">Edit Video Note</h2>
<button
id="closeEditVideoModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="p-6 space-y-6">
<p class="text-sm text-gray-400">
Fields that already have values are locked. Click <strong>Edit field</strong> to make changes.
</p>
<form id="editVideoForm" class="space-y-5">
<div class="space-y-1">
<label for="editVideoTitle" class="block text-sm font-medium text-gray-200">
Title
</label>
<div class="flex flex-col gap-2 sm:flex-row">
<input
type="text"
id="editVideoTitle"
class="flex-1 rounded-md border border-gray-700 bg-gray-800 text-gray-100 focus:border-blue-500 focus:ring-blue-500"
placeholder="Video title"
/>
<button
type="button"
class="edit-field-button hidden"
data-edit-target="editVideoTitle"
>
Edit field
</button>
</div>
</div>
<div class="space-y-1">
<label for="editVideoUrl" class="block text-sm font-medium text-gray-200">
Hosted video URL (https)
</label>
<div class="flex flex-col gap-2 sm:flex-row">
<input
type="url"
id="editVideoUrl"
class="flex-1 rounded-md border border-gray-700 bg-gray-800 text-gray-100 focus:border-blue-500 focus:ring-blue-500"
placeholder="https://cdn.example.com/video.mp4"
/>
<button
type="button"
class="edit-field-button hidden"
data-edit-target="editVideoUrl"
>
Edit field
</button>
</div>
</div>
<div class="space-y-1">
<label for="editVideoMagnet" class="block text-sm font-medium text-gray-200">
Magnet link
</label>
<div class="flex flex-col gap-2 sm:flex-row">
<input
type="text"
id="editVideoMagnet"
class="flex-1 rounded-md border border-gray-700 bg-gray-800 text-gray-100 focus:border-blue-500 focus:ring-blue-500"
placeholder="magnet:?xt=urn:btih:..."
/>
<button
type="button"
class="edit-field-button hidden"
data-edit-target="editVideoMagnet"
>
Edit field
</button>
</div>
</div>
<div class="space-y-1">
<label for="editVideoThumbnail" class="block text-sm font-medium text-gray-200">
Thumbnail URL
</label>
<div class="flex flex-col gap-2 sm:flex-row">
<input
type="url"
id="editVideoThumbnail"
class="flex-1 rounded-md border border-gray-700 bg-gray-800 text-gray-100 focus:border-blue-500 focus:ring-blue-500"
placeholder="https://cdn.example.com/preview.jpg"
/>
<button
type="button"
class="edit-field-button hidden"
data-edit-target="editVideoThumbnail"
>
Edit field
</button>
</div>
</div>
<div class="space-y-1">
<label for="editVideoDescription" class="block text-sm font-medium text-gray-200">
Description
</label>
<div class="flex flex-col gap-2">
<textarea
id="editVideoDescription"
rows="4"
class="w-full rounded-md border border-gray-700 bg-gray-800 text-gray-100 focus:border-blue-500 focus:ring-blue-500"
placeholder="What is this video about?"
></textarea>
<button
type="button"
class="edit-field-button self-start hidden"
data-edit-target="editVideoDescription"
>
Edit field
</button>
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:justify-end">
<button
type="button"
id="cancelEditVideo"
class="w-full sm:w-auto px-4 py-2 rounded-md bg-gray-800 text-gray-200 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-gray-500"
>
Cancel
</button>
<button
type="submit"
id="submitEditVideo"
class="w-full sm:w-auto px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-blue-500"
>
Save changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
+31
View File
@@ -705,3 +705,34 @@ footer a:hover {
align-items: center;
display: flex;
}
.edit-field-button {
align-items: center;
background-color: #1f2937;
border-radius: 0.5rem;
color: #e2e8f0;
display: inline-flex;
font-size: 0.75rem;
font-weight: 600;
justify-content: center;
padding: 0.5rem 0.85rem;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.edit-field-button:hover,
.edit-field-button:focus {
background-color: #334155;
color: #f8fafc;
transform: translateY(-1px);
}
.locked-input {
background-color: rgba(31, 41, 55, 0.75) !important;
border-color: rgba(55, 65, 81, 0.9) !important;
color: #94a3b8 !important;
cursor: default;
}
.locked-input:focus {
outline: none;
box-shadow: none;
}
+323 -79
View File
@@ -539,6 +539,16 @@ class bitvidApp {
this.r2ZoneIdInput = null;
this.r2BaseDomainInput = null;
// Edit video modal elements
this.editVideoModal = null;
this.editVideoOverlay = null;
this.editVideoForm = null;
this.closeEditVideoModalBtn = null;
this.cancelEditVideoBtn = null;
this.editVideoSubmitBtn = null;
this.editVideoFieldButtons = [];
this.activeEditVideo = null;
// Optional small inline player stats
this.status = document.getElementById("status") || null;
this.progressBar = document.getElementById("progress") || null;
@@ -817,6 +827,9 @@ class bitvidApp {
await this.initUploadModal();
initQuickR2Upload(this);
// 2.5 Initialize the edit modal (components/edit-video-modal.html)
await this.initEditVideoModal();
// 3. (Optional) Initialize the profile modal (components/profile-modal.html)
await this.initProfileModal();
@@ -1280,6 +1293,241 @@ class bitvidApp {
}
}
async initEditVideoModal() {
try {
let modal = document.getElementById("editVideoModal");
if (!modal) {
const resp = await fetch("components/edit-video-modal.html");
if (!resp.ok) {
throw new Error(`HTTP error! status: ${resp.status}`);
}
const html = await resp.text();
const modalContainer = document.getElementById("modalContainer");
if (!modalContainer) {
throw new Error("Modal container element not found!");
}
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
removeTrackingScripts(wrapper);
modalContainer.appendChild(wrapper);
modal = wrapper.querySelector("#editVideoModal");
}
if (!modal) {
throw new Error("Edit video modal markup missing after load.");
}
this.editVideoModal = modal;
this.editVideoOverlay =
modal.querySelector("#editVideoModalOverlay") || null;
this.editVideoForm = modal.querySelector("#editVideoForm") || null;
this.closeEditVideoModalBtn =
modal.querySelector("#closeEditVideoModal") || null;
this.cancelEditVideoBtn =
modal.querySelector("#cancelEditVideo") || null;
this.editVideoSubmitBtn =
modal.querySelector("#submitEditVideo") || null;
this.editVideoFieldButtons = Array.from(
modal.querySelectorAll("[data-edit-target]")
);
if (this.editVideoForm) {
this.editVideoForm.addEventListener("submit", (event) => {
event.preventDefault();
this.handleEditVideoSubmit();
});
}
if (this.closeEditVideoModalBtn) {
this.closeEditVideoModalBtn.addEventListener("click", () => {
this.hideEditVideoModal();
});
}
if (this.cancelEditVideoBtn) {
this.cancelEditVideoBtn.addEventListener("click", () => {
this.hideEditVideoModal();
});
}
if (this.editVideoOverlay) {
this.editVideoOverlay.addEventListener("click", () => {
this.hideEditVideoModal();
});
}
if (Array.isArray(this.editVideoFieldButtons)) {
this.editVideoFieldButtons.forEach((btn) => {
btn.addEventListener("click", (event) => {
this.handleEditFieldToggle(event);
});
});
}
this.resetEditVideoForm();
return true;
} catch (error) {
console.error("initEditVideoModal failed:", error);
this.showError(`Failed to initialize edit modal: ${error.message}`);
return false;
}
}
resetEditVideoForm() {
if (!this.editVideoModal) {
this.activeEditVideo = null;
return;
}
const fieldIds = [
"editVideoTitle",
"editVideoUrl",
"editVideoMagnet",
"editVideoThumbnail",
"editVideoDescription",
];
fieldIds.forEach((id) => {
const input = this.editVideoModal.querySelector(`#${id}`);
if (input) {
input.value = "";
input.readOnly = false;
input.classList.remove("locked-input");
delete input.dataset.originalValue;
}
const button = this.editVideoModal.querySelector(
`[data-edit-target="${id}"]`
);
if (button) {
button.classList.add("hidden");
button.dataset.mode = "locked";
button.textContent = "Edit field";
}
});
this.activeEditVideo = null;
}
showEditVideoModal() {
if (this.editVideoModal) {
this.editVideoModal.classList.remove("hidden");
}
}
hideEditVideoModal() {
if (this.editVideoModal) {
this.editVideoModal.classList.add("hidden");
}
this.resetEditVideoForm();
}
populateEditVideoForm(video) {
if (!video || !this.editVideoModal) {
return;
}
this.resetEditVideoForm();
const fieldMap = {
editVideoTitle: video.title || "",
editVideoUrl: video.url || "",
editVideoMagnet: video.magnet || "",
editVideoThumbnail: video.thumbnail || "",
editVideoDescription: video.description || "",
};
Object.entries(fieldMap).forEach(([id, rawValue]) => {
const input = this.editVideoModal.querySelector(`#${id}`);
const button = this.editVideoModal.querySelector(
`[data-edit-target="${id}"]`
);
const value = typeof rawValue === "string" ? rawValue : "";
const hasValue = value.trim().length > 0;
if (input) {
input.value = value;
input.dataset.originalValue = value;
if (hasValue) {
input.readOnly = true;
input.classList.add("locked-input");
} else {
input.readOnly = false;
input.classList.remove("locked-input");
}
}
if (button) {
if (hasValue) {
button.classList.remove("hidden");
button.dataset.mode = "locked";
button.textContent = "Edit field";
} else {
button.classList.add("hidden");
button.dataset.mode = "locked";
button.textContent = "Edit field";
}
}
});
this.activeEditVideo = video;
}
handleEditFieldToggle(event) {
const button = event?.currentTarget;
if (!button || !this.editVideoModal) {
return;
}
const targetId = button.dataset?.editTarget;
if (!targetId) {
return;
}
const input = this.editVideoModal.querySelector(`#${targetId}`);
if (!input) {
return;
}
const mode = button.dataset.mode || "locked";
if (mode === "locked") {
input.readOnly = false;
input.classList.remove("locked-input");
button.dataset.mode = "editing";
button.textContent = "Restore original";
if (typeof input.focus === "function") {
input.focus();
if (typeof input.setSelectionRange === "function") {
const length = input.value.length;
try {
input.setSelectionRange(length, length);
} catch (err) {
// ignore selection errors (e.g. for input types that do not support it)
}
}
}
return;
}
const originalValue = input.dataset?.originalValue || "";
input.value = originalValue;
if (originalValue) {
input.readOnly = true;
input.classList.add("locked-input");
button.dataset.mode = "locked";
button.textContent = "Edit field";
} else {
input.readOnly = false;
input.classList.remove("locked-input");
button.classList.add("hidden");
button.dataset.mode = "locked";
button.textContent = "Edit field";
}
}
setUploadMode(mode) {
const normalized = mode === "cloudflare" ? "cloudflare" : "custom";
this.activeUploadMode = normalized;
@@ -2519,6 +2767,73 @@ class bitvidApp {
}
}
async handleEditVideoSubmit() {
if (!this.activeEditVideo || !this.editVideoModal) {
this.showError("No video selected for editing.");
return;
}
const fieldValue = (id) => {
const el = this.editVideoModal.querySelector(`#${id}`);
if (!el || typeof el.value !== "string") {
return "";
}
return el.value.trim();
};
const original = this.activeEditVideo;
const newTitle = fieldValue("editVideoTitle");
const newUrl = fieldValue("editVideoUrl");
const newMagnet = fieldValue("editVideoMagnet");
const newThumbnail = fieldValue("editVideoThumbnail");
const newDescription = fieldValue("editVideoDescription");
const finalTitle = newTitle || original.title || "";
const finalUrl = newUrl || original.url || "";
const finalMagnet = newMagnet || original.magnet || "";
const finalThumbnail = newThumbnail || original.thumbnail || "";
const finalDescription = newDescription || original.description || "";
if (!finalTitle || (!finalUrl && !finalMagnet)) {
this.showError("Title and at least one of URL or Magnet is required.");
return;
}
if (finalUrl && !/^https:\/\//i.test(finalUrl)) {
this.showError("Hosted video URLs must use HTTPS.");
return;
}
const updatedData = {
version: original.version || 2,
title: finalTitle,
magnet: finalMagnet,
url: finalUrl,
thumbnail: finalThumbnail,
description: finalDescription,
mode: isDevMode ? "dev" : "live",
};
const originalEvent = {
id: original.id,
pubkey: original.pubkey,
videoRootId: original.videoRootId,
};
try {
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
await this.loadVideos();
this.videosMap.clear();
this.showSuccess("Video updated successfully!");
this.hideEditVideoModal();
this.forceRefreshAllProfiles();
} catch (err) {
console.error("Failed to edit video:", err);
this.showError("Failed to edit video. Please try again.");
}
}
/**
* Actually handle the upload form submission.
*/
@@ -4339,88 +4654,17 @@ class bitvidApp {
return;
}
// 3) Prompt the user for updated fields
const newTitle = prompt("New Title? (blank=keep existing)", video.title);
if (newTitle === null) {
if (!this.editVideoModal) {
await this.initEditVideoModal();
}
if (!this.editVideoModal) {
this.showError("Edit modal is not available right now.");
return;
}
const newMagnet = prompt(
"New Magnet? (blank=keep existing)",
video.magnet
);
if (newMagnet === null) {
return;
}
const newUrl = prompt(
"New URL? (blank=keep existing)",
video.url || ""
);
if (newUrl === null) {
return;
}
const newThumb = prompt(
"New Thumbnail? (blank=keep existing)",
video.thumbnail
);
if (newThumb === null) {
return;
}
const newDesc = prompt(
"New Description? (blank=keep existing)",
video.description
);
if (newDesc === null) {
return;
}
// const wantPrivate = confirm("Make this video private? OK=Yes, Cancel=No");
// 4) Build final updated fields (or fallback to existing)
const title =
!newTitle || !newTitle.trim() ? video.title : newTitle.trim();
const magnet =
!newMagnet || !newMagnet.trim() ? video.magnet : newMagnet.trim();
const url = !newUrl || !newUrl.trim() ? video.url : newUrl.trim();
const thumbnail =
!newThumb || !newThumb.trim() ? video.thumbnail : newThumb.trim();
const description =
!newDesc || !newDesc.trim() ? video.description : newDesc.trim();
// 5) Create an object with the new data
const updatedData = {
version: video.version || 2,
// isPrivate: wantPrivate,
title,
magnet,
url,
thumbnail,
description,
mode: isDevMode ? "dev" : "live",
};
// 6) Build the originalEvent stub, now including videoRootId to avoid extra fetch
const originalEvent = {
id: video.id,
pubkey: video.pubkey,
videoRootId: video.videoRootId, // <-- pass this if it exists
};
// 7) Call the editVideo method
await nostrClient.editVideo(originalEvent, updatedData, this.pubkey);
// 8) Refresh local UI
await this.loadVideos();
// 8.1) Purge the outdated cache
this.videosMap.clear();
this.showSuccess("Video updated successfully!");
// 9) Also refresh all profile caches so any new name/pic changes are reflected
this.forceRefreshAllProfiles();
this.populateEditVideoForm(video);
this.showEditVideoModal();
} catch (err) {
console.error("Failed to edit video:", err);
this.showError("Failed to edit video. Please try again.");