+
+
+
-
diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js
index 8d559a6a..5bb4e610 100644
--- a/js/ui/components/UploadModal.js
+++ b/js/ui/components/UploadModal.js
@@ -1,5 +1,4 @@
-// NOTE: Keep the Upload, Edit, and Revert modals in lockstep when updating NIP-71 form features.
-
+// components/UploadModal.js
import { createModalAccessibility } from "./modalAccessibility.js";
import { Nip71FormManager } from "./nip71FormManager.js";
import logger, { userLogger } from "../../utils/logger.js";
@@ -21,7 +20,7 @@ export class UploadModal {
getCurrentPubkey,
safeEncodeNpub,
eventTarget,
- container
+ container,
} = {}) {
this.authService = authService || null;
this.r2Service = r2Service || null;
@@ -47,183 +46,66 @@ export class UploadModal {
this.container = container || null;
this.root = null;
- this.activeMode = "custom";
this.isVisible = false;
+ this.activeSource = "upload"; // 'upload' | 'external'
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.customSubmitButton = null;
- this.customSubmitButtonDefaultLabel = "";
- this.customSubmitBlockedUntil = 0;
- this.customSubmitCooldownTimer = null;
- this.customSubmitCooldownMs = 60000;
-
- this.customAdvancedToggle = null;
- this.customAdvancedFields = null;
-
- 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.cloudflareProgress = null;
- this.cloudflareProgressStatus = null;
- this.cloudflareTitleInput = null;
- this.cloudflareDescriptionInput = null;
- this.cloudflareThumbnailInput = null;
- this.cloudflareMagnetInput = null;
- this.cloudflareWsInput = null;
- this.cloudflareXsInput = null;
- this.cloudflareEnableCommentsInput = null;
- this.cloudflareIsNsfwInput = null;
- this.cloudflareIsForKidsInput = null;
- this.cloudflareAdvancedToggle = null;
- this.cloudflareAdvancedToggleLabel = null;
- this.cloudflareAdvancedToggleIcon = null;
- this.cloudflareAdvancedFields = null;
- this.cloudflareUploadAdvancedToggle = null;
- this.cloudflareUploadAdvancedFields = null;
- this.r2AccountIdInput = null;
- this.r2AccessKeyIdInput = null;
- this.r2SecretAccessKeyInput = null;
- this.r2ApiTokenInput = null;
- this.r2ZoneIdInput = null;
- this.r2BaseDomainInput = null;
+ // UI References
+ this.form = null;
+ this.modeButtons = {};
+ this.sourceSections = {};
+ this.inputs = {};
+ this.toggles = {};
+ this.submitButton = null;
+ this.submitStatus = null;
+ // Logic/State
this.nip71FormManager = new Nip71FormManager();
+ this.r2Unsubscribes = [];
this.cleanupHandlers = [];
this.modalAccessibility = null;
- this.modalBackdrop = null;
- this.modalPanel = null;
this.loadPromise = null;
- this.eventsBound = false;
- // Automation State
- this.customSummaryLocked = true;
- this.cloudflareSummaryLocked = true;
- this.customWsDirty = false;
- this.cloudflareWsDirty = false;
+ // Automation
+ this.summaryLocked = true;
+ this.isUploading = false;
}
- 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;
- }
+ // --- Core Lifecycle ---
async load({ container } = {}) {
- if (this.root) {
- if (this.root.isConnected) {
- return this.root;
- }
- this.root = null;
- this.eventsBound = false;
- }
-
- if (this.loadPromise) {
- return this.loadPromise;
- }
+ if (this.root && this.root.isConnected) return this.root;
+ if (this.loadPromise) return this.loadPromise;
this.loadPromise = (async () => {
const targetContainer =
container || this.container || document.getElementById("modalContainer");
- if (!targetContainer) {
- throw new Error("Modal container element not found!");
- }
+ if (!targetContainer) throw new Error("Modal container not found!");
- const existingModals = targetContainer.querySelectorAll("#uploadModal");
- let modal = existingModals[0] || null;
- if (existingModals.length > 1) {
- existingModals.forEach((node, index) => {
- if (index > 0) {
- node.remove();
- }
- });
- }
+ // Cleanup existing
+ targetContainer.querySelectorAll("#uploadModal").forEach(n => n.remove());
- if (!modal) {
- 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 response = await fetch("components/upload-modal.html");
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
- const doc = targetContainer.ownerDocument || document;
- const wrapper = doc.createElement("div");
- wrapper.innerHTML = html;
- this.removeTrackingScripts(wrapper);
+ const html = await response.text();
+ const doc = targetContainer.ownerDocument || document;
+ const wrapper = doc.createElement("div");
+ wrapper.innerHTML = html;
+ this.removeTrackingScripts(wrapper);
- const fragment = doc.createDocumentFragment();
- while (wrapper.firstChild) {
- fragment.appendChild(wrapper.firstChild);
- }
- targetContainer.appendChild(fragment);
-
- modal = targetContainer.querySelector("#uploadModal");
- }
-
- if (!modal) {
- throw new Error("Upload modal markup missing after load.");
- }
-
- const previousRoot = this.root;
+ targetContainer.appendChild(wrapper.firstElementChild);
+ this.root = targetContainer.querySelector("#uploadModal");
this.container = targetContainer;
- this.root = modal;
- if (previousRoot && previousRoot !== modal) {
- this.eventsBound = false;
- }
- this.cacheElements(modal);
+ this.cacheElements();
+ this.bindEvents();
this.setupModalAccessibility();
- if (!this.eventsBound) {
- this.bindEvents();
- this.eventsBound = true;
- }
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);
+ // Initial State
+ await this.loadR2Settings();
+ this.setSourceMode("upload");
return this.root;
})();
@@ -235,1167 +117,478 @@ export class UploadModal {
}
}
- cacheElements(context) {
- const scope = this.root || context;
- this.modalBackdrop = scope?.querySelector?.(".bv-modal-backdrop") || null;
- this.modalPanel =
- scope?.querySelector?.(".bv-modal__panel") || scope || null;
+ cacheElements() {
+ const $ = (sel) => this.root.querySelector(sel);
- 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.form = $("#unifiedUploadForm");
+ this.submitButton = $("#btn-submit");
+ this.submitStatus = $("#submit-status");
+ this.closeButton = $("#closeUploadModal");
- 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,
- isNsfw: context.querySelector("#uploadIsNsfw") || null,
- isForKids: context.querySelector("#uploadIsForKids") || null,
- isPrivate: context.querySelector("#uploadIsPrivate") || null,
- // NIP-71 specific inputs located by form manager usually, but we need direct access for automation
- summary: this.customSection?.querySelector("#nip71Summary") || null,
- summaryUnlock: this.customSection?.querySelector("#nip71SummaryUnlock") || null,
- contentWarning: this.customSection?.querySelector("#nip71ContentWarning") || null,
+ // Mode Switchers
+ this.modeButtons = {
+ upload: $("#btn-mode-upload"),
+ external: $("#btn-mode-external"),
};
- this.customAdvancedToggle = this.customSection?.querySelector("#customAdvancedToggle") || null;
- this.customAdvancedFields = this.customSection?.querySelector("#customAdvancedFields") || null;
-
- this.customSubmitButton =
- this.customForm?.querySelector?.('button[type="submit"]') || null;
- if (this.customSubmitButton && !this.customSubmitButtonDefaultLabel) {
- const defaultLabel = this.customSubmitButton.textContent || "";
- this.customSubmitButtonDefaultLabel =
- defaultLabel.trim() || defaultLabel || "Publish";
- }
- this.updateCustomSubmitButtonState();
-
- 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.cloudflareProgress =
- context.querySelector("#cloudflareProgress") || null;
- this.cloudflareProgressStatus =
- context.querySelector("#cloudflareProgressStatus") || 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.cloudflareIsNsfwInput =
- context.querySelector("#cloudflareIsNsfw") || null;
- this.cloudflareIsForKidsInput =
- context.querySelector("#cloudflareIsForKids") || null;
- this.cloudflareSummaryInput =
- this.cloudflareSection?.querySelector("#cloudflareNip71Summary") || null;
- this.cloudflareSummaryUnlock =
- this.cloudflareSection?.querySelector("#cloudflareNip71SummaryUnlock") || null;
- this.cloudflareContentWarningInput =
- this.cloudflareSection?.querySelector("#cloudflareNip71ContentWarning") || 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.cloudflareUploadAdvancedToggle =
- context.querySelector("#cloudflareUploadAdvancedToggle") || null;
- this.cloudflareUploadAdvancedFields =
- context.querySelector("#cloudflareUploadAdvancedFields") || 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;
-
- this.nip71FormManager.registerSection("custom", this.customSection);
- this.nip71FormManager.registerSection("cloudflare", this.cloudflareSection);
- }
-
- readCheckboxValue(input, defaultValue = false) {
- if (!input) {
- return Boolean(defaultValue);
- }
- return input.checked === true;
- }
-
- resetCheckbox(input, defaultValue = false) {
- if (!input) {
- return;
- }
- let fallback = Boolean(defaultValue);
- if (input.dataset && typeof input.dataset.defaultChecked === "string") {
- fallback = input.dataset.defaultChecked === "true";
- } else if (typeof input.defaultChecked === "boolean") {
- fallback = input.defaultChecked;
- }
- input.checked = fallback;
- }
-
- sanitizeAudienceFlags(flags = {}) {
- const isNsfw = flags?.isNsfw === true;
- const isForKids = flags?.isForKids === true && !isNsfw;
- return { isNsfw, isForKids };
- }
-
- setupMutuallyExclusiveCheckboxes(firstInput, secondInput) {
- if (!firstInput || !secondInput) {
- return;
- }
-
- const enforceExclusion = (primary, secondary) => {
- if (primary.checked) {
- secondary.checked = false;
- }
+ // Sections
+ this.sourceSections = {
+ upload: $("#section-source-upload"),
+ external: $("#section-source-external"),
+ settings: $("#section-storage-settings"),
+ advanced: $("#section-advanced"),
+ r2Advanced: $("#section-r2-advanced"),
+ progress: $("#upload-progress-container"),
};
- const handleFirstChange = () => enforceExclusion(firstInput, secondInput);
- const handleSecondChange = () => enforceExclusion(secondInput, firstInput);
+ // Inputs (Common)
+ this.inputs = {
+ title: $("#input-title"),
+ description: $("#input-description"),
+ thumbnail: $("#input-thumbnail"),
+ file: $("#input-file"),
+ url: $("#input-url"),
+ magnet: $("#input-magnet"),
- if (firstInput.checked && secondInput.checked) {
- secondInput.checked = false;
- }
+ // Settings
+ r2Account: $("#input-r2-account"),
+ r2Key: $("#input-r2-key"),
+ r2Secret: $("#input-r2-secret"),
+ r2Token: $("#input-r2-token"),
+ r2Zone: $("#input-r2-zone"),
+ r2Domain: $("#input-r2-domain"),
- firstInput.addEventListener("change", handleFirstChange);
- secondInput.addEventListener("change", handleSecondChange);
+ // Advanced (Manual or NIP71 managed)
+ ws: $("#input-ws"),
+ xs: $("#input-xs"),
+ summary: $("#input-summary"),
+ contentWarning: $("#input-content-warning"),
+ duration: $("#input-duration"),
- this.cleanupHandlers.push(() => {
- firstInput.removeEventListener("change", handleFirstChange);
- secondInput.removeEventListener("change", handleSecondChange);
+ // Progress
+ progress: $("#input-progress"),
+ };
+
+ // Toggles/Buttons
+ this.toggles = {
+ nsfw: $("#check-nsfw"),
+ kids: $("#check-kids"),
+ comments: $("#check-comments"),
+ summaryUnlock: $("#check-summary-unlock"),
+
+ advanced: $("#btn-advanced-toggle"),
+ storageSettings: $("#btn-storage-settings"),
+ r2Advanced: $("#btn-r2-advanced"),
+ saveSettings: $("#btn-save-settings"),
+ };
+
+ // Status text
+ this.statusText = {
+ storage: $("#storage-status"),
+ uploadMain: $("#upload-status-text"),
+ uploadPercent: $("#upload-percent-text"),
+ };
+
+ this.nip71FormManager.registerSection("main", this.form);
+ }
+
+ bindEvents() {
+ // Mode Switching
+ this.modeButtons.upload.addEventListener("click", () => this.setSourceMode("upload"));
+ this.modeButtons.external.addEventListener("click", () => this.setSourceMode("external"));
+
+ // Toggles
+ this.setupAccordion(this.toggles.advanced, this.sourceSections.advanced);
+ this.setupAccordion(this.toggles.storageSettings, this.sourceSections.settings);
+ this.setupAccordion(this.toggles.r2Advanced, this.sourceSections.r2Advanced);
+
+ // Form Submission
+ this.form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ this.handleSubmit();
});
+
+ // Settings Save
+ this.toggles.saveSettings.addEventListener("click", async () => {
+ await this.handleSaveSettings();
+ });
+
+ // Automation
+ this.setupDescriptionMirror();
+ this.setupMutuallyExclusiveCheckboxes(this.toggles.nsfw, this.toggles.kids);
+ this.setupNsfwToContentWarning();
+
+ // Close
+ this.closeButton.addEventListener("click", () => this.close());
+
+ // NIP-71 Manager
+ this.nip71FormManager.bindSection("main");
}
- // New Automation Helpers
- setupDescriptionToSummaryMirror(descriptionInput, summaryInput, unlockCheckbox, isCustom) {
- if (!descriptionInput || !summaryInput) return;
+ // --- Logic & State ---
- const handleDescriptionInput = () => {
- const locked = isCustom ? this.customSummaryLocked : this.cloudflareSummaryLocked;
- if (locked) {
- summaryInput.value = descriptionInput.value;
+ setSourceMode(mode) {
+ this.activeSource = mode;
+
+ // UI Updates
+ const isUpload = mode === "upload";
+
+ // Toggle Buttons
+ this.modeButtons.upload.setAttribute("aria-pressed", isUpload);
+ this.modeButtons.upload.classList.toggle("bg-surface", isUpload);
+ this.modeButtons.upload.classList.toggle("text-text", isUpload);
+ this.modeButtons.upload.classList.toggle("shadow-sm", isUpload);
+ this.modeButtons.upload.classList.toggle("text-muted", !isUpload);
+
+ this.modeButtons.external.setAttribute("aria-pressed", !isUpload);
+ this.modeButtons.external.classList.toggle("bg-surface", !isUpload);
+ this.modeButtons.external.classList.toggle("text-text", !isUpload);
+ this.modeButtons.external.classList.toggle("shadow-sm", !isUpload);
+ this.modeButtons.external.classList.toggle("text-muted", isUpload);
+
+ // Sections
+ if (isUpload) {
+ this.sourceSections.upload.classList.remove("hidden");
+ this.sourceSections.external.classList.add("hidden");
+
+ // Auto-show settings if missing
+ if (!this.hasValidR2Settings() && this.sourceSections.settings.classList.contains("hidden")) {
+ this.toggles.storageSettings.click(); // Expand
+ }
+ } else {
+ this.sourceSections.upload.classList.add("hidden");
+ this.sourceSections.external.classList.remove("hidden");
+ }
+
+ // Update Button Text
+ this.submitButton.textContent = isUpload ? "Upload & Publish" : "Publish Video";
+ }
+
+ setupAccordion(btn, section) {
+ if (!btn || !section) return;
+ btn.addEventListener("click", () => {
+ const isHidden = section.classList.contains("hidden");
+ if (isHidden) {
+ section.classList.remove("hidden");
+ btn.setAttribute("aria-expanded", "true");
+ // Rotate icon if exists
+ const icon = btn.querySelector("svg");
+ if (icon) icon.classList.add("rotate-90");
+ } else {
+ section.classList.add("hidden");
+ btn.setAttribute("aria-expanded", "false");
+ const icon = btn.querySelector("svg");
+ if (icon) icon.classList.remove("rotate-90");
}
- };
+ });
+ }
- descriptionInput.addEventListener('input', handleDescriptionInput);
- this.cleanupHandlers.push(() => descriptionInput.removeEventListener('input', handleDescriptionInput));
+ // --- Automation Helpers ---
- if (unlockCheckbox) {
- unlockCheckbox.addEventListener('change', () => {
- const unlocked = unlockCheckbox.checked;
- if (isCustom) {
- this.customSummaryLocked = !unlocked;
- } else {
- this.cloudflareSummaryLocked = !unlocked;
- }
+ setupDescriptionMirror() {
+ const { description, summary } = this.inputs;
+ const { summaryUnlock } = this.toggles;
- summaryInput.readOnly = !unlocked;
- if (unlocked) {
- summaryInput.classList.remove('text-muted-strong');
- summaryInput.classList.remove('bg-surface-alt'); // if applicable
- } else {
- summaryInput.classList.add('text-muted-strong');
- // re-sync if locking back
- summaryInput.value = descriptionInput.value;
- }
+ if (!description || !summary) return;
+
+ description.addEventListener("input", () => {
+ if (this.summaryLocked) summary.value = description.value;
+ });
+
+ if (summaryUnlock) {
+ summaryUnlock.addEventListener("change", () => {
+ this.summaryLocked = !summaryUnlock.checked;
+ summary.readOnly = this.summaryLocked;
+ if (this.summaryLocked) {
+ summary.value = description.value;
+ summary.classList.add("text-muted");
+ } else {
+ summary.classList.remove("text-muted");
+ }
});
}
}
- setupUrlToWsMirror(urlInput, wsInput, isCustom) {
- if (!urlInput || !wsInput) return;
+ setupMutuallyExclusiveCheckboxes(a, b) {
+ if (!a || !b) return;
+ a.addEventListener("change", () => { if (a.checked) b.checked = false; });
+ b.addEventListener("change", () => { if (b.checked) a.checked = false; });
+ }
- const handleUrlInput = () => {
- const dirty = isCustom ? this.customWsDirty : this.cloudflareWsDirty;
- if (!dirty) {
- wsInput.value = urlInput.value;
+ setupNsfwToContentWarning() {
+ const { nsfw } = this.toggles;
+ const { contentWarning } = this.inputs;
+ if (!nsfw || !contentWarning) return;
+
+ nsfw.addEventListener("change", () => {
+ if (nsfw.checked && !contentWarning.value) {
+ contentWarning.value = "NSFW";
+ } else if (!nsfw.checked && contentWarning.value === "NSFW") {
+ contentWarning.value = "";
}
+ });
+ }
+
+ // --- R2 Integration ---
+
+ async loadR2Settings() {
+ if (!this.r2Service?.loadSettings) return;
+ const settings = await this.r2Service.loadSettings();
+ this.cloudflareSettings = settings || {};
+ this.fillSettingsForm(this.cloudflareSettings);
+ this.updateStorageStatus(this.hasValidR2Settings());
+ return settings;
+ }
+
+ hasValidR2Settings() {
+ const s = this.cloudflareSettings;
+ return Boolean(s?.accountId && s?.accessKeyId && s?.secretAccessKey);
+ }
+
+ fillSettingsForm(s) {
+ if (this.inputs.r2Account) this.inputs.r2Account.value = s.accountId || "";
+ if (this.inputs.r2Key) this.inputs.r2Key.value = s.accessKeyId || "";
+ if (this.inputs.r2Secret) this.inputs.r2Secret.value = s.secretAccessKey || "";
+ if (this.inputs.r2Token) this.inputs.r2Token.value = s.apiToken || "";
+ if (this.inputs.r2Zone) this.inputs.r2Zone.value = s.zoneId || "";
+ if (this.inputs.r2Domain) this.inputs.r2Domain.value = s.baseDomain || "";
+ }
+
+ collectSettingsForm() {
+ return {
+ accountId: this.inputs.r2Account?.value?.trim() || "",
+ accessKeyId: this.inputs.r2Key?.value?.trim() || "",
+ secretAccessKey: this.inputs.r2Secret?.value?.trim() || "",
+ apiToken: this.inputs.r2Token?.value?.trim() || "",
+ zoneId: this.inputs.r2Zone?.value?.trim() || "",
+ baseDomain: this.inputs.r2Domain?.value?.trim() || "",
};
-
- const handleWsInput = () => {
- if (isCustom) this.customWsDirty = true;
- else this.cloudflareWsDirty = true;
- };
-
- urlInput.addEventListener('input', handleUrlInput);
- wsInput.addEventListener('input', handleWsInput);
-
- this.cleanupHandlers.push(() => {
- urlInput.removeEventListener('input', handleUrlInput);
- wsInput.removeEventListener('input', handleWsInput);
- });
}
- setupNsfwToContentWarning(nsfwInput, contentWarningInput) {
- if (!nsfwInput || !contentWarningInput) return;
+ async handleSaveSettings() {
+ const settings = this.collectSettingsForm();
+ await this.r2Service.saveSettings(settings);
+ this.cloudflareSettings = settings;
+ this.updateStorageStatus(this.hasValidR2Settings());
- const handleNsfwChange = () => {
- if (nsfwInput.checked) {
- if (!contentWarningInput.value.trim()) {
- contentWarningInput.value = "NSFW";
- }
- } else {
- if (contentWarningInput.value.trim().toUpperCase() === "NSFW") {
- contentWarningInput.value = "";
- }
- }
- };
+ // Visual feedback
+ const btn = this.toggles.saveSettings;
+ const originalText = btn.textContent;
+ btn.textContent = "Saved!";
+ setTimeout(() => btn.textContent = originalText, 2000);
- nsfwInput.addEventListener('change', handleNsfwChange);
- this.cleanupHandlers.push(() => nsfwInput.removeEventListener('change', handleNsfwChange));
- }
-
- setupAdvancedToggle(toggleButton, container, icon) {
- if (!toggleButton || !container) return;
-
- const handleToggle = () => {
- const wasHidden = container.classList.contains('hidden');
- logger.dev.info("[UploadModal] toggle advanced (before)", {
- toggleId: toggleButton.id || null,
- containerId: container.id || null,
- isHidden: wasHidden
- });
- if (wasHidden) {
- container.classList.remove('hidden');
- toggleButton.setAttribute('aria-expanded', 'true');
- if (toggleButton.querySelector('span')) toggleButton.querySelector('span').textContent = "Hide advanced options";
- if (icon) icon.classList.add('rotate-90');
- } else {
- container.classList.add('hidden');
- toggleButton.setAttribute('aria-expanded', 'false');
- if (toggleButton.querySelector('span')) toggleButton.querySelector('span').textContent = "Show advanced options";
- if (icon) icon.classList.remove('rotate-90');
- }
- const computedDisplay =
- typeof window !== "undefined" && window.getComputedStyle
- ? window.getComputedStyle(container).display
- : null;
- logger.dev.info("[UploadModal] toggle advanced (after)", {
- toggleId: toggleButton.id || null,
- containerId: container.id || null,
- isHidden: container.classList.contains('hidden'),
- computedDisplay,
- offsetParent: container.offsetParent ? "visible" : "hidden"
- });
- };
-
- toggleButton.addEventListener('click', handleToggle);
- this.cleanupHandlers.push(() => toggleButton.removeEventListener('click', handleToggle));
- }
-
- buildAutoGeneratedImetaVariant(file) {
- if (!file) {
- return null;
- }
- const mimeType = typeof file.type === "string" ? file.type.trim() : "";
- if (!mimeType) {
- return null;
- }
- return {
- m: mimeType,
- dim: "",
- url: "",
- x: "",
- image: [],
- fallback: [],
- service: [],
- autoGenerated: true
- };
- }
-
- bindEvents() {
- logger.dev.info("[UploadModal] bindEvents", {
- rootConnected: Boolean(this.root?.isConnected),
- customAdvancedToggle: Boolean(this.customAdvancedToggle),
- cloudflareUploadAdvancedToggle: Boolean(this.cloudflareUploadAdvancedToggle),
- cloudflareAdvancedToggle: Boolean(this.cloudflareAdvancedToggle)
- });
-
- if (this.closeButton) {
- this.closeButton.addEventListener("click", () => {
- this.close();
- });
- }
-
- if (this.customForm) {
- this.customForm.addEventListener("submit", (event) => {
- event.preventDefault();
- if (this.isCustomSubmitOnCooldown()) {
- this.updateCustomSubmitButtonState();
- return;
- }
- 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
- );
- }
- });
- }
-
- if (this.customFormInputs?.isNsfw && this.customFormInputs?.isForKids) {
- this.setupMutuallyExclusiveCheckboxes(
- this.customFormInputs.isNsfw,
- this.customFormInputs.isForKids
- );
- }
-
- if (this.cloudflareIsNsfwInput && this.cloudflareIsForKidsInput) {
- this.setupMutuallyExclusiveCheckboxes(
- this.cloudflareIsNsfwInput,
- this.cloudflareIsForKidsInput
- );
- }
-
- // Bind Automation Logic - Custom
- this.setupDescriptionToSummaryMirror(
- this.customFormInputs.description,
- this.customFormInputs.summary,
- this.customFormInputs.summaryUnlock,
- true
- );
- this.setupUrlToWsMirror(
- this.customFormInputs.url,
- this.customFormInputs.ws,
- true
- );
- this.setupNsfwToContentWarning(
- this.customFormInputs.isNsfw,
- this.customFormInputs.contentWarning
- );
- this.setupAdvancedToggle(
- this.customAdvancedToggle,
- this.customAdvancedFields,
- this.customSection?.querySelector("#customAdvancedToggleIcon")
- );
-
- // Bind Automation Logic - Cloudflare
- this.setupDescriptionToSummaryMirror(
- this.cloudflareDescriptionInput,
- this.cloudflareSummaryInput,
- this.cloudflareSummaryUnlock,
- false
- );
- // Cloudflare WS is mainly for manual entry or populated via upload logic,
- // but if user types a URL (unlikely in CF mode as it uploads), we could mirror.
- // However, in CF mode the URL is usually auto-filled after upload.
- // If user manually types it, we can mirror.
- // Actually, Cloudflare form doesn't have a "Hosted URL" input that the user types before upload?
- // Wait, let's check HTML.
- // The "Cloudflare" form has a file input. It does NOT have a "Hosted video URL" input for the user to type.
- // The "Hosted URL" in IMETA is auto-filled.
- // So setupUrlToWsMirror is irrelevant for Cloudflare mode's main flow,
- // BUT there is a `cloudflareWs` input in advanced settings.
- // If the user *manually* fills the WS, they can. But there's no source URL input to mirror *from*.
- // So we skip setupUrlToWsMirror for Cloudflare mode.
-
- this.setupNsfwToContentWarning(
- this.cloudflareIsNsfwInput,
- this.cloudflareContentWarningInput
- );
- this.setupAdvancedToggle(
- this.cloudflareUploadAdvancedToggle,
- this.cloudflareUploadAdvancedFields,
- this.cloudflareSection?.querySelector("#cloudflareUploadAdvancedToggleIcon")
- );
-
-
- this.nip71FormManager.bindSection("custom");
- this.nip71FormManager.bindSection("cloudflare");
- }
-
- clearCustomSubmitCooldownTimer() {
- if (this.customSubmitCooldownTimer) {
- clearInterval(this.customSubmitCooldownTimer);
- this.customSubmitCooldownTimer = null;
- }
- }
-
- isCustomSubmitOnCooldown() {
- if (!this.customSubmitBlockedUntil) {
- return false;
- }
- return Date.now() < this.customSubmitBlockedUntil;
- }
-
- updateCustomSubmitButtonState() {
- const button = this.customSubmitButton;
- if (!button) {
- return;
- }
-
- if (!this.customSubmitButtonDefaultLabel) {
- const label = button.textContent || "";
- this.customSubmitButtonDefaultLabel = label.trim() || label || "Publish";
- }
-
- if (this.isCustomSubmitOnCooldown()) {
- const remainingMs = Math.max(
- 0,
- this.customSubmitBlockedUntil - Date.now()
- );
- const seconds = Math.max(1, Math.ceil(remainingMs / 1000));
- button.disabled = true;
- button.setAttribute("aria-disabled", "true");
- button.dataset.cooldown = "true";
- button.textContent = `${this.customSubmitButtonDefaultLabel} (${seconds})`;
- button.title = `Please wait ${seconds} seconds before sharing another video.`;
- return;
- }
-
- button.disabled = false;
- button.removeAttribute("aria-disabled");
- delete button.dataset.cooldown;
- button.textContent =
- this.customSubmitButtonDefaultLabel || button.textContent || "Publish";
- button.removeAttribute("title");
- }
-
- startCustomSubmitCooldown() {
- if (!this.customSubmitButton) {
- return;
- }
- const now = Date.now();
- this.customSubmitBlockedUntil = now + this.customSubmitCooldownMs;
- this.updateCustomSubmitButtonState();
- this.clearCustomSubmitCooldownTimer();
- this.customSubmitCooldownTimer = setInterval(() => {
- if (!this.isCustomSubmitOnCooldown()) {
- this.clearCustomSubmitCooldown();
- return;
+ // Collapse if valid
+ if (this.hasValidR2Settings()) {
+ this.sourceSections.settings.classList.add("hidden");
+ this.toggles.storageSettings.setAttribute("aria-expanded", "false");
}
- this.updateCustomSubmitButtonState();
- }, 1000);
}
- clearCustomSubmitCooldown() {
- this.clearCustomSubmitCooldownTimer();
- this.customSubmitBlockedUntil = 0;
- this.updateCustomSubmitButtonState();
- }
-
- cancelCustomSubmitCooldown() {
- this.clearCustomSubmitCooldown();
- }
-
- setupModalAccessibility() {
- if (!this.root) {
- return;
- }
-
- if (this.modalAccessibility?.destroy) {
- this.modalAccessibility.destroy();
- }
-
- this.modalAccessibility = createModalAccessibility({
- root: this.root,
- backdrop: this.modalBackdrop || this.root,
- panel: this.modalPanel || this.root,
- onRequestClose: () => this.close()
- });
-
- this.cleanupHandlers.push(() => {
- if (this.modalAccessibility?.destroy) {
- this.modalAccessibility.destroy();
+ updateStorageStatus(isValid) {
+ if (this.statusText.storage) {
+ this.statusText.storage.textContent = isValid ? "Ready" : "Missing Credentials";
+ this.statusText.storage.className = isValid ? "text-xs text-accent" : "text-xs text-critical";
}
- this.modalAccessibility = null;
- });
}
registerR2Subscriptions() {
- if (!this.r2Service?.on) {
- return;
- }
+ if (!this.r2Service?.on) return;
- if (Array.isArray(this.r2Unsubscribes)) {
- this.r2Unsubscribes.forEach((unsubscribe) => {
- try {
- if (typeof unsubscribe === "function") {
- unsubscribe();
- }
- } catch (error) {
- userLogger.warn("[UploadModal] Failed to remove R2 listener", error);
- }
+ // Clear old
+ this.r2Unsubscribes.forEach(u => u && u());
+ this.r2Unsubscribes = [];
+
+ const sub = (evt, fn) => {
+ const unsub = this.r2Service.on(evt, fn);
+ if (unsub) this.r2Unsubscribes.push(unsub);
+ };
+
+ sub("uploadProgress", ({ fraction }) => this.updateProgress(fraction));
+ sub("uploadStatus", ({ message, variant }) => this.updateUploadStatus(message, variant));
+ sub("uploadStateChange", ({ isUploading }) => {
+ this.isUploading = isUploading;
+ this.submitButton.disabled = isUploading;
});
- }
- 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);
- })
- );
}
+ updateProgress(fraction) {
+ const container = this.sourceSections.progress;
+ const bar = this.inputs.progress;
+ const txt = this.statusText.uploadPercent;
+
+ if (fraction === null || fraction < 0) {
+ container.classList.add("hidden");
+ return;
+ }
+
+ container.classList.remove("hidden");
+ const pct = Math.round(fraction * 100);
+ bar.value = pct;
+ txt.textContent = `${pct}%`;
+ }
+
+ updateUploadStatus(msg, variant) {
+ if (this.statusText.uploadMain) {
+ this.statusText.uploadMain.textContent = msg;
+ // Could style based on variant (error/success)
+ }
+ }
+
+ // --- Submission ---
+
+ async handleSubmit() {
+ if (this.isUploading) return;
+
+ const audienceFlags = {
+ isNsfw: this.toggles.nsfw?.checked || false,
+ isForKids: this.toggles.kids?.checked || false,
+ };
+
+ // Base Metadata
+ const metadata = {
+ title: this.inputs.title?.value?.trim() || "",
+ description: this.inputs.description?.value?.trim() || "",
+ thumbnail: this.inputs.thumbnail?.value?.trim() || "",
+ enableComments: this.toggles.comments?.checked || true,
+ ...audienceFlags,
+
+ // These might be empty depending on mode, filled below
+ url: "",
+ magnet: "",
+ ws: this.inputs.ws?.value?.trim() || "",
+ xs: this.inputs.xs?.value?.trim() || "",
+ };
+
+ // NIP-71 Advanced Data
+ const nip71 = this.nip71FormManager.collectSection("main");
+ if (nip71) {
+ metadata.nip71 = nip71;
+ }
+
+ try {
+ if (this.activeSource === "upload") {
+ await this.handleUploadFlow(metadata);
+ } else {
+ await this.handleExternalFlow(metadata);
+ }
+ } catch (err) {
+ userLogger.error("Upload failed", err);
+ this.showError(err.message || "An unexpected error occurred.");
+ }
+ }
+
+ async handleUploadFlow(metadata) {
+ const file = this.inputs.file?.files?.[0];
+ if (!file) throw new Error("Please select a video file to upload.");
+
+ if (!this.hasValidR2Settings()) throw new Error("Please configure R2 storage credentials.");
+
+ const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
+ if (!pubkey) throw new Error("Please login to publish.");
+
+ const npub = this.safeEncodeNpub(pubkey);
+
+ await this.r2Service.uploadVideo({
+ npub,
+ file,
+ metadata,
+ settingsInput: this.collectSettingsForm(),
+ publishVideoNote: this.publishVideoNote,
+ onReset: () => this.resetForm(),
+ });
+ }
+
+ async handleExternalFlow(metadata) {
+ metadata.url = this.inputs.url?.value?.trim() || "";
+ metadata.magnet = this.inputs.magnet?.value?.trim() || "";
+
+ const hasUrl = metadata.url.length > 0;
+ const hasMagnet = metadata.magnet.length > 0;
+ const hasImeta = metadata.nip71?.imeta?.some(v => v.url);
+
+ if (!hasUrl && !hasMagnet && !hasImeta) {
+ throw new Error("Please provide at least a Video URL or Magnet Link.");
+ }
+
+ if (!hasUrl && !hasImeta && hasMagnet) {
+ if (!confirm("Magnet-only uploads require active seeding. Proceed?")) return;
+ }
+
+ // Normalize & Publish
+ const { payload, errors } = normalizeVideoNotePayload(metadata);
+ if (errors.length) {
+ throw new Error(getVideoNoteErrorMessage(errors[0]));
+ }
+
+ // Simulate async publish
+ this.submitButton.disabled = true;
+ this.submitButton.textContent = "Publishing...";
+
+ try {
+ if (this.publishVideoNote) {
+ await this.publishVideoNote(payload);
+ this.showSuccess("Video published successfully!");
+ this.close();
+ this.resetForm();
+ }
+ } finally {
+ this.submitButton.disabled = false;
+ this.submitButton.textContent = "Publish Video";
+ }
+ }
+
+ resetForm() {
+ this.form.reset();
+ this.nip71FormManager.resetSection("main");
+ // Restore defaults
+ this.setSourceMode("upload");
+ this.toggles.comments.checked = true;
+ this.toggles.nsfw.checked = false;
+ this.toggles.kids.checked = false;
+ this.updateProgress(null);
+ }
+
+ // --- Modal Control ---
+
open({ triggerElement } = {}) {
- if (!this.root) {
- return;
- }
- const wasHidden = this.root.classList.contains("hidden");
- if (wasHidden) {
- this.root.classList.remove("hidden");
- this.setGlobalModalState("upload", true);
- this.emit("upload:open", { mode: this.activeMode });
- }
+ if (!this.root) return;
+ this.root.classList.remove("hidden");
+ this.setGlobalModalState("upload", true);
this.isVisible = true;
this.modalAccessibility?.activate({ triggerElement });
- this.updateCustomSubmitButtonState();
}
close() {
- if (!this.root) {
- return;
- }
- this.modalAccessibility?.deactivate();
- if (this.root.classList.contains("hidden")) {
- this.isVisible = false;
- return;
- }
+ if (!this.root) return;
this.root.classList.add("hidden");
- this.isVisible = false;
this.setGlobalModalState("upload", false);
- this.emit("upload:close", { mode: this.activeMode });
+ this.isVisible = false;
+ this.modalAccessibility?.deactivate();
}
- 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;
- if (isActive) {
- button.dataset.state = "active";
- } else {
- delete button.dataset.state;
- }
- button.setAttribute("aria-pressed", isActive ? "true" : "false");
- });
- }
-
- if (normalized === "cloudflare") {
- this.refreshCloudflareBucketPreview();
- } else {
- this.updateCustomSubmitButtonState();
- }
+ addEventListener(type, listener) {
+ this.eventTarget.addEventListener(type, listener);
}
- handleCustomSubmit() {
- const audienceFlags = this.sanitizeAudienceFlags({
- isNsfw: this.readCheckboxValue(this.customFormInputs.isNsfw, false),
- isForKids: this.readCheckboxValue(this.customFormInputs.isForKids, false)
+ emit(type, detail) {
+ this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
+ }
+
+ setupModalAccessibility() {
+ if (!this.root) return;
+ if (this.modalAccessibility?.destroy) this.modalAccessibility.destroy();
+
+ this.modalAccessibility = createModalAccessibility({
+ root: this.root,
+ backdrop: this.root.querySelector(".bv-modal-backdrop") || this.root,
+ panel: this.root.querySelector(".modal-sheet") || this.root,
+ onRequestClose: () => this.close()
});
-
- const rawPayload = {
- title: this.customFormInputs.title?.value ?? "",
- url: this.customFormInputs.url?.value ?? "",
- magnet: this.customFormInputs.magnet?.value ?? "",
- ws: this.customFormInputs.ws?.value ?? "",
- xs: this.customFormInputs.xs?.value ?? "",
- thumbnail: this.customFormInputs.thumbnail?.value ?? "",
- description: this.customFormInputs.description?.value ?? "",
- enableComments: this.readCheckboxValue(
- this.customFormInputs.enableComments,
- true
- ),
- ...audienceFlags
- };
-
- if (this.customFormInputs.isPrivate) {
- rawPayload.isPrivate = this.readCheckboxValue(
- this.customFormInputs.isPrivate,
- false
- );
- }
-
- const nip71Metadata = this.nip71FormManager.collectSection("custom");
- if (nip71Metadata) {
- rawPayload.nip71 = nip71Metadata;
- }
-
- const hasDirectUrl = typeof rawPayload.url === "string" && rawPayload.url.trim().length > 0;
- const hasMagnet = typeof rawPayload.magnet === "string" && rawPayload.magnet.trim().length > 0;
- const hasImetaUrl = nip71Metadata?.imeta?.some((v) => v.url && v.url.trim().length > 0);
-
- if (!hasDirectUrl && !hasImetaUrl && hasMagnet) {
- const confirmed = typeof window !== "undefined" && window.confirm
- ? window.confirm("You are uploading with a magnet link only.\n\nSince this video is not hosted on a server, it will only be playable as long as you (or others) continue seeding it.\n\nDo you want to proceed?")
- : true;
-
- if (!confirmed) {
- this.updateCustomSubmitButtonState();
- return;
- }
- }
-
- const { payload, errors } = normalizeVideoNotePayload(rawPayload);
-
- if (errors.length) {
- const message = getVideoNoteErrorMessage(errors[0]);
- this.showError(message);
- return;
- }
-
- this.startCustomSubmitCooldown();
- 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-info-strong",
- "text-critical",
- "text-warning-strong",
- "text-muted"
- );
-
- if (!message) {
- element.classList.add("text-muted");
- return;
- }
-
- let className = "text-muted";
- if (variant === "success") {
- className = "text-info-strong";
- } else if (variant === "error") {
- className = "text-critical";
- } else if (variant === "warning") {
- className = "text-warning-strong";
- }
- 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.cloudflareProgress) {
- this.emit("upload:r2-progress", { fraction: null });
- return;
- }
-
- if (!Number.isFinite(fraction) || fraction < 0) {
- this.cloudflareProgress.value = 0;
- delete this.cloudflareProgress.dataset.progress;
- this.cloudflareProgress.dataset.state = "idle";
- this.cloudflareProgress.hidden = true;
- this.cloudflareProgress.setAttribute(
- "aria-valuetext",
- "Upload progress unavailable",
- );
- this.cloudflareProgress.setAttribute("aria-hidden", "true");
- if (this.cloudflareProgressStatus) {
- this.cloudflareProgressStatus.textContent = "";
- }
- this.emit("upload:r2-progress", { fraction: null });
- return;
- }
-
- const clamped = Math.max(0, Math.min(1, fraction));
- const percent = Math.round(clamped * 100);
- const state = percent >= 100 ? "complete" : "active";
- const valueText = `Upload ${percent}% complete`;
-
- this.cloudflareProgress.max = 100;
- this.cloudflareProgress.value = percent;
- this.cloudflareProgress.dataset.progress = String(percent);
- this.cloudflareProgress.dataset.state = state;
- this.cloudflareProgress.hidden = false;
- this.cloudflareProgress.setAttribute("aria-hidden", "false");
- this.cloudflareProgress.setAttribute("aria-valuetext", valueText);
-
- if (this.cloudflareProgressStatus) {
- this.cloudflareProgressStatus.textContent = valueText;
- }
-
- 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.resetCheckbox(this.cloudflareEnableCommentsInput, true);
- if (this.cloudflareIsNsfwInput)
- this.resetCheckbox(this.cloudflareIsNsfwInput, false);
- if (this.cloudflareIsForKidsInput)
- this.resetCheckbox(this.cloudflareIsForKidsInput, false);
- if (this.cloudflareFileInput) this.cloudflareFileInput.value = "";
-
- // Reset Automation State
- this.cloudflareSummaryLocked = true;
- this.cloudflareWsDirty = false;
-
- // Reset inputs we automated or tracked
- if (this.cloudflareSummaryInput) {
- this.cloudflareSummaryInput.value = "";
- this.cloudflareSummaryInput.readOnly = true;
- this.cloudflareSummaryInput.classList.add('text-muted-strong');
- }
- if (this.cloudflareSummaryUnlock) this.cloudflareSummaryUnlock.checked = false;
- if (this.cloudflareContentWarningInput) this.cloudflareContentWarningInput.value = "";
-
- this.nip71FormManager.resetSection("cloudflare");
- 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.resetCheckbox(this.customFormInputs.enableComments, true);
- if (this.customFormInputs.isNsfw)
- this.resetCheckbox(this.customFormInputs.isNsfw, false);
- if (this.customFormInputs.isForKids)
- this.resetCheckbox(this.customFormInputs.isForKids, false);
- if (this.customFormInputs.isPrivate)
- this.resetCheckbox(this.customFormInputs.isPrivate, false);
-
- // Reset Automation State
- this.customSummaryLocked = true;
- this.customWsDirty = false;
-
- // Reset automated inputs
- if (this.customFormInputs.summary) {
- this.customFormInputs.summary.value = "";
- this.customFormInputs.summary.readOnly = true;
- this.customFormInputs.summary.classList.add('text-muted-strong');
- }
- if (this.customFormInputs.summaryUnlock) this.customFormInputs.summaryUnlock.checked = false;
- if (this.customFormInputs.contentWarning) this.customFormInputs.contentWarning.value = "";
-
- this.nip71FormManager.resetSection("custom");
- }
-
- 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) {
- userLogger.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) {
- userLogger.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 audienceFlags = this.sanitizeAudienceFlags({
- isNsfw: this.readCheckboxValue(this.cloudflareIsNsfwInput, false),
- isForKids: this.readCheckboxValue(this.cloudflareIsForKidsInput, false)
- });
- const metadata = {
- title: this.cloudflareTitleInput?.value ?? "",
- description: this.cloudflareDescriptionInput?.value ?? "",
- thumbnail: this.cloudflareThumbnailInput?.value ?? "",
- magnet: this.cloudflareMagnetInput?.value ?? "",
- ws: this.cloudflareWsInput?.value ?? "",
- xs: this.cloudflareXsInput?.value ?? "",
- enableComments: this.readCheckboxValue(
- this.cloudflareEnableCommentsInput,
- true
- ),
- ...audienceFlags
- };
-
- metadata.title = metadata.title?.trim() || "";
-
- if (!metadata.title) {
- this.setCloudflareUploadStatus(
- getVideoNoteErrorMessage(VIDEO_NOTE_ERROR_CODES.MISSING_TITLE),
- "error"
- );
- return;
- }
-
- const nip71Metadata = this.nip71FormManager.collectSection("cloudflare");
- if (nip71Metadata) {
- const imetaList = Array.isArray(nip71Metadata.imeta)
- ? [...nip71Metadata.imeta]
- : [];
- const autoImeta = this.buildAutoGeneratedImetaVariant(file);
- if (autoImeta && !imetaList.some((variant) => variant?.autoGenerated)) {
- imetaList.push(autoImeta);
- }
- metadata.nip71 = {
- ...nip71Metadata,
- imeta: imetaList
- };
- }
-
- 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) {
- userLogger.error("[UploadModal] Cloudflare upload failed", error);
- }
}
destroy() {
- this.clearCustomSubmitCooldown();
-
- if (this.modalAccessibility?.destroy) {
- this.modalAccessibility.destroy();
- }
- this.modalAccessibility = null;
- this.modalBackdrop = null;
- this.modalPanel = null;
-
- if (Array.isArray(this.cleanupHandlers)) {
- this.cleanupHandlers.forEach((cleanup) => {
- try {
- if (typeof cleanup === "function") {
- cleanup();
- }
- } catch (error) {
- userLogger.warn("[UploadModal] Failed to remove handler", error);
- }
- });
- this.cleanupHandlers = [];
- }
-
- if (Array.isArray(this.r2Unsubscribes)) {
- this.r2Unsubscribes.forEach((unsubscribe) => {
- try {
- if (typeof unsubscribe === "function") {
- unsubscribe();
- }
- } catch (error) {
- userLogger.warn("[UploadModal] Failed to cleanup R2 listener", error);
- }
- });
- }
- this.r2Unsubscribes = [];
- this.customSubmitButton = null;
- this.customSubmitButtonDefaultLabel = "";
- this.customSubmitBlockedUntil = 0;
- this.eventsBound = false;
- this.loadPromise = null;
+ this.r2Unsubscribes.forEach(u => u && u());
+ if (this.modalAccessibility?.destroy) this.modalAccessibility.destroy();
this.root = null;
this.container = null;
}