1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-16 18:01:37 +00:00

refactor(js): code cleanup and add jshint comments

This commit is contained in:
Frédéric Guillot 2025-08-02 12:33:50 -07:00
parent 3e1a7e411c
commit 62410659d5
2 changed files with 248 additions and 132 deletions

View file

@ -129,6 +129,20 @@ function findEntry(element) {
return document.querySelector(".entry"); return document.querySelector(".entry");
} }
/**
* Insert an icon label element into the parent element.
*
* @param {Element} parentElement The parent element to insert the icon label into.
* @param {string} iconLabelText The text to display in the icon label.
* @returns {void}
*/
function insertIconLabelElement(parentElement, iconLabelText) {
const span = document.createElement('span');
span.classList.add('icon-label');
span.textContent = iconLabelText;
parentElement.appendChild(span);
}
/** /**
* Navigate to a specific page. * Navigate to a specific page.
* *
@ -477,15 +491,12 @@ function handleEntryStatus(navigationDirection, element, setToRead) {
} }
} }
// Add an icon-label span element. /**
function appendIconLabel(element, labelTextContent) { * Toggle the entry status between "read" and "unread".
const span = document.createElement('span'); *
span.classList.add('icon-label'); * @param {Element} element The entry element to toggle the status for.
span.textContent = labelTextContent; * @param {boolean} toasting If true, show a toast notification after toggling the status.
element.appendChild(span); */
}
// Change the entry status to the opposite value.
function toggleEntryStatus(element, toasting) { function toggleEntryStatus(element, toasting) {
const entryID = parseInt(element.dataset.id, 10); const entryID = parseInt(element.dataset.id, 10);
const link = element.querySelector(":is(a, button)[data-toggle-status]"); const link = element.querySelector(":is(a, button)[data-toggle-status]");
@ -515,7 +526,7 @@ function toggleEntryStatus(element, toasting) {
} }
link.replaceChildren(iconElement.content.cloneNode(true)); link.replaceChildren(iconElement.content.cloneNode(true));
appendIconLabel(link, label); insertIconLabelElement(link, label);
link.dataset.value = newStatus; link.dataset.value = newStatus;
if (element.classList.contains("item-status-" + currentStatus)) { if (element.classList.contains("item-status-" + currentStatus)) {
@ -529,7 +540,11 @@ function toggleEntryStatus(element, toasting) {
}); });
} }
// Mark a single entry as read. /**
* Mark the entry as read if it is currently unread.
*
* @param {Element} element The entry element to mark as read.
*/
function markEntryAsRead(element) { function markEntryAsRead(element) {
if (element.classList.contains("item-status-unread")) { if (element.classList.contains("item-status-unread")) {
element.classList.remove("item-status-unread"); element.classList.remove("item-status-unread");
@ -540,15 +555,24 @@ function markEntryAsRead(element) {
} }
} }
// Send the Ajax request to refresh all feeds in the background /**
* Handle the refresh of all feeds.
*
* This function redirects the user to the URL specified in the data-refresh-all-feeds-url attribute of the body element.
*/
function handleRefreshAllFeeds() { function handleRefreshAllFeeds() {
const url = document.body.dataset.refreshAllFeedsUrl; const refreshAllFeedsUrl = document.body.dataset.refreshAllFeedsUrl;
if (url) { if (refreshAllFeedsUrl) {
window.location.href = url; window.location.href = refreshAllFeedsUrl;
} }
} }
// Send the Ajax request to change entries statuses. /**
* Update the status of multiple entries.
*
* @param {Array<number>} entryIDs - The IDs of the entries to update.
* @param {string} status - The new status to set for the entries (e.g., "read", "unread").
*/
function updateEntriesStatus(entryIDs, status, callback) { function updateEntriesStatus(entryIDs, status, callback) {
const url = document.body.dataset.entriesStatusUrl; const url = document.body.dataset.entriesStatusUrl;
const request = new RequestBuilder(url); const request = new RequestBuilder(url);
@ -569,7 +593,11 @@ function updateEntriesStatus(entryIDs, status, callback) {
request.execute(); request.execute();
} }
// Handle save entry from list view and entry view. /**
* Handle save entry from list view and entry view.
*
* @param {Element} element
*/
function handleSaveEntry(element) { function handleSaveEntry(element) {
const toasting = !element; const toasting = !element;
const currentEntry = findEntry(element); const currentEntry = findEntry(element);
@ -578,19 +606,25 @@ function handleSaveEntry(element) {
} }
} }
// Send the Ajax request to save an entry. /**
* Save the entry by sending an Ajax request to the server.
*
* @param {Element} element The element that triggered the save action.
* @param {boolean} toasting If true, show a toast notification after saving the entry.
* @return {void}
*/
function saveEntry(element, toasting) { function saveEntry(element, toasting) {
if (!element || element.dataset.completed) { if (!element || element.dataset.completed) {
return; return;
} }
element.textContent = ""; element.textContent = "";
appendIconLabel(element, element.dataset.labelLoading); insertIconLabelElement(element, element.dataset.labelLoading);
const request = new RequestBuilder(element.dataset.saveUrl); const request = new RequestBuilder(element.dataset.saveUrl);
request.withCallback(() => { request.withCallback(() => {
element.textContent = ""; element.textContent = "";
appendIconLabel(element, element.dataset.labelDone); insertIconLabelElement(element, element.dataset.labelDone);
element.dataset.completed = "true"; element.dataset.completed = "true";
if (toasting) { if (toasting) {
const iconElement = document.querySelector("template#icon-save"); const iconElement = document.querySelector("template#icon-save");
@ -600,7 +634,11 @@ function saveEntry(element, toasting) {
request.execute(); request.execute();
} }
// Handle bookmark from the list view and entry view. /**
* Handle bookmarking an entry.
*
* @param {Element} element - The element that triggered the bookmark action.
*/
function handleBookmark(element) { function handleBookmark(element) {
const toasting = !element; const toasting = !element;
const currentEntry = findEntry(element); const currentEntry = findEntry(element);
@ -609,7 +647,13 @@ function handleBookmark(element) {
} }
} }
// Send the Ajax request and change the icon when bookmarking an entry. /**
* Toggle the bookmark status of an entry.
*
* @param {Element} parentElement - The parent element containing the bookmark button.
* @param {boolean} toasting - Whether to show a toast notification.
* @returns {void}
*/
function toggleBookmark(parentElement, toasting) { function toggleBookmark(parentElement, toasting) {
const buttonElement = parentElement.querySelector(":is(a, button)[data-toggle-bookmark]"); const buttonElement = parentElement.querySelector(":is(a, button)[data-toggle-bookmark]");
if (!buttonElement) { if (!buttonElement) {
@ -617,7 +661,7 @@ function toggleBookmark(parentElement, toasting) {
} }
buttonElement.textContent = ""; buttonElement.textContent = "";
appendIconLabel(buttonElement, buttonElement.dataset.labelLoading); insertIconLabelElement(buttonElement, buttonElement.dataset.labelLoading);
const request = new RequestBuilder(buttonElement.dataset.bookmarkUrl); const request = new RequestBuilder(buttonElement.dataset.bookmarkUrl);
request.withCallback(() => { request.withCallback(() => {
@ -640,13 +684,17 @@ function toggleBookmark(parentElement, toasting) {
} }
buttonElement.replaceChildren(iconElement.content.cloneNode(true)); buttonElement.replaceChildren(iconElement.content.cloneNode(true));
appendIconLabel(buttonElement, label); insertIconLabelElement(buttonElement, label);
buttonElement.dataset.value = newStarStatus; buttonElement.dataset.value = newStarStatus;
}); });
request.execute(); request.execute();
} }
// Send the Ajax request to download the original web page. /**
* Handle fetching the original content of an entry.
*
* @returns {void}
*/
function handleFetchOriginalContent() { function handleFetchOriginalContent() {
if (isListView()) { if (isListView()) {
return; return;
@ -660,7 +708,7 @@ function handleFetchOriginalContent() {
const previousElement = buttonElement.cloneNode(true); const previousElement = buttonElement.cloneNode(true);
buttonElement.textContent = ""; buttonElement.textContent = "";
appendIconLabel(buttonElement, buttonElement.dataset.labelLoading); insertIconLabelElement(buttonElement, buttonElement.dataset.labelLoading);
const request = new RequestBuilder(buttonElement.dataset.fetchContentUrl); const request = new RequestBuilder(buttonElement.dataset.fetchContentUrl);
request.withCallback((response) => { request.withCallback((response) => {
@ -680,6 +728,12 @@ function handleFetchOriginalContent() {
request.execute(); request.execute();
} }
/**
* Open the original link of an entry.
*
* @param {boolean} openLinkInCurrentTab - Whether to open the link in the current tab.
* @returns {void}
*/
function openOriginalLink(openLinkInCurrentTab) { function openOriginalLink(openLinkInCurrentTab) {
const entryLink = document.querySelector(".entry h1 a"); const entryLink = document.querySelector(".entry h1 a");
if (entryLink !== null) { if (entryLink !== null) {
@ -704,6 +758,12 @@ function openOriginalLink(openLinkInCurrentTab) {
} }
} }
/**
* Open the comments link of an entry.
*
* @param {boolean} openLinkInCurrentTab - Whether to open the link in the current tab.
* @returns {void}
*/
function openCommentLink(openLinkInCurrentTab) { function openCommentLink(openLinkInCurrentTab) {
if (!isListView()) { if (!isListView()) {
const entryLink = document.querySelector(":is(a, button)[data-comments-link]"); const entryLink = document.querySelector(":is(a, button)[data-comments-link]");
@ -722,6 +782,12 @@ function openCommentLink(openLinkInCurrentTab) {
} }
} }
/**
* Open the selected item in the current view.
*
* If the current view is a list view, it will navigate to the link of the currently selected item.
* If the current view is an entry view, it will navigate to the link of the entry.
*/
function openSelectedItem() { function openSelectedItem() {
const currentItemLink = document.querySelector(".current-item .item-title a"); const currentItemLink = document.querySelector(".current-item .item-title a");
if (currentItemLink !== null) { if (currentItemLink !== null) {
@ -729,6 +795,9 @@ function openSelectedItem() {
} }
} }
/**
* Unsubscribe from the feed of the currently selected item.
*/
function unsubscribeFromFeed() { function unsubscribeFromFeed() {
const unsubscribeLinks = document.querySelectorAll("[data-action=remove-feed]"); const unsubscribeLinks = document.querySelectorAll("[data-action=remove-feed]");
if (unsubscribeLinks.length === 1) { if (unsubscribeLinks.length === 1) {
@ -746,6 +815,9 @@ function unsubscribeFromFeed() {
} }
} }
/**
* Scroll the page to the currently selected item.
*/
function scrollToCurrentItem() { function scrollToCurrentItem() {
const currentItem = document.querySelector(".current-item"); const currentItem = document.querySelector(".current-item");
if (currentItem !== null) { if (currentItem !== null) {
@ -753,18 +825,33 @@ function scrollToCurrentItem() {
} }
} }
/**
* Decrement the unread counter by a specified amount.
*
* @param {number} n - The amount to decrement the counter by.
*/
function decrementUnreadCounter(n) { function decrementUnreadCounter(n) {
updateUnreadCounterValue((current) => { updateUnreadCounterValue((current) => {
return current - n; return current - n;
}); });
} }
/**
* Increment the unread counter by a specified amount.
*
* @param {number} n - The amount to increment the counter by.
*/
function incrementUnreadCounter(n) { function incrementUnreadCounter(n) {
updateUnreadCounterValue((current) => { updateUnreadCounterValue((current) => {
return current + n; return current + n;
}); });
} }
/**
* Update the unread counter value.
*
* @param {function} callback - The function to call with the old value.
*/
function updateUnreadCounterValue(callback) { function updateUnreadCounterValue(callback) {
document.querySelectorAll("span.unread-counter").forEach((element) => { document.querySelectorAll("span.unread-counter").forEach((element) => {
const oldValue = parseInt(element.textContent, 10); const oldValue = parseInt(element.textContent, 10);
@ -784,6 +871,17 @@ function updateUnreadCounterValue(callback) {
} }
} }
/**
* Handle confirmation messages for actions that require user confirmation.
*
* This function modifies the link element to show a confirmation question with "Yes" and "No" buttons.
* If the user clicks "Yes", it calls the provided callback with the URL and redirect URL.
* If the user clicks "No", it either redirects to a no-action URL or restores the link element.
*
* @param {Element} linkElement - The link or button element that triggered the confirmation.
* @param {function} callback - The callback function to execute if the user confirms the action.
* @returns {void}
*/
function handleConfirmationMessage(linkElement, callback) { function handleConfirmationMessage(linkElement, callback) {
if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") { if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") {
linkElement = linkElement.parentNode; linkElement = linkElement.parentNode;
@ -838,14 +936,21 @@ function handleConfirmationMessage(linkElement, callback) {
containerElement.appendChild(questionElement); containerElement.appendChild(questionElement);
} }
function showToast(label, iconElement) { /**
if (!label || !iconElement) { * Show a toast notification.
*
* @param {string} toastMessage - The label to display in the toast.
* @param {Element} iconElement - The icon element to display in the toast.
* @returns {void}
*/
function showToast(toastMessage, iconElement) {
if (!toastMessage || !iconElement) {
return; return;
} }
const toastMsgElement = document.getElementById("toast-msg"); const toastMsgElement = document.getElementById("toast-msg");
toastMsgElement.replaceChildren(iconElement.content.cloneNode(true)); toastMsgElement.replaceChildren(iconElement.content.cloneNode(true));
appendIconLabel(toastMsgElement, label); insertIconLabelElement(toastMsgElement, toastMessage);
const toastElementWrapper = document.getElementById("toast-wrapper"); const toastElementWrapper = document.getElementById("toast-wrapper");
toastElementWrapper.classList.remove('toast-animate'); toastElementWrapper.classList.remove('toast-animate');
@ -855,26 +960,47 @@ function showToast(label, iconElement) {
} }
/** /**
* save player position to allow to resume playback later * Check if the player is actually playing a media
* @param {Element} playerElement *
* @param mediaElement the player element itself
* @returns {boolean}
*/
function isPlayerPlaying(mediaElement) {
return mediaElement &&
mediaElement.currentTime > 0 &&
!mediaElement.paused &&
!mediaElement.ended &&
mediaElement.readyState > 2; // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
}
/**
* Handle player progression save and mark as read on completion.
*
* This function is triggered on the `timeupdate` event of the media player.
* It saves the current playback position and marks the entry as read if the completion percentage is reached.
*
* @param {Element} playerElement The media player element (audio or video).
*/ */
function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) { function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
if (!isPlayerPlaying(playerElement)) { if (!isPlayerPlaying(playerElement)) {
return; //If the player is not playing, we do not want to save the progression and mark as read on completion return;
} }
const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
const currentPositionInSeconds = Math.floor(playerElement.currentTime);
const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10); const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion); //completion percentage to mark as read const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion);
const recordInterval = 10; const recordInterval = 10;
// we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds // We limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds
if (currentPositionInSeconds >= (lastKnownPositionInSeconds + recordInterval) || if (currentPositionInSeconds >= (lastKnownPositionInSeconds + recordInterval) ||
currentPositionInSeconds <= (lastKnownPositionInSeconds - recordInterval) currentPositionInSeconds <= (lastKnownPositionInSeconds - recordInterval)
) { ) {
playerElement.dataset.lastPosition = currentPositionInSeconds.toString(); playerElement.dataset.lastPosition = currentPositionInSeconds.toString();
const request = new RequestBuilder(playerElement.dataset.saveUrl); const request = new RequestBuilder(playerElement.dataset.saveUrl);
request.withBody({ progression: currentPositionInSeconds }); request.withBody({ progression: currentPositionInSeconds });
request.execute(); request.execute();
// Handle the mark as read on completion // Handle the mark as read on completion
if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) { if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) {
const completion = currentPositionInSeconds / playerElement.duration; const completion = currentPositionInSeconds / playerElement.duration;
@ -886,54 +1012,69 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
} }
/** /**
* Check if the player is actually playing a media * Handle media control actions like seeking and changing playback speed.
* @param element the player element itself *
* @returns {boolean} * This function is triggered by clicking on media control buttons.
* It adjusts the playback position or speed of media elements with the same enclosure ID.
*
* @param {Element} mediaPlayerButtonElement
*/ */
function isPlayerPlaying(element) { function handleMediaControlButtonClick(mediaPlayerButtonElement) {
return element && const actionType = mediaPlayerButtonElement.dataset.enclosureAction;
element.currentTime > 0 && const actionValue = parseFloat(mediaPlayerButtonElement.dataset.actionValue);
!element.paused && const enclosureID = mediaPlayerButtonElement.dataset.enclosureId;
!element.ended && const mediaElements = document.querySelectorAll(`audio[data-enclosure-id="${enclosureID}"],video[data-enclosure-id="${enclosureID}"]`);
element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState const speedIndicatorElements = document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${enclosureID}"]`);
} mediaElements.forEach((mediaElement) => {
switch (actionType) {
/**
* Handle all clicks on media player controls button on enclosures.
* Will change the current speed and position of the player accordingly.
* Will not save anything, all is done client-side, however, changing the position
* will trigger the handlePlayerProgressionSave and save the new position backends side.
* @param {Element} button
*/
function handleMediaControl(button) {
const action = button.dataset.enclosureAction;
const value = parseFloat(button.dataset.actionValue);
const targetEnclosureId = button.dataset.enclosureId;
const enclosures = document.querySelectorAll(`audio[data-enclosure-id="${targetEnclosureId}"],video[data-enclosure-id="${targetEnclosureId}"]`);
const speedIndicator = document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${targetEnclosureId}"]`);
enclosures.forEach((enclosure) => {
switch (action) {
case "seek": case "seek":
enclosure.currentTime = Math.max(enclosure.currentTime + value, 0); mediaElement.currentTime = Math.max(mediaElement.currentTime + actionValue, 0);
break; break;
case "speed": case "speed":
// I set a floor speed of 0.25 to avoid too slow speed where it gives the impression it stopped. // 0.25 was chosen because it will allow to get back to 1x in two "faster" clicks.
// 0.25 was chosen because it will allow to get back to 1x in two "faster" click, and lower value with same property would be 0. // A lower value would result in a playback rate of 0, effectively pausing playback.
enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value); mediaElement.playbackRate = Math.max(0.25, mediaElement.playbackRate + actionValue);
speedIndicator.forEach((speedI) => { speedIndicatorElements.forEach((speedIndicatorElement) => {
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters. speedIndicatorElement.innerText = `${mediaElement.playbackRate.toFixed(2)}x`;
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
}); });
break; break;
case "speed-reset": case "speed-reset":
enclosure.playbackRate = value ; mediaElement.playbackRate = actionValue ;
speedIndicator.forEach((speedI) => { speedIndicatorElements.forEach((speedIndicatorElement) => {
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters. // Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature // The trick only works on rates less than 10, but it feels an acceptable trade-off considering the feature
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; speedIndicatorElement.innerText = `${mediaElement.playbackRate.toFixed(2)}x`;
}); });
break; break;
} }
}); });
} }
/**
* Initialize media player event handlers.
*/
function initializeMediaPlayerHandlers() {
document.querySelectorAll("button[data-enclosure-action]").forEach((element) => {
element.addEventListener("click", () => handleMediaControlButtonClick(element));
});
// Set playback from the last position if available
document.querySelectorAll("audio[data-last-position],video[data-last-position]").forEach((element) => {
if (element.dataset.lastPosition) {
element.currentTime = element.dataset.lastPosition;
}
element.ontimeupdate = () => handlePlayerProgressionSaveAndMarkAsReadOnCompletion(element);
});
// Set playback speed from the data attribute if available
document.querySelectorAll("audio[data-playback-rate],video[data-playback-rate]").forEach((element) => {
if (element.dataset.playbackRate) {
element.playbackRate = element.dataset.playbackRate;
if (element.dataset.enclosureId) {
document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${element.dataset.enclosureId}"]`).forEach((speedIndicatorElement) => {
speedIndicatorElement.innerText = `${parseFloat(element.dataset.playbackRate).toFixed(2)}x`;
});
}
}
});
}

View file

@ -1,5 +1,7 @@
disableSubmitButtonsOnFormSubmit(); disableSubmitButtonsOnFormSubmit();
initializeMediaPlayerHandlers();
// Initialize the keyboard shortcuts if enabled.
if (!document.querySelector("body[data-disable-keyboard-shortcuts=true]")) { if (!document.querySelector("body[data-disable-keyboard-shortcuts=true]")) {
const keyboardHandler = new KeyboardHandler(); const keyboardHandler = new KeyboardHandler();
keyboardHandler.on("g u", () => goToPage("unread")); keyboardHandler.on("g u", () => goToPage("unread"));
@ -47,40 +49,11 @@ if (!document.querySelector("body[data-disable-keyboard-shortcuts=true]")) {
keyboardHandler.listen(); keyboardHandler.listen();
} }
// Initialize the touch handler for mobile devices.
const touchHandler = new TouchHandler(); const touchHandler = new TouchHandler();
touchHandler.listen(); touchHandler.listen();
if (WebAuthnHandler.isWebAuthnSupported()) { // Initialize click handlers.
const webauthnHandler = new WebAuthnHandler();
onClick("#webauthn-delete", () => { webauthnHandler.removeAllCredentials(); });
const registerButton = document.getElementById("webauthn-register");
if (registerButton !== null) {
registerButton.disabled = false;
onClick("#webauthn-register", () => {
webauthnHandler.register().catch((err) => WebAuthnHandler.showErrorMessage(err));
});
}
const loginButton = document.getElementById("webauthn-login");
if (loginButton !== null) {
const abortController = new AbortController();
loginButton.disabled = false;
onClick("#webauthn-login", () => {
const usernameField = document.getElementById("form-username");
if (usernameField !== null) {
abortController.abort();
webauthnHandler.login(usernameField.value).catch(err => WebAuthnHandler.showErrorMessage(err));
}
});
webauthnHandler.conditionalLogin(abortController).catch(err => WebAuthnHandler.showErrorMessage(err));
}
}
onClick(":is(a, button)[data-save-entry]", (event) => handleSaveEntry(event.target)); onClick(":is(a, button)[data-save-entry]", (event) => handleSaveEntry(event.target));
onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmark(event.target)); onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmark(event.target));
onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent); onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent);
@ -137,6 +110,7 @@ if ("serviceWorker" in navigator) {
} }
} }
// PWA install prompt handling.
window.addEventListener('beforeinstallprompt', (e) => { window.addEventListener('beforeinstallprompt', (e) => {
let deferredPrompt = e; let deferredPrompt = e;
const promptHomeScreen = document.getElementById('prompt-home-screen'); const promptHomeScreen = document.getElementById('prompt-home-screen');
@ -157,33 +131,34 @@ window.addEventListener('beforeinstallprompt', (e) => {
} }
}); });
// Save and resume media position // PassKey handling.
const lastPositionElements = document.querySelectorAll("audio[data-last-position],video[data-last-position]"); if (WebAuthnHandler.isWebAuthnSupported()) {
lastPositionElements.forEach((element) => { const webauthnHandler = new WebAuthnHandler();
if (element.dataset.lastPosition) {
element.currentTime = element.dataset.lastPosition;
}
element.ontimeupdate = () => handlePlayerProgressionSaveAndMarkAsReadOnCompletion(element);
});
// Set media playback rate onClick("#webauthn-delete", () => { webauthnHandler.removeAllCredentials(); });
const playbackRateElements = document.querySelectorAll("audio[data-playback-rate],video[data-playback-rate]");
playbackRateElements.forEach((element) => { const registerButton = document.getElementById("webauthn-register");
if (element.dataset.playbackRate) { if (registerButton !== null) {
element.playbackRate = element.dataset.playbackRate; registerButton.disabled = false;
if (element.dataset.enclosureId){
// In order to display properly the speed we need to do it on bootstrap. onClick("#webauthn-register", () => {
// Could not do it backend side because I didn't know how to do it because of the template inclusion and webauthnHandler.register().catch((err) => WebAuthnHandler.showErrorMessage(err));
// the way the initial playback speed is handled. See enclosure_media_controls.html if you want to try to fix this
document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${element.dataset.enclosureId}"]`).forEach((speedI)=>{
speedI.innerText = `${parseFloat(element.dataset.playbackRate).toFixed(2)}x`;
}); });
} }
}
});
// Set enclosure media controls handlers const loginButton = document.getElementById("webauthn-login");
const mediaControlsElements = document.querySelectorAll("button[data-enclosure-action]"); if (loginButton !== null) {
mediaControlsElements.forEach((element) => { const abortController = new AbortController();
element.addEventListener("click", () => handleMediaControl(element)); loginButton.disabled = false;
});
onClick("#webauthn-login", () => {
const usernameField = document.getElementById("form-username");
if (usernameField !== null) {
abortController.abort();
webauthnHandler.login(usernameField.value).catch(err => WebAuthnHandler.showErrorMessage(err));
}
});
webauthnHandler.conditionalLogin(abortController).catch(err => WebAuthnHandler.showErrorMessage(err));
}
}