diff --git a/js/app.js b/js/app.js index 5d2ec8b7..f1b141e3 100644 --- a/js/app.js +++ b/js/app.js @@ -44,6 +44,7 @@ import { removeTrackingScripts, } from "./utils/domUtils.js"; import { VideoModal } from "./ui/components/VideoModal.js"; +import { UploadModal } from "./ui/components/UploadModal.js"; import { getPubkey as getStoredPubkey, setPubkey as setStoredPubkey, @@ -456,48 +457,27 @@ class bitvidApp { this.profileSwitcherSelectionPubkey = null; this.currentUserNpub = null; - // Upload modal elements + // Upload modal component this.uploadButton = document.getElementById("uploadButton") || null; - this.uploadModal = document.getElementById("uploadModal") || null; - this.closeUploadModalBtn = - document.getElementById("closeUploadModal") || null; - this.uploadForm = document.getElementById("uploadForm") || null; - this.uploadEnableCommentsInput = null; - this.uploadModeToggleButtons = []; - this.customUploadSection = null; - this.cloudflareUploadSection = null; - this.activeUploadMode = "custom"; this.r2Service = r2Service; - this.cloudflareSettings = this.r2Service.getSettings(); - this.cloudflareSettingsForm = null; - this.cloudflareClearSettingsButton = null; - this.cloudflareSettingsStatus = null; - this.cloudflareBucketPreview = null; - this.cloudflareUploadForm = null; - this.cloudflareFileInput = null; - this.cloudflareUploadButton = null; - this.cloudflareUploadStatus = null; - this.cloudflareProgressBar = null; - this.cloudflareProgressFill = null; - this.cloudflareTitleInput = null; - this.cloudflareDescriptionInput = null; - this.cloudflareThumbnailInput = null; - this.cloudflareMagnetInput = null; - this.cloudflareWsInput = null; - this.cloudflareXsInput = null; - this.cloudflareEnableCommentsInput = null; - this.cloudflareAdvancedToggle = null; - this.cloudflareAdvancedToggleLabel = null; - this.cloudflareAdvancedToggleIcon = null; - this.cloudflareAdvancedFields = null; - this.cloudflareAdvancedVisible = this.r2Service.getCloudflareAdvancedVisibility(); - this.r2AccountIdInput = null; - this.r2AccessKeyIdInput = null; - this.r2SecretAccessKeyInput = null; - this.r2ApiTokenInput = null; - this.r2ZoneIdInput = null; - this.r2BaseDomainInput = null; - this.r2ServiceUnsubscribes = []; + const uploadModalEvents = new EventTarget(); + this.uploadModal = new UploadModal({ + authService: this.authService, + r2Service: this.r2Service, + publishVideoNote: (payload, options) => + this.publishVideoNote(payload, options), + removeTrackingScripts, + setGlobalModalState, + showError: (message) => this.showError(message), + showSuccess: (message) => this.showSuccess(message), + getCurrentPubkey: () => this.pubkey, + safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey), + eventTarget: uploadModalEvents, + container: document.getElementById("modalContainer") || null, + }); + this.uploadModal.addEventListener("upload:submit", (event) => { + this.handleUploadSubmitEvent(event); + }); // Edit video modal elements this.editVideoModal = null; @@ -783,7 +763,12 @@ class bitvidApp { } // 2. Initialize the upload modal (components/upload-modal.html) - await this.initUploadModal(); + try { + await this.uploadModal.load(); + } catch (error) { + console.error("initUploadModal failed:", error); + this.showError(`Failed to initialize upload modal: ${error.message}`); + } initQuickR2Upload(this); // 2.5 Initialize the edit modal (components/edit-video-modal.html) @@ -935,294 +920,6 @@ class bitvidApp { return this.videoModal.forceRemovePoster(reason); } - /** - * Initialize the upload modal (upload-modal.html). - */ - async initUploadModal() { - try { - const resp = await fetch("components/upload-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!"); - } - // Append the upload modal markup - const wrapper = document.createElement("div"); - wrapper.innerHTML = html; - removeTrackingScripts(wrapper); - modalContainer.appendChild(wrapper); - - // Grab references - this.uploadModal = document.getElementById("uploadModal") || null; - this.closeUploadModalBtn = - document.getElementById("closeUploadModal") || null; - this.uploadForm = document.getElementById("uploadForm") || null; - this.uploadEnableCommentsInput = - document.getElementById("uploadEnableComments") || null; - this.uploadModeToggleButtons = Array.from( - document.querySelectorAll(".upload-mode-toggle[data-upload-mode]") - ); - this.customUploadSection = - document.getElementById("customUploadSection") || null; - this.cloudflareUploadSection = - document.getElementById("cloudflareUploadSection") || null; - this.cloudflareSettingsForm = - document.getElementById("cloudflareSettingsForm") || null; - this.cloudflareClearSettingsButton = - document.getElementById("cloudflareClearSettings") || null; - this.cloudflareSettingsStatus = - document.getElementById("cloudflareSettingsStatus") || null; - this.cloudflareBucketPreview = - document.getElementById("cloudflareBucketPreview") || null; - this.cloudflareUploadForm = - document.getElementById("cloudflareUploadForm") || null; - this.cloudflareFileInput = - document.getElementById("cloudflareFile") || null; - this.cloudflareUploadButton = - document.getElementById("cloudflareUploadButton") || null; - this.cloudflareUploadStatus = - document.getElementById("cloudflareUploadStatus") || null; - this.cloudflareProgressBar = - document.getElementById("cloudflareProgressBar") || null; - this.cloudflareProgressFill = - document.getElementById("cloudflareProgressFill") || null; - this.cloudflareTitleInput = - document.getElementById("cloudflareTitle") || null; - this.cloudflareDescriptionInput = - document.getElementById("cloudflareDescription") || null; - this.cloudflareThumbnailInput = - document.getElementById("cloudflareThumbnail") || null; - this.cloudflareMagnetInput = - document.getElementById("cloudflareMagnet") || null; - this.cloudflareWsInput = - document.getElementById("cloudflareWs") || null; - this.cloudflareXsInput = - document.getElementById("cloudflareXs") || null; - this.cloudflareEnableCommentsInput = - document.getElementById("cloudflareEnableComments") || null; - this.cloudflareAdvancedToggle = - document.getElementById("cloudflareAdvancedToggle") || null; - this.cloudflareAdvancedToggleLabel = - document.getElementById("cloudflareAdvancedToggleLabel") || null; - this.cloudflareAdvancedToggleIcon = - document.getElementById("cloudflareAdvancedToggleIcon") || null; - this.cloudflareAdvancedFields = - document.getElementById("cloudflareAdvancedFields") || null; - this.r2AccountIdInput = - document.getElementById("r2AccountId") || null; - this.r2AccessKeyIdInput = - document.getElementById("r2AccessKeyId") || null; - this.r2SecretAccessKeyInput = - document.getElementById("r2SecretAccessKey") || null; - this.r2ApiTokenInput = - document.getElementById("r2ApiToken") || null; - this.r2ZoneIdInput = document.getElementById("r2ZoneId") || null; - this.r2BaseDomainInput = - document.getElementById("r2BaseDomain") || null; - - // Optional: if close button found, wire up - if (this.closeUploadModalBtn) { - this.closeUploadModalBtn.addEventListener("click", () => { - if (this.uploadModal) { - this.uploadModal.classList.add("hidden"); - setGlobalModalState("upload", false); - } - }); - } - // If the form is found, wire up - if (this.uploadForm) { - this.uploadForm.addEventListener("submit", (e) => { - e.preventDefault(); - this.handleUploadSubmit(); - }); - } - - if (Array.isArray(this.r2ServiceUnsubscribes)) { - this.r2ServiceUnsubscribes.forEach((fn) => { - try { - if (typeof fn === "function") { - fn(); - } - } catch (err) { - console.warn("Failed to unsubscribe from R2 service event:", err); - } - }); - } - this.r2ServiceUnsubscribes = []; - - const registerSubscription = (unsubscribe) => { - if (typeof unsubscribe === "function") { - this.r2ServiceUnsubscribes.push(unsubscribe); - } - }; - - registerSubscription( - this.r2Service.on("advancedVisibilityChange", ({ visible } = {}) => { - this.renderCloudflareAdvancedVisibility(visible); - }) - ); - - registerSubscription( - this.r2Service.on("settingsStatus", ({ message, variant } = {}) => { - this.applyCloudflareStatus( - this.cloudflareSettingsStatus, - message, - variant - ); - }) - ); - - registerSubscription( - this.r2Service.on("uploadStatus", ({ message, variant } = {}) => { - this.applyCloudflareStatus( - this.cloudflareUploadStatus, - message, - variant - ); - }) - ); - - registerSubscription( - this.r2Service.on("uploadStateChange", ({ isUploading } = {}) => { - this.renderCloudflareUploadingState(isUploading); - }) - ); - - registerSubscription( - this.r2Service.on("uploadProgress", ({ fraction } = {}) => { - this.updateCloudflareProgress(fraction); - }) - ); - - registerSubscription( - this.r2Service.on("settingsPopulated", ({ settings } = {}) => { - this.fillCloudflareSettingsInputs(settings); - }) - ); - - registerSubscription( - this.r2Service.on("settingsChanged", ({ settings } = {}) => { - this.cloudflareSettings = settings || this.cloudflareSettings; - }) - ); - - registerSubscription( - this.r2Service.on("bucketPreview", (detail = {}) => { - this.renderCloudflareBucketPreview(detail); - }) - ); - - if (this.cloudflareSettingsForm) { - this.cloudflareSettingsForm.addEventListener("submit", async (e) => { - e.preventDefault(); - try { - const formValues = this.collectCloudflareSettingsFormValues(); - const saved = await this.r2Service.saveSettings(formValues); - if (saved) { - await this.refreshCloudflareBucketPreview(); - } - } catch (err) { - console.error("Failed to save Cloudflare settings:", err); - } - }); - } - - if (this.cloudflareClearSettingsButton) { - this.cloudflareClearSettingsButton.addEventListener("click", async () => { - try { - const cleared = await this.r2Service.clearSettings(); - if (cleared) { - await this.refreshCloudflareBucketPreview(); - } - } catch (err) { - console.error("Failed to clear Cloudflare settings:", err); - } - }); - } - - if (this.cloudflareUploadForm) { - this.cloudflareUploadForm.addEventListener("submit", async (event) => { - event.preventDefault(); - if (!this.pubkey) { - this.showError("Please login to post a video."); - return; - } - - const npub = this.safeEncodeNpub(this.pubkey) || ""; - const file = this.cloudflareFileInput?.files?.[0] || null; - const metadata = { - title: (this.cloudflareTitleInput?.value || "").trim(), - description: (this.cloudflareDescriptionInput?.value || "").trim(), - thumbnail: (this.cloudflareThumbnailInput?.value || "").trim(), - magnet: (this.cloudflareMagnetInput?.value || "").trim(), - ws: (this.cloudflareWsInput?.value || "").trim(), - xs: (this.cloudflareXsInput?.value || "").trim(), - enableComments: this.cloudflareEnableCommentsInput - ? this.cloudflareEnableCommentsInput.checked - : true, - }; - - try { - await this.r2Service.uploadVideo({ - npub, - file, - metadata, - settingsInput: this.collectCloudflareSettingsFormValues(), - publishVideoNote: (payload, options) => - this.publishVideoNote(payload, options), - onReset: () => this.resetCloudflareUploadForm(), - }); - } catch (err) { - console.error("Cloudflare upload failed:", err); - } - }); - } - - if (this.uploadModeToggleButtons.length > 0) { - this.uploadModeToggleButtons.forEach((btn) => { - btn.addEventListener("click", () => { - const mode = btn.dataset.uploadMode || "custom"; - this.setUploadMode(mode); - }); - }); - } - - if (this.cloudflareAdvancedToggle) { - this.cloudflareAdvancedToggle.addEventListener("click", () => { - this.r2Service.setCloudflareAdvancedVisibility( - !this.r2Service.getCloudflareAdvancedVisibility() - ); - }); - } - - this.cloudflareSettings = this.r2Service.getSettings(); - this.renderCloudflareAdvancedVisibility( - this.r2Service.getCloudflareAdvancedVisibility() - ); - - try { - await this.r2Service.loadSettings(); - } catch (err) { - // errors already reported by the service - } - - await this.refreshCloudflareBucketPreview(); - - this.setUploadMode(this.activeUploadMode); - - console.log("Upload modal initialization successful"); - return true; - } catch (error) { - console.error("initUploadModal failed:", error); - this.showError(`Failed to initialize upload modal: ${error.message}`); - return false; - } - } - async initEditVideoModal() { try { let modal = document.getElementById("editVideoModal"); @@ -2540,214 +2237,6 @@ class bitvidApp { } } - setUploadMode(mode) { - const normalized = mode === "cloudflare" ? "cloudflare" : "custom"; - this.activeUploadMode = normalized; - - if (this.customUploadSection) { - if (normalized === "custom") { - this.customUploadSection.classList.remove("hidden"); - } else { - this.customUploadSection.classList.add("hidden"); - } - } - - if (this.cloudflareUploadSection) { - if (normalized === "cloudflare") { - this.cloudflareUploadSection.classList.remove("hidden"); - } else { - this.cloudflareUploadSection.classList.add("hidden"); - } - } - - if (Array.isArray(this.uploadModeToggleButtons)) { - this.uploadModeToggleButtons.forEach((btn) => { - if (!btn || !btn.dataset) { - return; - } - - const isActive = btn.dataset.uploadMode === normalized; - btn.classList.toggle("bg-blue-500", isActive); - btn.classList.toggle("text-white", isActive); - btn.classList.toggle("shadow", isActive); - btn.classList.toggle("text-gray-300", !isActive); - btn.setAttribute("aria-pressed", isActive ? "true" : "false"); - }); - } - - if (normalized === "cloudflare") { - this.refreshCloudflareBucketPreview(); - } - } - - collectCloudflareSettingsFormValues() { - return { - accountId: (this.r2AccountIdInput?.value || "").trim(), - accessKeyId: (this.r2AccessKeyIdInput?.value || "").trim(), - secretAccessKey: (this.r2SecretAccessKeyInput?.value || "").trim(), - apiToken: (this.r2ApiTokenInput?.value || "").trim(), - zoneId: (this.r2ZoneIdInput?.value || "").trim(), - baseDomain: this.r2BaseDomainInput?.value || "", - }; - } - - applyCloudflareStatus(element, message = "", variant = "info") { - if (!element) { - return; - } - - element.textContent = message || ""; - element.classList.remove( - "text-green-400", - "text-red-400", - "text-yellow-400", - "text-gray-400" - ); - if (!message) { - element.classList.add("text-gray-400"); - return; - } - - let cls = "text-gray-400"; - if (variant === "success") { - cls = "text-green-400"; - } else if (variant === "error") { - cls = "text-red-400"; - } else if (variant === "warning") { - cls = "text-yellow-400"; - } - element.classList.add(cls); - } - - renderCloudflareAdvancedVisibility(visible) { - const isVisible = Boolean(visible); - this.cloudflareAdvancedVisible = isVisible; - - if (this.cloudflareAdvancedFields) { - if (isVisible) { - this.cloudflareAdvancedFields.classList.remove("hidden"); - } else { - this.cloudflareAdvancedFields.classList.add("hidden"); - } - } - - if (this.cloudflareAdvancedToggle) { - this.cloudflareAdvancedToggle.setAttribute( - "aria-expanded", - isVisible ? "true" : "false" - ); - } - - if (this.cloudflareAdvancedToggleLabel) { - this.cloudflareAdvancedToggleLabel.textContent = isVisible - ? "Hide advanced options" - : "Show advanced options"; - } - - if (this.cloudflareAdvancedToggleIcon) { - this.cloudflareAdvancedToggleIcon.classList.toggle("rotate-90", isVisible); - } - } - - renderCloudflareUploadingState(isUploading) { - if (this.cloudflareUploadButton) { - this.cloudflareUploadButton.disabled = Boolean(isUploading); - this.cloudflareUploadButton.textContent = isUploading - ? "Uploading…" - : "Upload to R2 & publish"; - } - - if (this.cloudflareFileInput) { - this.cloudflareFileInput.disabled = Boolean(isUploading); - } - - if (this.cloudflareEnableCommentsInput) { - this.cloudflareEnableCommentsInput.disabled = Boolean(isUploading); - } - } - - fillCloudflareSettingsInputs(settings) { - const data = settings || {}; - - if (this.r2AccountIdInput) { - this.r2AccountIdInput.value = data.accountId || ""; - } - if (this.r2AccessKeyIdInput) { - this.r2AccessKeyIdInput.value = data.accessKeyId || ""; - } - if (this.r2SecretAccessKeyInput) { - this.r2SecretAccessKeyInput.value = data.secretAccessKey || ""; - } - if (this.r2ApiTokenInput) { - this.r2ApiTokenInput.value = data.apiToken || ""; - } - if (this.r2ZoneIdInput) { - this.r2ZoneIdInput.value = data.zoneId || ""; - } - if (this.r2BaseDomainInput) { - this.r2BaseDomainInput.value = data.baseDomain || ""; - } - } - - renderCloudflareBucketPreview({ text = "", title = "" } = {}) { - if (!this.cloudflareBucketPreview) { - return; - } - - this.cloudflareBucketPreview.textContent = text || ""; - if (title) { - this.cloudflareBucketPreview.setAttribute("title", title); - } else { - this.cloudflareBucketPreview.removeAttribute("title"); - } - } - - async refreshCloudflareBucketPreview() { - if (!this.r2Service) { - return; - } - const hasPubkey = Boolean(this.pubkey); - const npub = hasPubkey ? this.safeEncodeNpub(this.pubkey) : ""; - await this.r2Service.updateCloudflareBucketPreview({ hasPubkey, npub }); - } - - updateCloudflareProgress(fraction) { - if (!this.cloudflareProgressBar || !this.cloudflareProgressFill) { - return; - } - - if (!Number.isFinite(fraction) || fraction < 0) { - this.cloudflareProgressBar.classList.add("hidden"); - this.cloudflareProgressBar.setAttribute("aria-hidden", "true"); - this.cloudflareProgressFill.style.width = "0%"; - this.cloudflareProgressFill.setAttribute("aria-valuenow", "0"); - return; - } - - const clamped = Math.max(0, Math.min(1, fraction)); - const percent = Math.round(clamped * 100); - - this.cloudflareProgressBar.classList.remove("hidden"); - this.cloudflareProgressBar.setAttribute("aria-hidden", "false"); - this.cloudflareProgressFill.style.width = `${percent}%`; - this.cloudflareProgressFill.setAttribute("aria-valuenow", `${percent}`); - } - - resetCloudflareUploadForm() { - if (this.cloudflareTitleInput) this.cloudflareTitleInput.value = ""; - if (this.cloudflareDescriptionInput) - this.cloudflareDescriptionInput.value = ""; - if (this.cloudflareThumbnailInput) - this.cloudflareThumbnailInput.value = ""; - if (this.cloudflareMagnetInput) this.cloudflareMagnetInput.value = ""; - if (this.cloudflareWsInput) this.cloudflareWsInput.value = ""; - if (this.cloudflareXsInput) this.cloudflareXsInput.value = ""; - if (this.cloudflareEnableCommentsInput) - this.cloudflareEnableCommentsInput.checked = true; - if (this.cloudflareFileInput) this.cloudflareFileInput.value = ""; - this.updateCloudflareProgress(Number.NaN); - } - renderSavedProfiles() { const fallbackAvatar = "assets/svg/default-profile.svg"; const normalizedActive = this.normalizeHexPubkey( @@ -4862,8 +4351,7 @@ class bitvidApp { if (this.uploadButton) { this.uploadButton.addEventListener("click", () => { if (this.uploadModal) { - this.uploadModal.classList.remove("hidden"); - setGlobalModalState("upload", true); + this.uploadModal.open(); } }); } @@ -5207,8 +4695,7 @@ class bitvidApp { await onSuccess(); } if (suppressModalClose !== true && this.uploadModal) { - this.uploadModal.classList.add("hidden"); - setGlobalModalState("upload", false); + this.uploadModal.close(); } await this.loadVideos(); this.showSuccess("Video shared successfully!"); @@ -5359,42 +4846,17 @@ class bitvidApp { /** * Actually handle the upload form submission. */ - async handleUploadSubmit() { - const titleEl = document.getElementById("uploadTitle"); - const urlEl = document.getElementById("uploadUrl"); - const magnetEl = document.getElementById("uploadMagnet"); - const wsEl = document.getElementById("uploadWs"); - const xsEl = document.getElementById("uploadXs"); - const thumbEl = document.getElementById("uploadThumbnail"); - const descEl = document.getElementById("uploadDescription"); - const privEl = document.getElementById("uploadIsPrivate"); - const commentsEl = document.getElementById("uploadEnableComments"); - - const payload = { - title: titleEl?.value.trim() || "", - url: urlEl?.value.trim() || "", - magnet: magnetEl?.value.trim() || "", - ws: wsEl?.value.trim() || "", - xs: xsEl?.value.trim() || "", - thumbnail: thumbEl?.value.trim() || "", - description: descEl?.value.trim() || "", - enableComments: commentsEl ? commentsEl.checked : true, - }; + async handleUploadSubmitEvent(event) { + const payload = event?.detail?.payload || {}; try { await this.authService.handleUploadSubmit(payload, { publish: (data) => this.publishVideoNote(data, { onSuccess: () => { - if (titleEl) titleEl.value = ""; - if (urlEl) urlEl.value = ""; - if (magnetEl) magnetEl.value = ""; - if (wsEl) wsEl.value = ""; - if (xsEl) xsEl.value = ""; - if (thumbEl) thumbEl.value = ""; - if (descEl) descEl.value = ""; - if (privEl) privEl.checked = false; - if (commentsEl) commentsEl.checked = true; + if (this.uploadModal?.resetCustomForm) { + this.uploadModal.resetCustomForm(); + } }, }), }); @@ -5528,7 +4990,9 @@ class bitvidApp { await this.loadVideos(); this.forceRefreshAllProfiles(); - await this.refreshCloudflareBucketPreview(); + if (this.uploadModal?.refreshCloudflareBucketPreview) { + await this.uploadModal.refreshCloudflareBucketPreview(); + } } async handleAuthLogout(detail = {}) { @@ -5587,7 +5051,9 @@ class bitvidApp { await this.loadVideos(); this.forceRefreshAllProfiles(); - await this.refreshCloudflareBucketPreview(); + if (this.uploadModal?.refreshCloudflareBucketPreview) { + await this.uploadModal.refreshCloudflareBucketPreview(); + } } handleProfileUpdated(detail = {}) { diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js new file mode 100644 index 00000000..8cddfed3 --- /dev/null +++ b/js/ui/components/UploadModal.js @@ -0,0 +1,711 @@ +export class UploadModal { + constructor({ + authService, + r2Service, + publishVideoNote, + removeTrackingScripts, + setGlobalModalState, + showError, + showSuccess, + getCurrentPubkey, + safeEncodeNpub, + eventTarget, + container, + } = {}) { + this.authService = authService || null; + this.r2Service = r2Service || null; + this.publishVideoNote = typeof publishVideoNote === "function" ? publishVideoNote : null; + this.removeTrackingScripts = + typeof removeTrackingScripts === "function" ? removeTrackingScripts : () => {}; + this.setGlobalModalState = + typeof setGlobalModalState === "function" ? setGlobalModalState : () => {}; + this.showError = typeof showError === "function" ? showError : () => {}; + this.showSuccess = typeof showSuccess === "function" ? showSuccess : () => {}; + this.getCurrentPubkey = typeof getCurrentPubkey === "function" ? getCurrentPubkey : null; + this.safeEncodeNpub = typeof safeEncodeNpub === "function" ? safeEncodeNpub : () => ""; + this.eventTarget = eventTarget instanceof EventTarget ? eventTarget : new EventTarget(); + this.container = container || null; + + this.root = null; + this.activeMode = "custom"; + this.isVisible = false; + this.cloudflareSettings = this.r2Service?.getSettings?.() || null; + this.cloudflareAdvancedVisible = + this.r2Service?.getCloudflareAdvancedVisibility?.() || false; + this.r2Unsubscribes = []; + + this.uploadModeButtons = []; + this.customSection = null; + this.cloudflareSection = null; + + this.customForm = null; + this.customFormInputs = {}; + + this.cloudflareSettingsForm = null; + this.cloudflareClearSettingsButton = null; + this.cloudflareSettingsStatus = null; + this.cloudflareBucketPreview = null; + this.cloudflareUploadForm = null; + this.cloudflareFileInput = null; + this.cloudflareUploadButton = null; + this.cloudflareUploadStatus = null; + this.cloudflareProgressBar = null; + this.cloudflareProgressFill = null; + this.cloudflareTitleInput = null; + this.cloudflareDescriptionInput = null; + this.cloudflareThumbnailInput = null; + this.cloudflareMagnetInput = null; + this.cloudflareWsInput = null; + this.cloudflareXsInput = null; + this.cloudflareEnableCommentsInput = null; + this.cloudflareAdvancedToggle = null; + this.cloudflareAdvancedToggleLabel = null; + this.cloudflareAdvancedToggleIcon = null; + this.cloudflareAdvancedFields = null; + this.r2AccountIdInput = null; + this.r2AccessKeyIdInput = null; + this.r2SecretAccessKeyInput = null; + this.r2ApiTokenInput = null; + this.r2ZoneIdInput = null; + this.r2BaseDomainInput = null; + } + + addEventListener(type, listener, options) { + if (!this.eventTarget) { + return; + } + this.eventTarget.addEventListener(type, listener, options); + } + + removeEventListener(type, listener, options) { + if (!this.eventTarget) { + return; + } + this.eventTarget.removeEventListener(type, listener, options); + } + + emit(type, detail) { + if (!this.eventTarget) { + return; + } + this.eventTarget.dispatchEvent(new CustomEvent(type, { detail })); + } + + getRoot() { + return this.root; + } + + async load({ container } = {}) { + if (this.root) { + return this.root; + } + + const targetContainer = + container || this.container || document.getElementById("modalContainer"); + if (!targetContainer) { + throw new Error("Modal container element not found!"); + } + + const response = await fetch("components/upload-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); + + this.container = targetContainer; + this.root = wrapper.querySelector("#uploadModal"); + if (!this.root) { + throw new Error("Upload modal markup missing after load."); + } + + this.cacheElements(wrapper); + this.bindEvents(); + this.registerR2Subscriptions(); + + this.renderCloudflareAdvancedVisibility( + this.r2Service?.getCloudflareAdvancedVisibility?.() + ); + + try { + await this.loadR2Settings(); + } catch (error) { + // Errors already surfaced via the service listeners. + } + + await this.refreshCloudflareBucketPreview(); + this.setMode(this.activeMode); + this.updateCloudflareProgress(Number.NaN); + + return this.root; + } + + cacheElements(context) { + this.uploadModeButtons = Array.from( + context.querySelectorAll(".upload-mode-toggle[data-upload-mode]") + ); + this.customSection = context.querySelector("#customUploadSection") || null; + this.cloudflareSection = context.querySelector("#cloudflareUploadSection") || null; + + this.customForm = context.querySelector("#uploadForm") || null; + this.customFormInputs = { + title: context.querySelector("#uploadTitle") || null, + url: context.querySelector("#uploadUrl") || null, + magnet: context.querySelector("#uploadMagnet") || null, + ws: context.querySelector("#uploadWs") || null, + xs: context.querySelector("#uploadXs") || null, + thumbnail: context.querySelector("#uploadThumbnail") || null, + description: context.querySelector("#uploadDescription") || null, + enableComments: context.querySelector("#uploadEnableComments") || null, + isPrivate: context.querySelector("#uploadIsPrivate") || null, + }; + + this.closeButton = context.querySelector("#closeUploadModal") || null; + + this.cloudflareSettingsForm = context.querySelector("#cloudflareSettingsForm") || null; + this.cloudflareClearSettingsButton = + context.querySelector("#cloudflareClearSettings") || null; + this.cloudflareSettingsStatus = + context.querySelector("#cloudflareSettingsStatus") || null; + this.cloudflareBucketPreview = + context.querySelector("#cloudflareBucketPreview") || null; + this.cloudflareUploadForm = context.querySelector("#cloudflareUploadForm") || null; + this.cloudflareFileInput = context.querySelector("#cloudflareFile") || null; + this.cloudflareUploadButton = context.querySelector("#cloudflareUploadButton") || null; + this.cloudflareUploadStatus = + context.querySelector("#cloudflareUploadStatus") || null; + this.cloudflareProgressBar = context.querySelector("#cloudflareProgressBar") || null; + this.cloudflareProgressFill = context.querySelector("#cloudflareProgressFill") || null; + this.cloudflareTitleInput = context.querySelector("#cloudflareTitle") || null; + this.cloudflareDescriptionInput = + context.querySelector("#cloudflareDescription") || null; + this.cloudflareThumbnailInput = + context.querySelector("#cloudflareThumbnail") || null; + this.cloudflareMagnetInput = context.querySelector("#cloudflareMagnet") || null; + this.cloudflareWsInput = context.querySelector("#cloudflareWs") || null; + this.cloudflareXsInput = context.querySelector("#cloudflareXs") || null; + this.cloudflareEnableCommentsInput = + context.querySelector("#cloudflareEnableComments") || null; + this.cloudflareAdvancedToggle = + context.querySelector("#cloudflareAdvancedToggle") || null; + this.cloudflareAdvancedToggleLabel = + context.querySelector("#cloudflareAdvancedToggleLabel") || null; + this.cloudflareAdvancedToggleIcon = + context.querySelector("#cloudflareAdvancedToggleIcon") || null; + this.cloudflareAdvancedFields = + context.querySelector("#cloudflareAdvancedFields") || null; + this.r2AccountIdInput = context.querySelector("#r2AccountId") || null; + this.r2AccessKeyIdInput = context.querySelector("#r2AccessKeyId") || null; + this.r2SecretAccessKeyInput = context.querySelector("#r2SecretAccessKey") || null; + this.r2ApiTokenInput = context.querySelector("#r2ApiToken") || null; + this.r2ZoneIdInput = context.querySelector("#r2ZoneId") || null; + this.r2BaseDomainInput = context.querySelector("#r2BaseDomain") || null; + } + + bindEvents() { + if (this.closeButton) { + this.closeButton.addEventListener("click", () => { + this.close(); + }); + } + + if (this.customForm) { + this.customForm.addEventListener("submit", (event) => { + event.preventDefault(); + this.handleCustomSubmit(); + }); + } + + if (Array.isArray(this.uploadModeButtons)) { + this.uploadModeButtons.forEach((button) => { + if (!button) return; + button.addEventListener("click", () => { + const mode = button.dataset.uploadMode || "custom"; + this.setMode(mode); + }); + }); + } + + if (this.cloudflareSettingsForm) { + this.cloudflareSettingsForm.addEventListener("submit", async (event) => { + event.preventDefault(); + await this.handleCloudflareSettingsSubmit(); + }); + } + + if (this.cloudflareClearSettingsButton) { + this.cloudflareClearSettingsButton.addEventListener("click", async () => { + await this.handleCloudflareClearSettings(); + }); + } + + if (this.cloudflareUploadForm) { + this.cloudflareUploadForm.addEventListener("submit", async (event) => { + event.preventDefault(); + await this.handleCloudflareUploadSubmit(); + }); + } + + if (this.cloudflareAdvancedToggle) { + this.cloudflareAdvancedToggle.addEventListener("click", () => { + if (this.r2Service?.setCloudflareAdvancedVisibility && + this.r2Service?.getCloudflareAdvancedVisibility) { + const nextState = !this.r2Service.getCloudflareAdvancedVisibility(); + this.r2Service.setCloudflareAdvancedVisibility(nextState); + } else { + this.renderCloudflareAdvancedVisibility(!this.cloudflareAdvancedVisible); + } + }); + } + } + + registerR2Subscriptions() { + if (!this.r2Service?.on) { + return; + } + + if (Array.isArray(this.r2Unsubscribes)) { + this.r2Unsubscribes.forEach((unsubscribe) => { + try { + if (typeof unsubscribe === "function") { + unsubscribe(); + } + } catch (error) { + console.warn("[UploadModal] Failed to remove R2 listener", error); + } + }); + } + this.r2Unsubscribes = []; + + const register = (unsubscribe) => { + if (typeof unsubscribe === "function") { + this.r2Unsubscribes.push(unsubscribe); + } + }; + + register( + this.r2Service.on("advancedVisibilityChange", ({ visible } = {}) => { + this.renderCloudflareAdvancedVisibility(visible); + }) + ); + + register( + this.r2Service.on("settingsStatus", ({ message, variant } = {}) => { + this.applyCloudflareStatus(this.cloudflareSettingsStatus, message, variant); + }) + ); + + register( + this.r2Service.on("uploadStatus", ({ message, variant } = {}) => { + this.applyCloudflareStatus(this.cloudflareUploadStatus, message, variant); + }) + ); + + register( + this.r2Service.on("uploadStateChange", ({ isUploading } = {}) => { + this.renderCloudflareUploadingState(isUploading); + }) + ); + + register( + this.r2Service.on("uploadProgress", ({ fraction } = {}) => { + this.updateCloudflareProgress(fraction); + }) + ); + + register( + this.r2Service.on("settingsPopulated", ({ settings } = {}) => { + this.fillCloudflareSettingsInputs(settings); + }) + ); + + register( + this.r2Service.on("settingsChanged", ({ settings } = {}) => { + this.cloudflareSettings = settings || this.cloudflareSettings; + }) + ); + + register( + this.r2Service.on("bucketPreview", (detail = {}) => { + this.renderCloudflareBucketPreview(detail); + }) + ); + } + + open() { + if (!this.root) { + return; + } + if (!this.root.classList.contains("hidden")) { + this.isVisible = true; + return; + } + this.root.classList.remove("hidden"); + this.isVisible = true; + this.setGlobalModalState("upload", true); + this.emit("upload:open", { mode: this.activeMode }); + } + + close() { + if (!this.root) { + return; + } + if (this.root.classList.contains("hidden")) { + this.isVisible = false; + return; + } + this.root.classList.add("hidden"); + this.isVisible = false; + this.setGlobalModalState("upload", false); + this.emit("upload:close", { mode: this.activeMode }); + } + + setMode(mode) { + const normalized = mode === "cloudflare" ? "cloudflare" : "custom"; + this.activeMode = normalized; + + if (this.customSection) { + if (normalized === "custom") { + this.customSection.classList.remove("hidden"); + } else { + this.customSection.classList.add("hidden"); + } + } + + if (this.cloudflareSection) { + if (normalized === "cloudflare") { + this.cloudflareSection.classList.remove("hidden"); + } else { + this.cloudflareSection.classList.add("hidden"); + } + } + + if (Array.isArray(this.uploadModeButtons)) { + this.uploadModeButtons.forEach((button) => { + if (!button?.dataset) { + return; + } + const isActive = button.dataset.uploadMode === normalized; + button.classList.toggle("bg-blue-500", isActive); + button.classList.toggle("text-white", isActive); + button.classList.toggle("shadow", isActive); + button.classList.toggle("text-gray-300", !isActive); + button.setAttribute("aria-pressed", isActive ? "true" : "false"); + }); + } + + if (normalized === "cloudflare") { + this.refreshCloudflareBucketPreview(); + } + } + + handleCustomSubmit() { + const payload = { + title: this.customFormInputs.title?.value?.trim() || "", + url: this.customFormInputs.url?.value?.trim() || "", + magnet: this.customFormInputs.magnet?.value?.trim() || "", + ws: this.customFormInputs.ws?.value?.trim() || "", + xs: this.customFormInputs.xs?.value?.trim() || "", + thumbnail: this.customFormInputs.thumbnail?.value?.trim() || "", + description: this.customFormInputs.description?.value?.trim() || "", + enableComments: this.customFormInputs.enableComments + ? !!this.customFormInputs.enableComments.checked + : true, + }; + + if (this.customFormInputs.isPrivate) { + payload.isPrivate = !!this.customFormInputs.isPrivate.checked; + } + + this.emit("upload:submit", { payload }); + } + + collectCloudflareSettingsFormValues() { + return { + accountId: this.r2AccountIdInput?.value?.trim() || "", + accessKeyId: this.r2AccessKeyIdInput?.value?.trim() || "", + secretAccessKey: this.r2SecretAccessKeyInput?.value?.trim() || "", + apiToken: this.r2ApiTokenInput?.value?.trim() || "", + zoneId: this.r2ZoneIdInput?.value?.trim() || "", + baseDomain: this.r2BaseDomainInput?.value || "", + }; + } + + applyCloudflareStatus(element, message = "", variant = "info") { + if (!element) { + return; + } + + element.textContent = message || ""; + element.classList.remove( + "text-green-400", + "text-red-400", + "text-yellow-400", + "text-gray-400" + ); + + if (!message) { + element.classList.add("text-gray-400"); + return; + } + + let className = "text-gray-400"; + if (variant === "success") { + className = "text-green-400"; + } else if (variant === "error") { + className = "text-red-400"; + } else if (variant === "warning") { + className = "text-yellow-400"; + } + element.classList.add(className); + } + + renderCloudflareAdvancedVisibility(visible) { + const isVisible = Boolean(visible); + this.cloudflareAdvancedVisible = isVisible; + + if (this.cloudflareAdvancedFields) { + if (isVisible) { + this.cloudflareAdvancedFields.classList.remove("hidden"); + } else { + this.cloudflareAdvancedFields.classList.add("hidden"); + } + } + + if (this.cloudflareAdvancedToggle) { + this.cloudflareAdvancedToggle.setAttribute( + "aria-expanded", + isVisible ? "true" : "false" + ); + } + + if (this.cloudflareAdvancedToggleLabel) { + this.cloudflareAdvancedToggleLabel.textContent = isVisible + ? "Hide advanced options" + : "Show advanced options"; + } + + if (this.cloudflareAdvancedToggleIcon) { + this.cloudflareAdvancedToggleIcon.classList.toggle("rotate-90", isVisible); + } + } + + renderCloudflareUploadingState(isUploading) { + if (this.cloudflareUploadButton) { + this.cloudflareUploadButton.disabled = Boolean(isUploading); + this.cloudflareUploadButton.textContent = isUploading + ? "Uploading…" + : "Upload to R2 & publish"; + } + + if (this.cloudflareFileInput) { + this.cloudflareFileInput.disabled = Boolean(isUploading); + } + + if (this.cloudflareEnableCommentsInput) { + this.cloudflareEnableCommentsInput.disabled = Boolean(isUploading); + } + } + + fillCloudflareSettingsInputs(settings) { + const data = settings || {}; + + if (this.r2AccountIdInput) { + this.r2AccountIdInput.value = data.accountId || ""; + } + if (this.r2AccessKeyIdInput) { + this.r2AccessKeyIdInput.value = data.accessKeyId || ""; + } + if (this.r2SecretAccessKeyInput) { + this.r2SecretAccessKeyInput.value = data.secretAccessKey || ""; + } + if (this.r2ApiTokenInput) { + this.r2ApiTokenInput.value = data.apiToken || ""; + } + if (this.r2ZoneIdInput) { + this.r2ZoneIdInput.value = data.zoneId || ""; + } + if (this.r2BaseDomainInput) { + this.r2BaseDomainInput.value = data.baseDomain || ""; + } + } + + renderCloudflareBucketPreview({ text = "", title = "" } = {}) { + if (!this.cloudflareBucketPreview) { + return; + } + + this.cloudflareBucketPreview.textContent = text || ""; + if (title) { + this.cloudflareBucketPreview.setAttribute("title", title); + } else { + this.cloudflareBucketPreview.removeAttribute("title"); + } + } + + async refreshCloudflareBucketPreview() { + if (!this.r2Service?.updateCloudflareBucketPreview) { + return; + } + + const pubkey = + (this.getCurrentPubkey && this.getCurrentPubkey()) || + (this.authService?.getActivePubkey?.() ?? null); + const hasPubkey = Boolean(pubkey); + const npub = hasPubkey ? this.safeEncodeNpub(pubkey) : ""; + + await this.r2Service.updateCloudflareBucketPreview({ hasPubkey, npub }); + } + + updateCloudflareProgress(fraction) { + if (!this.cloudflareProgressBar || !this.cloudflareProgressFill) { + this.emit("upload:r2-progress", { fraction: null }); + return; + } + + if (!Number.isFinite(fraction) || fraction < 0) { + this.cloudflareProgressBar.classList.add("hidden"); + this.cloudflareProgressBar.setAttribute("aria-hidden", "true"); + this.cloudflareProgressFill.style.width = "0%"; + this.cloudflareProgressFill.setAttribute("aria-valuenow", "0"); + this.emit("upload:r2-progress", { fraction: null }); + return; + } + + const clamped = Math.max(0, Math.min(1, fraction)); + const percent = Math.round(clamped * 100); + + this.cloudflareProgressBar.classList.remove("hidden"); + this.cloudflareProgressBar.setAttribute("aria-hidden", "false"); + this.cloudflareProgressFill.style.width = `${percent}%`; + this.cloudflareProgressFill.setAttribute("aria-valuenow", `${percent}`); + this.emit("upload:r2-progress", { fraction: clamped }); + } + + resetCloudflareUploadForm() { + if (this.cloudflareTitleInput) this.cloudflareTitleInput.value = ""; + if (this.cloudflareDescriptionInput) this.cloudflareDescriptionInput.value = ""; + if (this.cloudflareThumbnailInput) this.cloudflareThumbnailInput.value = ""; + if (this.cloudflareMagnetInput) this.cloudflareMagnetInput.value = ""; + if (this.cloudflareWsInput) this.cloudflareWsInput.value = ""; + if (this.cloudflareXsInput) this.cloudflareXsInput.value = ""; + if (this.cloudflareEnableCommentsInput) + this.cloudflareEnableCommentsInput.checked = true; + if (this.cloudflareFileInput) this.cloudflareFileInput.value = ""; + this.updateCloudflareProgress(Number.NaN); + } + + resetCustomForm() { + if (this.customFormInputs.title) this.customFormInputs.title.value = ""; + if (this.customFormInputs.url) this.customFormInputs.url.value = ""; + if (this.customFormInputs.magnet) this.customFormInputs.magnet.value = ""; + if (this.customFormInputs.ws) this.customFormInputs.ws.value = ""; + if (this.customFormInputs.xs) this.customFormInputs.xs.value = ""; + if (this.customFormInputs.thumbnail) this.customFormInputs.thumbnail.value = ""; + if (this.customFormInputs.description) + this.customFormInputs.description.value = ""; + if (this.customFormInputs.enableComments) + this.customFormInputs.enableComments.checked = true; + if (this.customFormInputs.isPrivate) + this.customFormInputs.isPrivate.checked = false; + } + + async loadR2Settings() { + if (!this.r2Service?.loadSettings) { + return null; + } + const settings = await this.r2Service.loadSettings(); + this.cloudflareSettings = settings; + return settings; + } + + async handleCloudflareSettingsSubmit() { + if (!this.r2Service?.saveSettings) { + return; + } + try { + const formValues = this.collectCloudflareSettingsFormValues(); + const saved = await this.r2Service.saveSettings(formValues); + if (saved) { + await this.refreshCloudflareBucketPreview(); + } + } catch (error) { + console.error("[UploadModal] Failed to save Cloudflare settings", error); + } + } + + async handleCloudflareClearSettings() { + if (!this.r2Service?.clearSettings) { + return; + } + try { + const cleared = await this.r2Service.clearSettings(); + if (cleared) { + await this.refreshCloudflareBucketPreview(); + } + } catch (error) { + console.error("[UploadModal] Failed to clear Cloudflare settings", error); + } + } + + async handleCloudflareUploadSubmit() { + if (!this.r2Service?.uploadVideo) { + return; + } + + const pubkey = + (this.getCurrentPubkey && this.getCurrentPubkey()) || + (this.authService?.getActivePubkey?.() ?? null); + if (!pubkey) { + this.showError("Please login to post a video."); + return; + } + + const npub = this.safeEncodeNpub(pubkey) || ""; + const file = this.cloudflareFileInput?.files?.[0] || null; + const metadata = { + title: this.cloudflareTitleInput?.value?.trim() || "", + description: this.cloudflareDescriptionInput?.value?.trim() || "", + thumbnail: this.cloudflareThumbnailInput?.value?.trim() || "", + magnet: this.cloudflareMagnetInput?.value?.trim() || "", + ws: this.cloudflareWsInput?.value?.trim() || "", + xs: this.cloudflareXsInput?.value?.trim() || "", + enableComments: this.cloudflareEnableCommentsInput + ? !!this.cloudflareEnableCommentsInput.checked + : true, + }; + + try { + await this.r2Service.uploadVideo({ + npub, + file, + metadata, + settingsInput: this.collectCloudflareSettingsFormValues(), + publishVideoNote: (payload, options) => + this.publishVideoNote ? this.publishVideoNote(payload, options) : null, + onReset: () => this.resetCloudflareUploadForm(), + }); + } catch (error) { + console.error("[UploadModal] Cloudflare upload failed", error); + } + } + + destroy() { + if (Array.isArray(this.r2Unsubscribes)) { + this.r2Unsubscribes.forEach((unsubscribe) => { + try { + if (typeof unsubscribe === "function") { + unsubscribe(); + } + } catch (error) { + console.warn("[UploadModal] Failed to cleanup R2 listener", error); + } + }); + } + this.r2Unsubscribes = []; + } +}