peertube-plugin-transposer-connector
Version:
Transposer connector is a PeerTube language tool plugin to transcribe and translate with Whisper
1,123 lines (966 loc) • 43.5 kB
JavaScript
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 = `<!-- 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 = `
<!-- 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 = `<!-- 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 = `<!-- 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 = `<!-- 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 = `<!-- 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.label;
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;
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>
<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">${title}</td>
<td id="descriptionCell">${description}</td>
<td id="languageCell">${videoLanguage}</td>
<td id="actionCell" colspan="4">
<div class="action-buttons-row">
<div class="action-cell">
<button id="triggerTranscription" data-id="${videoId}" class="action-button ${
alreadyTranscripted ? "success" : "not-triggered"
}">
${alreadyTranscripted ? successSVG : notriggeredSVG}
<span class="counter-label">${
alreadyTranscripted ? "1/1" : "0/1"
}</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);
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;
}
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?";
// Reset button text to default if it was changed
const confirmButton = document.getElementById("confirmButton");
confirmButton.textContent = "Yes, Overwrite";
const cancelButton = document.getElementById("cancelButton");
cancelButton.textContent = "No, Cancel";
modal.style.display = "flex";
document.getElementById("cancelButton").focus();
return;
}
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", () => {
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);
}
#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;
padding: 4px;
}
#plugincontainer .action-button.label-xl {
font-size: 90%;
padding: var(--input-y-padding) calc(var(--input-x-padding) / 2) !important;
}
#plugincontainer .counter-label {
font-size: 0.75em;
margin-top: 4px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Language notice styles */
#plugincontainer .language-notice {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
}
#plugincontainer .language-notice.success {
background-color: #e6ffe6;
border: 1px solid #b3ffb3;
}
#plugincontainer .language-notice.warning {
background-color: #fff8e6;
border: 1px solid #ffe0b3;
}
#plugincontainer .language-notice p {
margin: 0;
font-size: 14px;
}
/* Field container styles */
#plugincontainer .field-container {
display: flex;
align-items: center;
width: 100%;
}
#plugincontainer .field-container input,
#plugincontainer .field-container textarea {
flex: 1;
min-width: 200px;
}
/* For the translation status colors */
#plugincontainer input[style*="background-color: #e6ffe6"],
#plugincontainer textarea[style*="background-color: #e6ffe6"] {
background-color: #e6ffe6 !important;
}
#plugincontainer input[style*="background-color: #ffe6e6"],
#plugincontainer textarea[style*="background-color: #ffe6e6"] {
background-color: #ffe6e6 !important;
}
#plugincontainer input[style*="background-color: #fff8e6"],
#plugincontainer textarea[style*="background-color: #fff8e6"] {
background-color: #fff8e6 !important;
}
/* Additional styles for making table fade when loading */
#plugincontainer #myTable {
transition: opacity 0.3s ease;
}
#plugincontainer #myTable.loading {
opacity: 0.5;
pointer-events: none;
}
/* Responsive styles for different screen sizes */
@media (max-width: 768px) {
#plugincontainer .page-button {
min-width: 32px;
height: 32px;
font-size: 12px;
padding: 3px 6px;
}
#plugincontainer .page-info {
display: block;
width: 100%;
margin-top: 10px;
text-align: center;
}
#plugincontainer table th,
#plugincontainer table td {
padding: 8px 10px;
}
#plugincontainer .action-button {
width: 48px;
height: 48px;
}
#plugincontainer .modal-content {
width: 90%;
max-width: 400px;
}
}
`;
}
function getEditSVG() {
return `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" class="edit-icon">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>`;
}
async function initialize() {
await getVideos(0, window.videosPerPage);
}
},
});
}
export { register };