UNPKG

peertube-plugin-transposer-connector

Version:

Transposer connector is a PeerTube language tool plugin to transcribe and translate with Whisper

1,230 lines (1,064 loc) 53.8 kB
async function register({ registerClientRoute, peertubeHelpers }) { const { notifier } = peertubeHelpers; function getInstanceBaseUrl() { return window.location.origin; } registerClientRoute({ route: "/transposer", onMount: ({ rootEl }) => { window.currentPage = 1; window.videosPerPage = 10; const detailSVG = `<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5h10M11 9h5"/><rect width="4" height="4" x="3" y="5" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" rx="1"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 15h10m-10 4h5"/><rect width="4" height="4" x="3" y="15" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" rx="1"/></svg>`; const reloadSVG = `<?xml version="1.0" encoding="utf-8"?> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <svg fill="#000000" width="24" height="24" viewBox="-7 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg"> <title>loading</title> <path d="M2.080 14.040l4-1.040c0.44-0.12 0.72-0.56 0.6-1.040-0.12-0.44-0.56-0.72-1.040-0.6l-2.080 0.56c0.68-0.88 1.56-1.6 2.64-2.080 1.64-0.72 3.44-0.76 5.12-0.12 1.64 0.64 2.96 1.92 3.68 3.52 0.2 0.44 0.68 0.6 1.12 0.44 0.44-0.2 0.6-0.68 0.44-1.12-0.88-2.040-2.52-3.6-4.6-4.44-2.080-0.8-4.36-0.76-6.4 0.12-1.36 0.6-2.48 1.52-3.36 2.68l-0.52-1.96c-0.12-0.44-0.56-0.72-1.040-0.6-0.44 0.12-0.72 0.56-0.6 1.040l1.040 4c0.12 0.56 0.4 0.8 1 0.64zM17.72 22.52l-1.040-3.96c0 0-0.16-0.8-0.96-0.6v0l-4 1.040c-0.44 0.12-0.72 0.56-0.6 1.040 0.12 0.44 0.56 0.72 1.040 0.6l2.080-0.56c-1.76 2.32-4.88 3.28-7.72 2.16-1.64-0.64-2.96-1.92-3.68-3.52-0.2-0.44-0.68-0.6-1.12-0.44-0.44 0.2-0.6 0.68-0.44 1.12 0.88 2.040 2.52 3.6 4.6 4.44 1 0.4 2 0.56 3.040 0.56 2.64 0 5.12-1.24 6.72-3.4l0.52 1.96c0.080 0.36 0.44 0.64 0.8 0.64 0.080 0 0.16 0 0.2-0.040 0.4-0.16 0.68-0.6 0.56-1.040z"></path> </svg>`; const notriggeredSVG = `<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.02502 12.0002C5.02526 8.66068 7.32548 5.78639 10.519 5.1351C13.7125 4.48382 16.9098 6.23693 18.1557 9.32231C19.4015 12.4077 18.3489 15.966 15.6415 17.8212C12.9341 19.6764 9.32601 19.3117 7.02377 16.9502C5.74387 15.6373 5.02489 13.8568 5.02502 12.0002Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M13.1819 5.1352C10.2334 4.53373 7.24846 5.98176 5.83137 8.70099C4.41427 11.4202 4.89879 14.7702 7.0245 16.9505C9.15022 19.1307 12.4165 19.6276 15.0677 18.1742C17.719 16.7208 19.1308 13.6593 18.5444 10.6352" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`; const loadingSVG = `<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2.99988V5.99988M12 20.9999V17.9999M4.20577 16.4999L6.80385 14.9999M21 11.9999H18M16.5 19.7941L15 17.196M3 11.9999H6M7.5 4.20565L9 6.80373M7.5 19.7941L9 17.196M19.7942 16.4999L17.1962 14.9999M4.20577 7.49988L6.80385 8.99988" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>`; const successSVG = `<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM16.0303 8.96967C16.3232 9.26256 16.3232 9.73744 16.0303 10.0303L11.0303 15.0303C10.7374 15.3232 10.2626 15.3232 9.96967 15.0303L7.96967 13.0303C7.67678 12.7374 7.67678 12.2626 7.96967 11.9697C8.26256 11.6768 8.73744 11.6768 9.03033 11.9697L10.5 13.4393L12.7348 11.2045L14.9697 8.96967C15.2626 8.67678 15.7374 8.67678 16.0303 8.96967Z" fill="#1C274C"/></svg>`; const errorSVG = `<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 16H12.01M12 8V12M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`; const modifySVG = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-edit" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4CBDC9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" /><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" /><path d="M16 5l3 3" /></svg>`; const deleteSVG = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4CBDC9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" /></svg>`; rootEl.innerHTML = getPluginContainerHTML(); const style = document.createElement("style"); style.innerHTML = getThemeStyles(); rootEl.appendChild(style); const loadingIndicator = rootEl.querySelector("#loadingIndicator"); if (loadingIndicator) { loadingIndicator.style.display = "flex"; } initialize(); function getPluginContainerHTML() { return ` <div id="plugincontainer"> <h1>Transposer</h1> <div id="loadingIndicator" class="loading-circle"> <div class="spinner"></div> </div> <table id="myTable"> </table> <div id="pageButtonsContainer"> </div> <div id="messageContainer"> </div> <!-- Add custom modal dialog --> <div id="confirmationModal" class="modal" style="display:none;"> <div class="modal-content"> <h3>Confirm Action</h3> <p id="confirmationMessage">There is already a transcription. Do you want to overwrite it?</p> <div class="modal-buttons"> <button id="confirmButton">Yes, Overwrite</button> <button id="cancelButton">No, Cancel</button> </div> </div> </div> </div> `; } async function getVideos(start = 0, count = window.videosPerPage) { const loadingIndicator = rootEl.querySelector("#loadingIndicator"); loadingIndicator.style.display = "flex"; const table = rootEl.querySelector("#myTable"); table.innerHTML = ""; table.classList.add("loading"); var serverConf = getInstanceBaseUrl(); console.log(start); console.log(count); const endpoint = `${serverConf}/api/v1/users/me/videos/?start=${start}&count=${count}`; console.log(endpoint); try { await fetch(endpoint, { method: "GET", headers: peertubeHelpers.getAuthHeader(), }) .then((res) => { console.log("api:", endpoint); console.log("result:", res); return res.json(); }) .then(async (videos) => { const messageContainer = rootEl.querySelector("#messageContainer"); if (videos.total == 0) { messageContainer.textContent = "No Videos yet"; loadingIndicator.style.display = "none"; table.classList.remove("loading"); return; } if (videos.total > 0) { messageContainer.textContent = ""; } window.totalVideos = videos.total; const newTableBody = document.createElement("tbody"); const tableHeaderRow = document.createElement("tr"); tableHeaderRow.innerHTML = ` <th>Published At</th> <th>Link</th> <th>Title</th> <th>Description</th> <th>Default Language</th> <th colspan="4" class="action-column-header"> <div class="action-headers-container"> <span class="action-header">Transcribe</span> <span class="action-header">Translate</span> <span class="action-header">Metadata</span> <span class="action-header">Edit</span> </div> </th> `; newTableBody.appendChild(tableHeaderRow); for (const video of videos.data) { const videoId = video.id; const captionData = await fetch( `${getInstanceBaseUrl()}/api/v1/videos/${videoId}/captions`, { method: "GET", headers: peertubeHelpers.getAuthHeader(), } ) .then((response) => { if (!response.ok) { console.error( `Failed to fetch captions: ${response.status}` ); return { total: 0, data: [] }; } return response.json(); }) .catch((error) => { console.error(`Error fetching captions: ${error}`); return { total: 0, data: [] }; }); const metadataData = await fetch( `${peertubeHelpers.getBaseRouterRoute()}/language/metadata/video/${videoId}`, { headers: peertubeHelpers.getAuthHeader(), } ) .then((response) => { if (!response.ok) { console.error( `Failed to fetch metadata: ${response.status}` ); return {}; } return response.json(); }) .catch((error) => { console.error(`Error fetching metadata: ${error}`); return {}; }); const metadataFields = Object.keys(metadataData); const languages = new Set(); metadataFields.forEach((field) => { if (metadataData[field]) { Object.keys(metadataData[field]).forEach((lang) => { if ( metadataData[field][lang] && metadataData[field][lang].trim() !== "" ) { languages.add(lang); } }); } }); const translatedCount = languages.size; var totalCaptions = captionData.total; const alreadyTranscripted = totalCaptions > 0 ? true : false; const videoUuid = video.uuid; const videoShortUuid = video.shortUUID; const videoState = video.state.label; const videoLanguage = video.language ? video.language.label : null; const videoLanguageId = video.language ? video.language.id : null; const title = video.name; const description = video.description; const publishedAt = new Date(video.publishedAt); const formattedPublishedAt = `${publishedAt.toLocaleDateString()} ${publishedAt.toLocaleTimeString()}`; const thumbnailUrl = video.thumbnailPath; const videoUrl = video.url; // Check if video has a language set - language exists, has valid id (not null), and valid label (not "Unknown") const hasLanguage = !!( video.language && video.language.id && video.language.id !== null && video.language.label && video.language.label !== "Unknown" ); console.log( `Video ${videoId}: videoLanguage="${videoLanguage}", videoLanguageId="${videoLanguageId}", hasLanguage=${hasLanguage}, language.id="${video.language?.id}", language.label="${video.language?.label}"` ); // PUBLIC = 1, UNLISTED = 2, PRIVATE = 3, INTERNAL = 4 let privacyId; if (video.privacy && typeof video.privacy.id !== "undefined") { privacyId = video.privacy.id; } else if (typeof video.privacy === "number") { privacyId = video.privacy; } else { console.warn( `Video ${videoId}: Unknown privacy structure`, video.privacy ); privacyId = 1; } const isPublic = privacyId === 1; // PUBLIC = 1 const isUnlisted = privacyId === 2; // UNLISTED = 2 const isPrivate = privacyId === 3; // PRIVATE = 3 const isInternal = privacyId === 4; // INTERNAL = 4 // Check password protection with multiple fallbacks const hasPassword = video.passwordProtected === true || video.hasPassword === true; // Check if accessible (public/unlisted and no password) - separate from language const isAccessibleByPrivacy = (isPublic || isUnlisted) && !hasPassword; // Only consider restricted if privacy/password issues (not language) const isRestricted = !isAccessibleByPrivacy; console.log( `Video ${videoId}: privacyId=${privacyId}, isPublic=${isPublic}, isUnlisted=${isUnlisted}, isPrivate=${isPrivate}, isInternal=${isInternal}, hasPassword=${hasPassword}, hasLanguage=${hasLanguage}, isAccessibleByPrivacy=${isAccessibleByPrivacy}, isRestricted=${isRestricted}` ); // Determine transcription button state let transcribeButtonClass, transcribeButtonSVG, transcribeButtonText; let transcribeButtonDisabled = false; let transcribeButtonTitle = ""; if (!hasLanguage) { // No language set - button should be clickable but will show language modal transcribeButtonClass = "not-triggered"; transcribeButtonSVG = notriggeredSVG; transcribeButtonText = "0/1"; transcribeButtonDisabled = false; transcribeButtonTitle = "Click to set language and transcribe video"; } else if (isRestricted) { // Language is set but privacy/password issues transcribeButtonClass = "disabled"; transcribeButtonSVG = notriggeredSVG; transcribeButtonText = "0/1"; transcribeButtonDisabled = true; if (hasPassword) { transcribeButtonTitle = "Transcription is not available for password-protected videos. Please remove the password."; } else if (isPrivate) { transcribeButtonTitle = "Transcription is not available for private videos. Please make the video public or unlisted."; } else if (isInternal) { transcribeButtonTitle = "Transcription is not available for internal videos. Please make the video public or unlisted."; } else { transcribeButtonTitle = "Transcription is not available for this video type. Please make the video public or unlisted."; } } else if (alreadyTranscripted) { // Language is set, no restrictions, already transcribed transcribeButtonClass = "success"; transcribeButtonSVG = successSVG; transcribeButtonText = "1/1"; transcribeButtonTitle = "Video has already been transcribed"; } else { // Language is set, no restrictions, ready to transcribe transcribeButtonClass = "not-triggered"; transcribeButtonSVG = notriggeredSVG; transcribeButtonText = "0/1"; transcribeButtonTitle = "Click to transcribe video"; } const tableRow = document.createElement("tr"); newTableBody.appendChild(tableRow); tableRow.innerHTML = ` <div id="videoId" hidden>${videoId}</div> <div id="videoUuid" hidden>${videoUuid}</div> <div id="videoShortUuid" hidden>${videoShortUuid}</div> <div id="alreadyTranscripted-${videoId}" hidden>${alreadyTranscripted}</div> <div id="isRestricted-${videoId}" hidden>${isRestricted}</div> <div id="hasLanguage-${videoId}" hidden>${hasLanguage}</div> <td id="formattedPublishedAtCell">${formattedPublishedAt}</td> <td id="thumbnailCell"><img src="${thumbnailUrl}" alt="Thumbnail" width="100" onclick="(function() { const videoUrl = '${videoUrl}'; if (videoUrl) { window.open(videoUrl, '_blank'); } })();"></td> <td id="titleCell" class="clickable-title" onclick="(function() { const videoUrl = '${videoUrl}'; if (videoUrl) { window.open(videoUrl, '_blank'); } })();" title="Click to open video">${title}</td> <td id="descriptionCell">${description}</td> <td id="languageCell">${ videoLanguage || "No language set" }</td> <td id="actionCell" colspan="4"> <div class="action-buttons-row"> <div class="action-cell"> <button id="triggerTranscription" data-id="${videoId}" class="action-button ${transcribeButtonClass}" ${transcribeButtonDisabled ? "disabled" : ""} title="${transcribeButtonTitle}"> ${transcribeButtonSVG} <span class="counter-label">${transcribeButtonText}</span> </button> </div> <div class="action-cell"> <button id="triggerTranslation" data-id="${videoId}" class="action-button ${ totalCaptions > 0 ? "success" : "not-triggered" }"> ${totalCaptions > 0 ? successSVG : notriggeredSVG} <span class="counter-label">${totalCaptions}/26</span> </button> </div> <div class="action-cell"> <button id="triggerMetadataTranslation" data-id="${videoId}" class="action-button ${ translatedCount > 0 ? "success" : "not-triggered" }"> ${translatedCount > 0 ? successSVG : notriggeredSVG} <span class="counter-label">${translatedCount}/26</span> </button> </div> <div class="action-cell"> <button id="triggerDetail" data-id="${videoId}" class="action-button edit"> ${getEditSVG()} </button> </div> </div> </td> `; } table.appendChild(newTableBody); loadingIndicator.style.display = "none"; table.classList.remove("loading"); addEventListenerToActionButtons(); renderPagination(); }) .catch((error) => { console.log("Error:", error); loadingIndicator.style.display = "none"; table.classList.remove("loading"); }); } catch (error) { console.error("Error:", error); loadingIndicator.style.display = "none"; table.classList.remove("loading"); } } let pendingTranscriptionVideoId = null; async function transcribeVideo(videoId) { console.log("Checking transcription for video ID:", videoId); // Check if video has a language set FIRST var hasLanguageElement = document.querySelector( `#hasLanguage-${videoId}` ); var hasLanguage = false; if (hasLanguageElement) { const hasLanguageText = hasLanguageElement.innerHTML.trim(); hasLanguage = hasLanguageText === "true"; console.log( `Video ${videoId}: hasLanguageText="${hasLanguageText}", hasLanguage=${hasLanguage}` ); } // If no language is set, show language requirement modal if (!hasLanguage) { console.log("Cannot transcribe video without language set"); const modal = document.getElementById("confirmationModal"); const confirmationMessage = document.getElementById( "confirmationMessage" ); confirmationMessage.textContent = "A language must be selected for this video before transcription can be performed. Please edit the video and set a language first."; const confirmButton = document.getElementById("confirmButton"); confirmButton.textContent = "Edit Video"; const cancelButton = document.getElementById("cancelButton"); cancelButton.textContent = "Cancel"; modal.style.display = "flex"; // Set up event handlers for language requirement modal const handleEditVideo = () => { modal.style.display = "none"; showDetailVideo(videoId); confirmButton.removeEventListener("click", handleEditVideo); cancelButton.removeEventListener("click", handleCancel); modal.removeEventListener("click", handleModalClick); }; const handleCancel = () => { modal.style.display = "none"; confirmButton.removeEventListener("click", handleEditVideo); cancelButton.removeEventListener("click", handleCancel); modal.removeEventListener("click", handleModalClick); }; const handleModalClick = (event) => { if (event.target === modal) { modal.style.display = "none"; confirmButton.removeEventListener("click", handleEditVideo); cancelButton.removeEventListener("click", handleCancel); modal.removeEventListener("click", handleModalClick); } }; confirmButton.addEventListener("click", handleEditVideo); cancelButton.addEventListener("click", handleCancel); modal.addEventListener("click", handleModalClick); cancelButton.focus(); return; } // Language is set, now check other restrictions var isRestrictedElement = document.querySelector( `#isRestricted-${videoId}` ); var isRestricted = false; if (isRestrictedElement) { isRestricted = isRestrictedElement.innerHTML === "true"; } if (isRestricted) { console.log( "Cannot transcribe restricted video (not public/unlisted or password-protected)" ); notifier.error( "Cannot transcribe restricted videos. Only public and unlisted videos without password protection can be transcribed." ); return; } var alreadyTranscripted = false; var alreadyTranscriptedElement = document.querySelector( `#alreadyTranscripted-${videoId}` ); if (alreadyTranscriptedElement) { alreadyTranscripted = alreadyTranscriptedElement.innerHTML === "true"; } if (!videoId || isNaN(videoId)) { console.error("Invalid videoId provided"); notifier.error("Invalid video ID"); return; } // Language is set and no restrictions, check if already transcribed if (alreadyTranscripted) { console.log( "Video already has transcription, showing confirmation dialog" ); pendingTranscriptionVideoId = videoId; const modal = document.getElementById("confirmationModal"); const confirmationMessage = document.getElementById( "confirmationMessage" ); confirmationMessage.textContent = "There is already a transcription for this video. Do you want to overwrite it?"; const confirmButton = document.getElementById("confirmButton"); confirmButton.textContent = "Yes, Overwrite"; const cancelButton = document.getElementById("cancelButton"); cancelButton.textContent = "No, Cancel"; modal.style.display = "flex"; cancelButton.focus(); return; } // All checks passed, proceed with transcription executeTranscription(videoId); } async function executeTranscription(videoId) { const videoButton = document.querySelector( `#triggerTranscription[data-id="${videoId}"]` ); if (videoButton) { // Show processing time modal const modal = document.getElementById("confirmationModal"); const confirmationMessage = document.getElementById( "confirmationMessage" ); confirmationMessage.textContent = "Transcription process may take up to 30 minutes. If it doesn't complete within this time, please contact support at support@displayeurope.eu."; // Update modal button text const confirmButton = document.getElementById("confirmButton"); confirmButton.textContent = "OK"; modal.style.display = "flex"; // Set up event handlers for this specific confirmation const handleConfirm = () => { modal.style.display = "none"; startTranscriptionProcess(videoId, videoButton); // Remove event listeners confirmButton.removeEventListener("click", handleConfirm); cancelButton.removeEventListener("click", handleCancel); modal.removeEventListener("click", handleModalClick); }; const handleCancel = () => { modal.style.display = "none"; // Remove event listeners confirmButton.removeEventListener("click", handleConfirm); cancelButton.removeEventListener("click", handleCancel); modal.removeEventListener("click", handleModalClick); }; const handleModalClick = (event) => { if (event.target === modal) { modal.style.display = "none"; // Remove event listeners confirmButton.removeEventListener("click", handleConfirm); cancelButton.removeEventListener("click", handleCancel); modal.removeEventListener("click", handleModalClick); } }; // Add event listeners confirmButton.addEventListener("click", handleConfirm); const cancelButton = document.getElementById("cancelButton"); cancelButton.addEventListener("click", handleCancel); modal.addEventListener("click", handleModalClick); } } // Function to start the actual transcription process async function startTranscriptionProcess(videoId, videoButton) { if (videoButton) { const originalHTML = videoButton.innerHTML; videoButton.innerHTML = `${loadingSVG}<span class="counter-label">Loading...</span>`; videoButton.disabled = true; } try { const authHeaders = peertubeHelpers.getAuthHeader(); const translationResponse = await fetch( `${peertubeHelpers.getBaseRouterRoute()}/transposer/translate/${videoId}`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders, }, body: JSON.stringify({}), } ); if (!translationResponse.ok) { const errorMessage = await translationResponse.text(); throw new Error( `Translation request failed: ${translationResponse.status} ${errorMessage}` ); } const translationResult = await translationResponse.json(); console.log("Translation result:", translationResult); notifier.success( "Translation sent successfully - now wait until completed" ); if (videoButton) { videoButton.innerHTML = `${successSVG}<span class="counter-label">1/1</span>`; videoButton.classList.remove("not-triggered"); videoButton.classList.add("success"); videoButton.disabled = false; } } catch (error) { notifier.error(`Error transcribing video: ${error}`); console.error("Error transcribing video:", error); if (videoButton) { videoButton.innerHTML = `${notriggeredSVG}<span class="counter-label">0/1</span>`; videoButton.disabled = false; } } } function translateVideo(videoId) { console.log("start translate all languages for Video", videoId); window.open( `${getInstanceBaseUrl()}/p/transposer/translate?videoId=${videoId}`, "_self" ); } function translateMetadata(videoId) { console.log("start translate Metadata", videoId); window.open( `${getInstanceBaseUrl()}/p/transposer/translate/metadata?videoId=${videoId}`, "_self" ); } function showDetailVideo(videoId) { console.log("redirecting to video update page", videoId); window.open( `${getInstanceBaseUrl()}/videos/update/${videoId}`, "_self" ); } function addEventListenerToActionButtons() { const triggerTranscriptionButton = rootEl.querySelectorAll( "#triggerTranscription" ); triggerTranscriptionButton.forEach((button) => { var videoId = button.dataset.id; button.addEventListener("click", () => { // Check if button is disabled before proceeding if (button.disabled || button.classList.contains("disabled")) { return; // Do nothing if disabled } transcribeVideo(videoId); }); }); const triggerTranslationButtons = rootEl.querySelectorAll( "#triggerTranslation" ); console.log(triggerTranslationButtons); triggerTranslationButtons.forEach((button) => { console.log(button); button.addEventListener("click", () => { const videoId = button.dataset.id; translateVideo(videoId); }); }); const triggerMetadataTranslationButton = rootEl.querySelectorAll( "#triggerMetadataTranslation" ); triggerMetadataTranslationButton.forEach((button) => { button.addEventListener("click", () => { const videoId = button.dataset.id; translateMetadata(videoId); }); }); const triggerDetailButton = rootEl.querySelectorAll("#triggerDetail"); triggerDetailButton.forEach((button) => { button.addEventListener("click", () => { const videoId = button.dataset.id; showDetailVideo(videoId); }); }); const confirmButton = document.getElementById("confirmButton"); const cancelButton = document.getElementById("cancelButton"); const modal = document.getElementById("confirmationModal"); confirmButton.addEventListener("click", () => { if (pendingTranscriptionVideoId) { executeTranscription(pendingTranscriptionVideoId); pendingTranscriptionVideoId = null; } modal.style.display = "none"; }); cancelButton.addEventListener("click", () => { console.log("Transcription cancelled by user"); pendingTranscriptionVideoId = null; modal.style.display = "none"; }); modal.addEventListener("click", (event) => { if (event.target === modal) { pendingTranscriptionVideoId = null; modal.style.display = "none"; } }); } function renderPagination() { const pageButtonsContainer = document.getElementById( "pageButtonsContainer" ); pageButtonsContainer.innerHTML = ""; const currentPage = window.currentPage; window.totalPages = Math.ceil( window.totalVideos / window.videosPerPage ); let startPage = Math.max(1, currentPage - 2); let endPage = Math.min(window.totalPages, startPage + 4); if (endPage === window.totalPages) { startPage = Math.max(1, endPage - 4); } if (startPage > 1) { const firstButton = document.createElement("button"); firstButton.textContent = "«"; firstButton.classList.add("page-button"); firstButton.title = "First page"; firstButton.addEventListener("click", async () => { window.currentPage = 1; await getVideos(0, window.videosPerPage); }); pageButtonsContainer.appendChild(firstButton); } if (currentPage > 1) { const prevButton = document.createElement("button"); prevButton.textContent = "‹"; prevButton.classList.add("page-button"); prevButton.title = "Previous page"; prevButton.addEventListener("click", async () => { window.currentPage = currentPage - 1; await getVideos( (window.currentPage - 1) * window.videosPerPage, window.videosPerPage ); }); pageButtonsContainer.appendChild(prevButton); } for (let i = startPage; i <= endPage; i++) { const pageButton = document.createElement("button"); pageButton.textContent = i; pageButton.classList.add("page-button"); pageButton.disabled = i === currentPage; if (i === currentPage) { pageButton.classList.add("current-page-button"); } pageButton.addEventListener("click", async () => { if (i !== currentPage) { window.currentPage = i; await getVideos( (i - 1) * window.videosPerPage, window.videosPerPage ); } }); pageButtonsContainer.appendChild(pageButton); } if (currentPage < window.totalPages) { const nextButton = document.createElement("button"); nextButton.textContent = "›"; nextButton.classList.add("page-button"); nextButton.title = "Next page"; nextButton.addEventListener("click", async () => { window.currentPage = currentPage + 1; await getVideos( (window.currentPage - 1) * window.videosPerPage, window.videosPerPage ); }); pageButtonsContainer.appendChild(nextButton); } if (endPage < window.totalPages) { const lastButton = document.createElement("button"); lastButton.textContent = "»"; lastButton.classList.add("page-button"); lastButton.title = "Last page"; lastButton.addEventListener("click", async () => { window.currentPage = window.totalPages; await getVideos( (window.totalPages - 1) * window.videosPerPage, window.videosPerPage ); }); pageButtonsContainer.appendChild(lastButton); } const pageInfoText = document.createElement("span"); pageInfoText.classList.add("page-info"); pageInfoText.textContent = ` Page ${currentPage} of ${window.totalPages} (${window.totalVideos} videos) `; pageButtonsContainer.appendChild(pageInfoText); } function getThemeStyles() { return ` #plugincontainer { max-width: 100%; padding: 20px; position: relative; } #plugincontainer h1 { color: var(--mainColor); } #plugincontainer #formcontainer { display: flex; flex-wrap: wrap; } #plugincontainer .modifyButton, #plugincontainer .saveButton, #plugincontainer .cancelButton, #plugincontainer .deleteButton { cursor: pointer; padding: 20px; } #plugincontainer input, #plugincontainer textarea, #plugincontainer select { display: block; font-size: 16px; padding: var(--input-y-padding, 8px) var(--input-x-padding, 12px); width: 100%; max-width: 100%; color: var(--input-fg, #000); background-color: var(--input-bg, #fff); border: 1px solid var(--input-border-color, #ccc); border-radius: var(--input-border-radius, 4px); line-height: 24px; transition: background-color 0.3s ease; } #plugincontainer p { font-size: 18px; } #plugincontainer table { width: 100%; border-collapse: collapse; margin: 25px 0; font-size: 0.9em; min-width: 130px; min-height: 130px; } #plugincontainer table tr { text-align: left; } #plugincontainer table th, #plugincontainer table td { padding: 12px 15px; border-bottom: 1px solid var(--inputPlaceholderColor, #ccc); } #plugincontainer table th { background-color: var(--greyBackgroundColor, #f8f9fa); color: var(--mainForegroundColor); } /* General button styling */ #plugincontainer button { color: var(--on-primary, var(--mainForegroundColor)); background-color: var(--primary, var(--mainColor)); border: 1px solid var(--primary, var(--mainColor)); padding: var(--input-y-padding, 8px) var(--input-x-padding, 12px); border-radius: 4px; cursor: pointer; transition: background-color 0.3s; vertical-align: top; } /* Top-level action buttons */ #plugincontainer > button { width: auto; min-width: 100px; margin-right: 10px; } /* Table buttons */ #plugincontainer td button { width: auto; min-width: 80px; margin: 0 5px; } #plugincontainer button:hover { background-color: var(--secondaryColor); } #plugincontainer button:disabled { background-color: var(--greyBackgroundColor, #cccccc); color: var(--greyForegroundColor, #666666); cursor: not-allowed; } /* Action buttons like in the video list */ #plugincontainer .action-button { position: relative; width: 56px; height: 56px; display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: unset; padding: 8px; } #plugincontainer .action-button.success { background-color: var(--primary, var(--mainColor)); border-color: var(--primary, var(--mainColor)); } #plugincontainer .action-button.not-triggered { background-color: var(--greyBackgroundColor, #f8f9fa); border-color: var(--greyForegroundColor, #ccc); color: var(--greyForegroundColor, #666); } /* Clickable title styling */ #plugincontainer .clickable-title { cursor: pointer; color: var(--primary, var(--mainColor)); text-decoration: underline; transition: color 0.3s ease; } #plugincontainer .clickable-title:hover { color: var(--secondaryColor, var(--mainColor)); text-decoration: none; } /* Disabled action button styles */ #plugincontainer .action-button.disabled { background-color: var(--greyBackgroundColor, #e9ecef); border-color: var(--greyForegroundColor, #ccc); color: var(--greyForegroundColor, #6c757d); cursor: not-allowed; opacity: 0.6; } #plugincontainer .action-button.disabled:hover { background-color: var(--greyBackgroundColor, #e9ecef); cursor: not-allowed; } #plugincontainer .action-button.disabled svg { fill: var(--greyForegroundColor, #6c757d); stroke: var(--greyForegroundColor, #6c757d); opacity: 0.6; } #plugincontainer .action-button svg { width: 24px; height: 24px; margin-bottom: 6px; fill: var(--on-primary, var(--mainForegroundColor)); stroke: var(--on-primary, var(--mainForegroundColor)); } #plugincontainer .action-button.not-triggered svg { fill: var(--greyForegroundColor, #666); stroke: var(--greyForegroundColor, #666); } /* Specialized button styles */ #plugincontainer .translate-field-btn { background-color: var(--greyBackgroundColor, #6c757d); font-size: 0.8em; width: auto; padding: 2px 5px; margin-left: 5px; min-width: unset; } #plugincontainer .translate-field-btn:hover { background-color: var(--secondaryColor, #5a6268); } #plugincontainer #triggerDetail { background-color: var(--primary, var(--mainColor)); border: 1px solid var(--primary, var(--mainColor)); border-radius: 4px; padding: 6px; display: flex; align-items: center; justify-content: center; } #plugincontainer #triggerDetail svg { fill: var(--on-primary, var(--mainForegroundColor)); stroke: var(--on-primary, var(--mainForegroundColor)); } #plugincontainer .edit-icon path { stroke: var(--on-primary, var(--mainForegroundColor)); } /* Loading Circle Indicator */ #plugincontainer .loading-circle { display: flex; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000; background-color: rgba(255, 255, 255, 0.7); border-radius: 50%; width: 80px; height: 80px; justify-content: center; align-items: center; } #plugincontainer .spinner { border: 5px solid rgba(0, 0, 0, 0.1); width: 50px; height: 50px; border-radius: 50%; border-left-color: var(--primary, var(--mainColor)); animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Pagination styles */ #plugincontainer #pageButtonsContainer { margin: 15px 0; text-align: center; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 5px; } #plugincontainer .page-button { background-color: var(--primary, var(--mainColor)); min-width: 40px; height: 40px; padding: 5px 10px; margin: 0 2px; display: flex; align-items: center; justify-content: center; font-size: 14px; } #plugincontainer .current-page-button { background-color: var(--secondaryColor); font-weight: bold; } #plugincontainer .page-info { margin-left: 10px; font-size: 14px; color: var(--greyForegroundColor, #666); } /* Confirmation Modal Styles */ #plugincontainer .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: none; justify-content: center; align-items: center; z-index: 1000; } #plugincontainer .modal-content { background-color: var(--backgroundPrimary, white); padding: 20px; border-radius: 8px; width: 400px; text-align: center; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); position: relative; } #plugincontainer .modal-content h3 { margin-top: 0; color: var(--mainColor); font-size: 1.4em; margin-bottom: 15px; } #plugincontainer .modal-content p { margin-bottom: 20px; font-size: 16px; line-height: 1.5; } #plugincontainer .modal-buttons { display: flex; justify-content: center; gap: 15px; } #plugincontainer #confirmButton { background-color: var(--secondaryColor, #d9534f); border-color: var(--secondaryColor, #d9534f); } #plugincontainer #cancelButton { background-color: var(--greyBackgroundColor, #6c757d); border-color: var(--greyBackgroundColor, #6c757d); } #plugincontainer #confirmationBox { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--primary, var(--mainColor)); padding: 20px; border-radius: 5px; } #plugincontainer #confirmationBox p { margin-bottom: 20px; color: var(--on-primary, var(--mainForegroundColor)); } #plugincontainer #confirmationBox button { margin-right: 10px; padding: 8px 16px; border: none; border-radius: 3px; background-color: var(--on-primary, var(--mainForegroundColor)); color: var(--primary, var(--mainColor)); cursor: pointer; } /* Action buttons and Header Styles */ #plugincontainer .action-column-header { padding: 0; } #plugincontainer .action-headers-container { display: flex; justify-content: space-between; width: 100%; } #plugincontainer .action-header { flex: 1; text-align: center; font-size: 0.9em; font-weight: bold; padding: 8px 4px; white-space: nowrap; } #plugincontainer .action-buttons-row { display: flex; justify-content: space-between; width: 100%; } #plugincontainer .action-cell { flex: 1; display: flex; justify-content: center; align-items: center; p