diff --git a/internal/database/migrations.go b/internal/database/migrations.go index e48217ad81a..065d271ee54 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -903,4 +903,9 @@ var migrations = []func(tx *sql.Tx) error{ _, err = tx.Exec(sql) return err }, + func(tx *sql.Tx) (err error) { + sql := `ALTER TABLE users ADD COLUMN cache_for_offline boolean default 'f'` + _, err = tx.Exec(sql) + return err + }, } diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 71867ffe3e2..ca4dbeed5fa 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -377,6 +377,7 @@ "form.prefs.label.default_home_page": "Default home page", "form.prefs.label.categories_sorting_order": "Categories sorting", "form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed", + "form.prefs.label.cache_for_offline": "Cache entries for offline reading", "form.prefs.fieldset.application_settings": "Application Settings", "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", diff --git a/internal/model/user.go b/internal/model/user.go index 62aff600795..abf58afd234 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -36,6 +36,7 @@ type User struct { CategoriesSortingOrder string `json:"categories_sorting_order"` MarkReadOnView bool `json:"mark_read_on_view"` MediaPlaybackRate float64 `json:"media_playback_rate"` + CacheForOffline bool `json:"cache_for_offline"` } // UserCreationRequest represents the request to create a user. @@ -72,6 +73,7 @@ type UserModificationRequest struct { CategoriesSortingOrder *string `json:"categories_sorting_order"` MarkReadOnView *bool `json:"mark_read_on_view"` MediaPlaybackRate *float64 `json:"media_playback_rate"` + CacheForOffline bool `json:"cache_for_offline"` } // Patch updates the User object with the modification request. @@ -167,6 +169,10 @@ func (u *UserModificationRequest) Patch(user *User) { if u.MediaPlaybackRate != nil { user.MediaPlaybackRate = *u.MediaPlaybackRate } + + if u.CacheForOffline { + user.CacheForOffline = u.CacheForOffline + } } // UseTimezone converts last login date to the given timezone. diff --git a/internal/storage/user.go b/internal/storage/user.go index 4f30ac0da3e..552982d88e3 100644 --- a/internal/storage/user.go +++ b/internal/storage/user.go @@ -92,7 +92,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m default_home_page, categories_sorting_order, mark_read_on_view, - media_playback_rate + media_playback_rate, + cache_for_offline ` tx, err := s.db.Begin() @@ -132,6 +133,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m &user.CategoriesSortingOrder, &user.MarkReadOnView, &user.MediaPlaybackRate, + &user.CacheForOffline, ) if err != nil { tx.Rollback() @@ -189,9 +191,10 @@ func (s *Storage) UpdateUser(user *model.User) error { default_home_page=$20, categories_sorting_order=$21, mark_read_on_view=$22, - media_playback_rate=$23 + media_playback_rate=$23, + cache_for_offline=$24 WHERE - id=$24 + id=$25 ` _, err = s.db.Exec( @@ -219,6 +222,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.CategoriesSortingOrder, user.MarkReadOnView, user.MediaPlaybackRate, + user.CacheForOffline, user.ID, ) if err != nil { @@ -248,9 +252,10 @@ func (s *Storage) UpdateUser(user *model.User) error { default_home_page=$19, categories_sorting_order=$20, mark_read_on_view=$21, - media_playback_rate=$22 + media_playback_rate=$22, + cache_for_offline=$23 WHERE - id=$23 + id=$24 ` _, err := s.db.Exec( @@ -277,6 +282,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.CategoriesSortingOrder, user.MarkReadOnView, user.MediaPlaybackRate, + user.CacheForOffline, user.ID, ) @@ -325,7 +331,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) { default_home_page, categories_sorting_order, mark_read_on_view, - media_playback_rate + media_playback_rate, + cache_for_offline FROM users WHERE @@ -361,7 +368,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) { default_home_page, categories_sorting_order, mark_read_on_view, - media_playback_rate + media_playback_rate, + cache_for_offline FROM users WHERE @@ -397,7 +405,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) { default_home_page, categories_sorting_order, mark_read_on_view, - media_playback_rate + media_playback_rate, + cache_for_offline FROM users WHERE @@ -440,7 +449,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) { u.default_home_page, u.categories_sorting_order, u.mark_read_on_view, - media_playback_rate + media_playback_rate, + u.cache_for_offline FROM users u LEFT JOIN @@ -478,6 +488,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err &user.CategoriesSortingOrder, &user.MarkReadOnView, &user.MediaPlaybackRate, + &user.CacheForOffline, ) if err == sql.ErrNoRows { @@ -586,7 +597,8 @@ func (s *Storage) Users() (model.Users, error) { default_home_page, categories_sorting_order, mark_read_on_view, - media_playback_rate + media_playback_rate, + cache_for_offline FROM users ORDER BY username ASC @@ -625,6 +637,7 @@ func (s *Storage) Users() (model.Users, error) { &user.CategoriesSortingOrder, &user.MarkReadOnView, &user.MediaPlaybackRate, + &user.CacheForOffline, ) if err != nil { diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html index 0be77f626ba..c78cbc9b075 100644 --- a/internal/template/templates/views/settings.html +++ b/internal/template/templates/views/settings.html @@ -114,6 +114,7 @@

{{ t "page.settings.title" }}

+
diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go index a46d97145a8..8ce0d5a8401 100644 --- a/internal/ui/form/settings.go +++ b/internal/ui/form/settings.go @@ -34,6 +34,7 @@ type SettingsForm struct { CategoriesSortingOrder string MarkReadOnView bool MediaPlaybackRate float64 + CacheForOffline bool } // Merge updates the fields of the given user. @@ -57,6 +58,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User { user.CategoriesSortingOrder = s.CategoriesSortingOrder user.MarkReadOnView = s.MarkReadOnView user.MediaPlaybackRate = s.MediaPlaybackRate + user.CacheForOffline = s.CacheForOffline if s.Password != "" { user.Password = s.Password @@ -133,5 +135,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm { CategoriesSortingOrder: r.FormValue("categories_sorting_order"), MarkReadOnView: r.FormValue("mark_read_on_view") == "1", MediaPlaybackRate: mediaPlaybackRate, + CacheForOffline: r.FormValue("cache_for_offline") == "1", } } diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go index 3a96b29ce34..b3ab9b38e23 100644 --- a/internal/ui/settings_show.go +++ b/internal/ui/settings_show.go @@ -42,6 +42,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { CategoriesSortingOrder: user.CategoriesSortingOrder, MarkReadOnView: user.MarkReadOnView, MediaPlaybackRate: user.MediaPlaybackRate, + CacheForOffline: user.CacheForOffline, } timezones, err := h.store.Timezones() diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 03f2880e0bc..39d11314102 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -1,144 +1,161 @@ // OnClick attaches a listener to the elements that match the selector. function onClick(selector, callback, noPreventDefault) { - document.querySelectorAll(selector).forEach((element) => { - element.onclick = (event) => { - if (!noPreventDefault) { - event.preventDefault(); - } - callback(event); - }; - }); + document.querySelectorAll(selector).forEach((element) => { + element.onclick = (event) => { + if (!noPreventDefault) { + event.preventDefault(); + } + callback(event); + }; + }); } function onAuxClick(selector, callback, noPreventDefault) { - document.querySelectorAll(selector).forEach((element) => { - element.onauxclick = (event) => { - if (!noPreventDefault) { - event.preventDefault(); - } - callback(event); - }; - }); + document.querySelectorAll(selector).forEach((element) => { + element.onauxclick = (event) => { + if (!noPreventDefault) { + event.preventDefault(); + } + callback(event); + }; + }); } // make logo element as button on mobile layout function checkMenuToggleModeByLayout() { - const logoElement = document.querySelector(".logo"); - if (!logoElement) return; - - const homePageLinkElement = document.querySelector(".logo > a"); - - if (document.documentElement.clientWidth < 620) { - const navMenuElement = document.getElementById("header-menu"); - const navMenuElementIsExpanded = navMenuElement.classList.contains("js-menu-show"); - const logoToggleButtonLabel = logoElement.getAttribute("data-toggle-button-label"); - logoElement.setAttribute("role", "button"); - logoElement.setAttribute("tabindex", "0"); - logoElement.setAttribute("aria-label", logoToggleButtonLabel); - logoElement.setAttribute("aria-expanded", navMenuElementIsExpanded?"true":"false"); - homePageLinkElement.setAttribute("tabindex", "-1"); - } else { - logoElement.removeAttribute("role"); - logoElement.removeAttribute("tabindex"); - logoElement.removeAttribute("aria-expanded"); - logoElement.removeAttribute("aria-label"); - homePageLinkElement.removeAttribute("tabindex"); - } + const logoElement = document.querySelector(".logo"); + if (!logoElement) return; + + const homePageLinkElement = document.querySelector(".logo > a"); + + if (document.documentElement.clientWidth < 620) { + const navMenuElement = document.getElementById("header-menu"); + const navMenuElementIsExpanded = + navMenuElement.classList.contains("js-menu-show"); + const logoToggleButtonLabel = logoElement.getAttribute( + "data-toggle-button-label", + ); + logoElement.setAttribute("role", "button"); + logoElement.setAttribute("tabindex", "0"); + logoElement.setAttribute("aria-label", logoToggleButtonLabel); + logoElement.setAttribute( + "aria-expanded", + navMenuElementIsExpanded ? "true" : "false", + ); + homePageLinkElement.setAttribute("tabindex", "-1"); + } else { + logoElement.removeAttribute("role"); + logoElement.removeAttribute("tabindex"); + logoElement.removeAttribute("aria-expanded"); + logoElement.removeAttribute("aria-label"); + homePageLinkElement.removeAttribute("tabindex"); + } } function fixVoiceOverDetailsSummaryBug() { - document.querySelectorAll("details").forEach((details) => { - const summaryElement = details.querySelector("summary"); - summaryElement.setAttribute("role", "button"); - summaryElement.setAttribute("aria-expanded", details.open? "true": "false"); - - details.addEventListener("toggle", () => { - summaryElement.setAttribute("aria-expanded", details.open? "true": "false"); - }); + document.querySelectorAll("details").forEach((details) => { + const summaryElement = details.querySelector("summary"); + summaryElement.setAttribute("role", "button"); + summaryElement.setAttribute( + "aria-expanded", + details.open ? "true" : "false", + ); + + details.addEventListener("toggle", () => { + summaryElement.setAttribute( + "aria-expanded", + details.open ? "true" : "false", + ); }); + }); } // Show and hide the main menu on mobile devices. function toggleMainMenu(event) { - if (event.type === "keydown" && !(event.key === "Enter" || event.key === " ")) { - return; - } - - if (event.currentTarget.getAttribute("role")) { - event.preventDefault(); - } - - const menu = document.querySelector(".header nav ul"); - const menuToggleButton = document.querySelector(".logo"); - if (menu.classList.contains("js-menu-show")) { - menu.classList.remove("js-menu-show"); - menuToggleButton.setAttribute("aria-expanded", false); - } else { - menu.classList.add("js-menu-show"); - menuToggleButton.setAttribute("aria-expanded", true); - } + if ( + event.type === "keydown" && + !(event.key === "Enter" || event.key === " ") + ) { + return; + } + + if (event.currentTarget.getAttribute("role")) { + event.preventDefault(); + } + + const menu = document.querySelector(".header nav ul"); + const menuToggleButton = document.querySelector(".logo"); + if (menu.classList.contains("js-menu-show")) { + menu.classList.remove("js-menu-show"); + menuToggleButton.setAttribute("aria-expanded", false); + } else { + menu.classList.add("js-menu-show"); + menuToggleButton.setAttribute("aria-expanded", true); + } } // Handle click events for the main menu (
  • and ). function onClickMainMenuListItem(event) { - const element = event.target; + const element = event.target; - if (element.tagName === "A") { - window.location.href = element.getAttribute("href"); - } else { - const linkElement = element.querySelector("a") || element.closest("a"); - window.location.href = linkElement.getAttribute("href"); - } + if (element.tagName === "A") { + window.location.href = element.getAttribute("href"); + } else { + const linkElement = element.querySelector("a") || element.closest("a"); + window.location.href = linkElement.getAttribute("href"); + } } // Change the button label when the page is loading. function handleSubmitButtons() { - document.querySelectorAll("form").forEach((element) => { - element.onsubmit = () => { - const button = element.querySelector("button"); - if (button) { - button.textContent = button.dataset.labelLoading; - button.disabled = true; - } - }; - }); + document.querySelectorAll("form").forEach((element) => { + element.onsubmit = () => { + const button = element.querySelector("button"); + if (button) { + button.textContent = button.dataset.labelLoading; + button.disabled = true; + } + }; + }); } // Show modal dialog with the list of keyboard shortcuts. function showKeyboardShortcuts() { - const template = document.getElementById("keyboard-shortcuts"); - if (template !== null) { - ModalHandler.open(template.content, "dialog-title"); - } + const template = document.getElementById("keyboard-shortcuts"); + if (template !== null) { + ModalHandler.open(template.content, "dialog-title"); + } } // Mark as read visible items of the current page. function markPageAsRead() { - const items = DomHelper.getVisibleElements(".items .item"); - const entryIDs = []; - - items.forEach((element) => { - element.classList.add("item-status-read"); - entryIDs.push(parseInt(element.dataset.id, 10)); + const items = DomHelper.getVisibleElements(".items .item"); + const entryIDs = []; + + items.forEach((element) => { + element.classList.add("item-status-read"); + entryIDs.push(parseInt(element.dataset.id, 10)); + }); + + if (entryIDs.length > 0) { + updateEntriesStatus(entryIDs, "read", () => { + // Make sure the Ajax request reach the server before we reload the page. + + const element = document.querySelector( + ":is(a, button)[data-action=markPageAsRead]", + ); + let showOnlyUnread = false; + if (element) { + showOnlyUnread = element.dataset.showOnlyUnread || false; + } + + if (showOnlyUnread) { + window.location.href = window.location.href; + } else { + goToPage("next", true); + } }); - - if (entryIDs.length > 0) { - updateEntriesStatus(entryIDs, "read", () => { - // Make sure the Ajax request reach the server before we reload the page. - - const element = document.querySelector(":is(a, button)[data-action=markPageAsRead]"); - let showOnlyUnread = false; - if (element) { - showOnlyUnread = element.dataset.showOnlyUnread || false; - } - - if (showOnlyUnread) { - window.location.href = window.location.href; - } else { - goToPage("next", true); - } - }); - } + } } /** @@ -149,286 +166,314 @@ function markPageAsRead() { * @param {boolean} setToRead */ function handleEntryStatus(item, element, setToRead) { - const toasting = !element; - const currentEntry = findEntry(element); - if (currentEntry) { - if (!setToRead || currentEntry.querySelector(":is(a, button)[data-toggle-status]").dataset.value == "unread") { - toggleEntryStatus(currentEntry, toasting); - } - if (isListView() && currentEntry.classList.contains('current-item')) { - switch (item) { - case "previous": - goToListItem(-1); - break; - case "next": - goToListItem(1); - break; - } - } + const toasting = !element; + const currentEntry = findEntry(element); + if (currentEntry) { + if ( + !setToRead || + currentEntry.querySelector(":is(a, button)[data-toggle-status]").dataset + .value == "unread" + ) { + toggleEntryStatus(currentEntry, toasting); } + if (isListView() && currentEntry.classList.contains("current-item")) { + switch (item) { + case "previous": + goToListItem(-1); + break; + case "next": + goToListItem(1); + break; + } + } + } } // Add an icon-label span element. function appendIconLabel(element, labelTextContent) { - const span = document.createElement('span'); - span.classList.add('icon-label'); - span.textContent = labelTextContent; - element.appendChild(span); + const span = document.createElement("span"); + span.classList.add("icon-label"); + span.textContent = labelTextContent; + element.appendChild(span); } // Change the entry status to the opposite value. function toggleEntryStatus(element, toasting) { - const entryID = parseInt(element.dataset.id, 10); - const link = element.querySelector(":is(a, button)[data-toggle-status]"); - - const currentStatus = link.dataset.value; - const newStatus = currentStatus === "read" ? "unread" : "read"; - - link.querySelector("span").textContent = link.dataset.labelLoading; - updateEntriesStatus([entryID], newStatus, () => { - let iconElement, label; - - if (currentStatus === "read") { - iconElement = document.querySelector("template#icon-read"); - label = link.dataset.labelRead; - if (toasting) { - showToast(link.dataset.toastUnread, iconElement); - } - } else { - iconElement = document.querySelector("template#icon-unread"); - label = link.dataset.labelUnread; - if (toasting) { - showToast(link.dataset.toastRead, iconElement); - } - } + const entryID = parseInt(element.dataset.id, 10); + const link = element.querySelector(":is(a, button)[data-toggle-status]"); + + const currentStatus = link.dataset.value; + const newStatus = currentStatus === "read" ? "unread" : "read"; + + link.querySelector("span").textContent = link.dataset.labelLoading; + updateEntriesStatus([entryID], newStatus, () => { + let iconElement, label; + + if (currentStatus === "read") { + iconElement = document.querySelector("template#icon-read"); + label = link.dataset.labelRead; + if (toasting) { + showToast(link.dataset.toastUnread, iconElement); + } + } else { + iconElement = document.querySelector("template#icon-unread"); + label = link.dataset.labelUnread; + if (toasting) { + showToast(link.dataset.toastRead, iconElement); + } + } - link.replaceChildren(iconElement.content.cloneNode(true)); - appendIconLabel(link, label); - link.dataset.value = newStatus; + link.replaceChildren(iconElement.content.cloneNode(true)); + appendIconLabel(link, label); + link.dataset.value = newStatus; - if (element.classList.contains("item-status-" + currentStatus)) { - element.classList.remove("item-status-" + currentStatus); - element.classList.add("item-status-" + newStatus); - } - }); + if (element.classList.contains("item-status-" + currentStatus)) { + element.classList.remove("item-status-" + currentStatus); + element.classList.add("item-status-" + newStatus); + } + }); } // Mark a single entry as read. function markEntryAsRead(element) { - if (element.classList.contains("item-status-unread")) { - element.classList.remove("item-status-unread"); - element.classList.add("item-status-read"); + if (element.classList.contains("item-status-unread")) { + element.classList.remove("item-status-unread"); + element.classList.add("item-status-read"); - const entryID = parseInt(element.dataset.id, 10); - updateEntriesStatus([entryID], "read"); - } + const entryID = parseInt(element.dataset.id, 10); + updateEntriesStatus([entryID], "read"); + } } // Send the Ajax request to refresh all feeds in the background function handleRefreshAllFeeds() { - const url = document.body.dataset.refreshAllFeedsUrl; - if (url) { - window.location.href = url; - } + const url = document.body.dataset.refreshAllFeedsUrl; + if (url) { + window.location.href = url; + } } // Send the Ajax request to change entries statuses. function updateEntriesStatus(entryIDs, status, callback) { - const url = document.body.dataset.entriesStatusUrl; - const request = new RequestBuilder(url); - request.withBody({ entry_ids: entryIDs, status: status }); - request.withCallback((resp) => { - resp.json().then(count => { - if (callback) { - callback(resp); - } - - if (status === "read") { - decrementUnreadCounter(count); - } else { - incrementUnreadCounter(count); - } - }); + const url = document.body.dataset.entriesStatusUrl; + const request = new RequestBuilder(url); + request.withBody({ entry_ids: entryIDs, status: status }); + request.withCallback((resp) => { + resp.json().then((count) => { + if (callback) { + callback(resp); + } + + if (status === "read") { + decrementUnreadCounter(count); + } else { + incrementUnreadCounter(count); + } }); - request.execute(); + }); + request.execute(); } // Handle save entry from list view and entry view. function handleSaveEntry(element) { - const toasting = !element; - const currentEntry = findEntry(element); - if (currentEntry) { - saveEntry(currentEntry.querySelector(":is(a, button)[data-save-entry]"), toasting); - } + const toasting = !element; + const currentEntry = findEntry(element); + if (currentEntry) { + saveEntry( + currentEntry.querySelector(":is(a, button)[data-save-entry]"), + toasting, + ); + } } // Send the Ajax request to save an entry. function saveEntry(element, toasting) { - if (!element || element.dataset.completed) { - return; - } + if (!element || element.dataset.completed) { + return; + } - element.textContent = ""; - appendIconLabel(element, element.dataset.labelLoading); + element.textContent = ""; + appendIconLabel(element, element.dataset.labelLoading); - const request = new RequestBuilder(element.dataset.saveUrl); - request.withCallback(() => { - element.textContent = ""; - appendIconLabel(element, element.dataset.labelDone); - element.dataset.completed = true; - if (toasting) { - const iconElement = document.querySelector("template#icon-save"); - showToast(element.dataset.toastDone, iconElement); - } - }); - request.execute(); + const request = new RequestBuilder(element.dataset.saveUrl); + request.withCallback(() => { + element.textContent = ""; + appendIconLabel(element, element.dataset.labelDone); + element.dataset.completed = true; + if (toasting) { + const iconElement = document.querySelector("template#icon-save"); + showToast(element.dataset.toastDone, iconElement); + } + }); + request.execute(); } // Handle bookmark from the list view and entry view. function handleBookmark(element) { - const toasting = !element; - const currentEntry = findEntry(element); - if (currentEntry) { - toggleBookmark(currentEntry, toasting); - } + const toasting = !element; + const currentEntry = findEntry(element); + if (currentEntry) { + toggleBookmark(currentEntry, toasting); + } } // Send the Ajax request and change the icon when bookmarking an entry. function toggleBookmark(parentElement, toasting) { - const buttonElement = parentElement.querySelector(":is(a, button)[data-toggle-bookmark]"); - if (!buttonElement) { - return; + const buttonElement = parentElement.querySelector( + ":is(a, button)[data-toggle-bookmark]", + ); + if (!buttonElement) { + return; + } + + buttonElement.textContent = ""; + appendIconLabel(buttonElement, buttonElement.dataset.labelLoading); + + const request = new RequestBuilder(buttonElement.dataset.bookmarkUrl); + request.withCallback(() => { + const currentStarStatus = buttonElement.dataset.value; + const newStarStatus = currentStarStatus === "star" ? "unstar" : "star"; + + let iconElement, label; + if (currentStarStatus === "star") { + iconElement = document.querySelector("template#icon-star"); + label = buttonElement.dataset.labelStar; + if (toasting) { + showToast(buttonElement.dataset.toastUnstar, iconElement); + } + } else { + iconElement = document.querySelector("template#icon-unstar"); + label = buttonElement.dataset.labelUnstar; + if (toasting) { + showToast(buttonElement.dataset.toastStar, iconElement); + } } - buttonElement.textContent = ""; - appendIconLabel(buttonElement, buttonElement.dataset.labelLoading); - - const request = new RequestBuilder(buttonElement.dataset.bookmarkUrl); - request.withCallback(() => { - const currentStarStatus = buttonElement.dataset.value; - const newStarStatus = currentStarStatus === "star" ? "unstar" : "star"; - - let iconElement, label; - if (currentStarStatus === "star") { - iconElement = document.querySelector("template#icon-star"); - label = buttonElement.dataset.labelStar; - if (toasting) { - showToast(buttonElement.dataset.toastUnstar, iconElement); - } - } else { - iconElement = document.querySelector("template#icon-unstar"); - label = buttonElement.dataset.labelUnstar; - if (toasting) { - showToast(buttonElement.dataset.toastStar, iconElement); - } - } - - buttonElement.replaceChildren(iconElement.content.cloneNode(true)); - appendIconLabel(buttonElement, label); - buttonElement.dataset.value = newStarStatus; - }); - request.execute(); + buttonElement.replaceChildren(iconElement.content.cloneNode(true)); + appendIconLabel(buttonElement, label); + buttonElement.dataset.value = newStarStatus; + }); + request.execute(); } // Send the Ajax request to download the original web page. function handleFetchOriginalContent() { - if (isListView()) { - return; - } + if (isListView()) { + return; + } - const buttonElement = document.querySelector(":is(a, button)[data-fetch-content-entry]"); - if (!buttonElement) { - return; - } + const buttonElement = document.querySelector( + ":is(a, button)[data-fetch-content-entry]", + ); + if (!buttonElement) { + return; + } - const previousElement = buttonElement.cloneNode(true); + const previousElement = buttonElement.cloneNode(true); + buttonElement.textContent = ""; + appendIconLabel(buttonElement, buttonElement.dataset.labelLoading); + + const request = new RequestBuilder(buttonElement.dataset.fetchContentUrl); + request.withCallback((response) => { buttonElement.textContent = ""; - appendIconLabel(buttonElement, buttonElement.dataset.labelLoading); - - const request = new RequestBuilder(buttonElement.dataset.fetchContentUrl); - request.withCallback((response) => { - buttonElement.textContent = ''; - buttonElement.appendChild(previousElement); - - response.json().then((data) => { - if (data.hasOwnProperty("content") && data.hasOwnProperty("reading_time")) { - document.querySelector(".entry-content").innerHTML = ttpolicy.createHTML(data.content); - const entryReadingtimeElement = document.querySelector(".entry-reading-time"); - if (entryReadingtimeElement) { - entryReadingtimeElement.textContent = data.reading_time; - } - } - }); + buttonElement.appendChild(previousElement); + + response.json().then((data) => { + if ( + data.hasOwnProperty("content") && + data.hasOwnProperty("reading_time") + ) { + document.querySelector(".entry-content").innerHTML = + ttpolicy.createHTML(data.content); + const entryReadingtimeElement = document.querySelector( + ".entry-reading-time", + ); + if (entryReadingtimeElement) { + entryReadingtimeElement.textContent = data.reading_time; + } + } }); - request.execute(); + }); + request.execute(); } function openOriginalLink(openLinkInCurrentTab) { - const entryLink = document.querySelector(".entry h1 a"); - if (entryLink !== null) { - if (openLinkInCurrentTab) { - window.location.href = entryLink.getAttribute("href"); - } else { - DomHelper.openNewTab(entryLink.getAttribute("href")); - } - return; + const entryLink = document.querySelector(".entry h1 a"); + if (entryLink !== null) { + if (openLinkInCurrentTab) { + window.location.href = entryLink.getAttribute("href"); + } else { + DomHelper.openNewTab(entryLink.getAttribute("href")); } + return; + } - const currentItemOriginalLink = document.querySelector(".current-item :is(a, button)[data-original-link]"); - if (currentItemOriginalLink !== null) { - DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href")); + const currentItemOriginalLink = document.querySelector( + ".current-item :is(a, button)[data-original-link]", + ); + if (currentItemOriginalLink !== null) { + DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href")); - const currentItem = document.querySelector(".current-item"); - // If we are not on the list of starred items, move to the next item - if (document.location.href != document.querySelector(':is(a, button)[data-page=starred]').href) { - goToListItem(1); - } - markEntryAsRead(currentItem); + const currentItem = document.querySelector(".current-item"); + // If we are not on the list of starred items, move to the next item + if ( + document.location.href != + document.querySelector(":is(a, button)[data-page=starred]").href + ) { + goToListItem(1); } + markEntryAsRead(currentItem); + } } function openCommentLink(openLinkInCurrentTab) { - if (!isListView()) { - const entryLink = document.querySelector(":is(a, button)[data-comments-link]"); - if (entryLink !== null) { - if (openLinkInCurrentTab) { - window.location.href = entryLink.getAttribute("href"); - } else { - DomHelper.openNewTab(entryLink.getAttribute("href")); - } - return; - } - } else { - const currentItemCommentsLink = document.querySelector(".current-item :is(a, button)[data-comments-link]"); - if (currentItemCommentsLink !== null) { - DomHelper.openNewTab(currentItemCommentsLink.getAttribute("href")); - } + if (!isListView()) { + const entryLink = document.querySelector( + ":is(a, button)[data-comments-link]", + ); + if (entryLink !== null) { + if (openLinkInCurrentTab) { + window.location.href = entryLink.getAttribute("href"); + } else { + DomHelper.openNewTab(entryLink.getAttribute("href")); + } + return; + } + } else { + const currentItemCommentsLink = document.querySelector( + ".current-item :is(a, button)[data-comments-link]", + ); + if (currentItemCommentsLink !== null) { + DomHelper.openNewTab(currentItemCommentsLink.getAttribute("href")); } + } } function openSelectedItem() { - const currentItemLink = document.querySelector(".current-item .item-title a"); - if (currentItemLink !== null) { - window.location.href = currentItemLink.getAttribute("href"); - } + const currentItemLink = document.querySelector(".current-item .item-title a"); + if (currentItemLink !== null) { + window.location.href = currentItemLink.getAttribute("href"); + } } function unsubscribeFromFeed() { - const unsubscribeLinks = document.querySelectorAll("[data-action=remove-feed]"); - if (unsubscribeLinks.length === 1) { - const unsubscribeLink = unsubscribeLinks[0]; - - const request = new RequestBuilder(unsubscribeLink.dataset.url); - request.withCallback(() => { - if (unsubscribeLink.dataset.redirectUrl) { - window.location.href = unsubscribeLink.dataset.redirectUrl; - } else { - window.location.reload(); - } - }); - request.execute(); - } + const unsubscribeLinks = document.querySelectorAll( + "[data-action=remove-feed]", + ); + if (unsubscribeLinks.length === 1) { + const unsubscribeLink = unsubscribeLinks[0]; + + const request = new RequestBuilder(unsubscribeLink.dataset.url); + request.withCallback(() => { + if (unsubscribeLink.dataset.redirectUrl) { + window.location.href = unsubscribeLink.dataset.redirectUrl; + } else { + window.location.reload(); + } + }); + request.execute(); + } } /** @@ -436,13 +481,15 @@ function unsubscribeFromFeed() { * @param {boolean} fallbackSelf Refresh actual page if the page is not found. */ function goToPage(page, fallbackSelf) { - const element = document.querySelector(":is(a, button)[data-page=" + page + "]"); + const element = document.querySelector( + ":is(a, button)[data-page=" + page + "]", + ); - if (element) { - document.location.href = element.href; - } else if (fallbackSelf) { - window.location.reload(); - } + if (element) { + document.location.href = element.href; + } else if (fallbackSelf) { + window.location.reload(); + } } /** @@ -450,14 +497,14 @@ function goToPage(page, fallbackSelf) { * @param {(number|event)} offset - many items to jump for focus. */ function goToPrevious(offset) { - if (offset instanceof KeyboardEvent) { - offset = -1; - } - if (isListView()) { - goToListItem(offset); - } else { - goToPage("previous"); - } + if (offset instanceof KeyboardEvent) { + offset = -1; + } + if (isListView()) { + goToListItem(offset); + } else { + goToPage("previous"); + } } /** @@ -465,36 +512,38 @@ function goToPrevious(offset) { * @param {(number|event)} offset - How many items to jump for focus. */ function goToNext(offset) { - if (offset instanceof KeyboardEvent) { - offset = 1; - } - if (isListView()) { - goToListItem(offset); - } else { - goToPage("next"); - } + if (offset instanceof KeyboardEvent) { + offset = 1; + } + if (isListView()) { + goToListItem(offset); + } else { + goToPage("next"); + } } function goToFeedOrFeeds() { - if (isEntry()) { - goToFeed(); - } else { - goToPage('feeds'); - } + if (isEntry()) { + goToFeed(); + } else { + goToPage("feeds"); + } } function goToFeed() { - if (isEntry()) { - const feedAnchor = document.querySelector("span.entry-website a"); - if (feedAnchor !== null) { - window.location.href = feedAnchor.href; - } - } else { - const currentItemFeed = document.querySelector(".current-item :is(a, button)[data-feed-link]"); - if (currentItemFeed !== null) { - window.location.href = currentItemFeed.getAttribute("href"); - } + if (isEntry()) { + const feedAnchor = document.querySelector("span.entry-website a"); + if (feedAnchor !== null) { + window.location.href = feedAnchor.href; + } + } else { + const currentItemFeed = document.querySelector( + ".current-item :is(a, button)[data-feed-link]", + ); + if (currentItemFeed !== null) { + window.location.href = currentItemFeed.getAttribute("href"); } + } } // Sentinel values for specific list navigation @@ -505,173 +554,177 @@ const BOTTOM = -9999; * @param {number} offset How many items to jump for focus. */ function goToListItem(offset) { - const items = DomHelper.getVisibleElements(".items .item"); - if (items.length === 0) { - return; - } + const items = DomHelper.getVisibleElements(".items .item"); + if (items.length === 0) { + return; + } - if (document.querySelector(".current-item") === null) { - items[0].classList.add("current-item"); - items[0].focus(); - return; - } + if (document.querySelector(".current-item") === null) { + items[0].classList.add("current-item"); + items[0].focus(); + return; + } - for (let i = 0; i < items.length; i++) { - if (items[i].classList.contains("current-item")) { - items[i].classList.remove("current-item"); - - // By default adjust selection by offset - let itemOffset = (i + offset + items.length) % items.length; - // Allow jumping to top or bottom - if (offset == TOP) { - itemOffset = 0; - } else if (offset == BOTTOM) { - itemOffset = items.length - 1; - } - const item = items[itemOffset]; - - item.classList.add("current-item"); - DomHelper.scrollPageTo(item); - item.focus(); - - break; - } + for (let i = 0; i < items.length; i++) { + if (items[i].classList.contains("current-item")) { + items[i].classList.remove("current-item"); + + // By default adjust selection by offset + let itemOffset = (i + offset + items.length) % items.length; + // Allow jumping to top or bottom + if (offset == TOP) { + itemOffset = 0; + } else if (offset == BOTTOM) { + itemOffset = items.length - 1; + } + const item = items[itemOffset]; + + item.classList.add("current-item"); + DomHelper.scrollPageTo(item); + item.focus(); + + break; } + } } function scrollToCurrentItem() { - const currentItem = document.querySelector(".current-item"); - if (currentItem !== null) { - DomHelper.scrollPageTo(currentItem, true); - } + const currentItem = document.querySelector(".current-item"); + if (currentItem !== null) { + DomHelper.scrollPageTo(currentItem, true); + } } function decrementUnreadCounter(n) { - updateUnreadCounterValue((current) => { - return current - n; - }); + updateUnreadCounterValue((current) => { + return current - n; + }); } function incrementUnreadCounter(n) { - updateUnreadCounterValue((current) => { - return current + n; - }); + updateUnreadCounterValue((current) => { + return current + n; + }); } function updateUnreadCounterValue(callback) { - document.querySelectorAll("span.unread-counter").forEach((element) => { - const oldValue = parseInt(element.textContent, 10); - element.textContent = callback(oldValue); - }); + document.querySelectorAll("span.unread-counter").forEach((element) => { + const oldValue = parseInt(element.textContent, 10); + element.textContent = callback(oldValue); + }); - if (window.location.href.endsWith('/unread')) { - const oldValue = parseInt(document.title.split('(')[1], 10); - const newValue = callback(oldValue); + if (window.location.href.endsWith("/unread")) { + const oldValue = parseInt(document.title.split("(")[1], 10); + const newValue = callback(oldValue); - document.title = document.title.replace( - /(.*?)\(\d+\)(.*?)/, - function (match, prefix, suffix, offset, string) { - return prefix + '(' + newValue + ')' + suffix; - } - ); - } + document.title = document.title.replace( + /(.*?)\(\d+\)(.*?)/, + function (match, prefix, suffix, offset, string) { + return prefix + "(" + newValue + ")" + suffix; + }, + ); + } } function isEntry() { - return document.querySelector("section.entry") !== null; + return document.querySelector("section.entry") !== null; } function isListView() { - return document.querySelector(".items") !== null; + return document.querySelector(".items") !== null; } function findEntry(element) { - if (isListView()) { - if (element) { - return element.closest(".item"); - } - return document.querySelector(".current-item"); + if (isListView()) { + if (element) { + return element.closest(".item"); } - return document.querySelector(".entry"); + return document.querySelector(".current-item"); + } + return document.querySelector(".entry"); } function handleConfirmationMessage(linkElement, callback) { - if (linkElement.tagName != 'A' && linkElement.tagName != "BUTTON") { - linkElement = linkElement.parentNode; - } + if (linkElement.tagName != "A" && linkElement.tagName != "BUTTON") { + linkElement = linkElement.parentNode; + } - linkElement.style.display = "none"; + linkElement.style.display = "none"; - const containerElement = linkElement.parentNode; - const questionElement = document.createElement("span"); + const containerElement = linkElement.parentNode; + const questionElement = document.createElement("span"); - function createLoadingElement() { - const loadingElement = document.createElement("span"); - loadingElement.className = "loading"; - loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading)); + function createLoadingElement() { + const loadingElement = document.createElement("span"); + loadingElement.className = "loading"; + loadingElement.appendChild( + document.createTextNode(linkElement.dataset.labelLoading), + ); - questionElement.remove(); - containerElement.appendChild(loadingElement); - } + questionElement.remove(); + containerElement.appendChild(loadingElement); + } - const yesElement = document.createElement("button"); - yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes)); - yesElement.onclick = (event) => { - event.preventDefault(); + const yesElement = document.createElement("button"); + yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes)); + yesElement.onclick = (event) => { + event.preventDefault(); - createLoadingElement(); + createLoadingElement(); - callback(linkElement.dataset.url, linkElement.dataset.redirectUrl); - }; + callback(linkElement.dataset.url, linkElement.dataset.redirectUrl); + }; - const noElement = document.createElement("button"); - noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo)); - noElement.onclick = (event) => { - event.preventDefault(); + const noElement = document.createElement("button"); + noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo)); + noElement.onclick = (event) => { + event.preventDefault(); - const noActionUrl = linkElement.dataset.noActionUrl; - if (noActionUrl) { - createLoadingElement(); + const noActionUrl = linkElement.dataset.noActionUrl; + if (noActionUrl) { + createLoadingElement(); - callback(noActionUrl, linkElement.dataset.redirectUrl); - } else { - linkElement.style.display = "inline"; - questionElement.remove(); - } - }; + callback(noActionUrl, linkElement.dataset.redirectUrl); + } else { + linkElement.style.display = "inline"; + questionElement.remove(); + } + }; - questionElement.className = "confirm"; - questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " ")); - questionElement.appendChild(yesElement); - questionElement.appendChild(document.createTextNode(", ")); - questionElement.appendChild(noElement); + questionElement.className = "confirm"; + questionElement.appendChild( + document.createTextNode(linkElement.dataset.labelQuestion + " "), + ); + questionElement.appendChild(yesElement); + questionElement.appendChild(document.createTextNode(", ")); + questionElement.appendChild(noElement); - containerElement.appendChild(questionElement); + containerElement.appendChild(questionElement); } function showToast(label, iconElement) { - if (!label || !iconElement) { - return; - } + if (!label || !iconElement) { + return; + } - const toastMsgElement = document.getElementById("toast-msg"); - if (toastMsgElement) { - toastMsgElement.replaceChildren(iconElement.content.cloneNode(true)); - appendIconLabel(toastMsgElement, label); - - const toastElementWrapper = document.getElementById("toast-wrapper"); - if (toastElementWrapper) { - toastElementWrapper.classList.remove('toast-animate'); - setTimeout(function () { - toastElementWrapper.classList.add('toast-animate'); - }, 100); - } + const toastMsgElement = document.getElementById("toast-msg"); + if (toastMsgElement) { + toastMsgElement.replaceChildren(iconElement.content.cloneNode(true)); + appendIconLabel(toastMsgElement, label); + + const toastElementWrapper = document.getElementById("toast-wrapper"); + if (toastElementWrapper) { + toastElementWrapper.classList.remove("toast-animate"); + setTimeout(function () { + toastElementWrapper.classList.add("toast-animate"); + }, 100); } + } } /** Navigate to the new subscription page. */ function goToAddSubscription() { - window.location.href = document.body.dataset.addSubscriptionUrl; + window.location.href = document.body.dataset.addSubscriptionUrl; } /** @@ -679,68 +732,74 @@ function goToAddSubscription() { * @param {Element} playerElement */ function handlePlayerProgressionSave(playerElement) { - const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value - const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10); - const recordInterval = 10; - - // we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds - if (currentPositionInSeconds >= (lastKnownPositionInSeconds + recordInterval) || - currentPositionInSeconds <= (lastKnownPositionInSeconds - recordInterval) - ) { - playerElement.dataset.lastPosition = currentPositionInSeconds.toString(); - const request = new RequestBuilder(playerElement.dataset.saveUrl); - request.withBody({ progression: currentPositionInSeconds }); - request.execute(); - } + const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value + const lastKnownPositionInSeconds = parseInt( + playerElement.dataset.lastPosition, + 10, + ); + const recordInterval = 10; + + // we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds + if ( + currentPositionInSeconds >= lastKnownPositionInSeconds + recordInterval || + currentPositionInSeconds <= lastKnownPositionInSeconds - recordInterval + ) { + playerElement.dataset.lastPosition = currentPositionInSeconds.toString(); + const request = new RequestBuilder(playerElement.dataset.saveUrl); + request.withBody({ progression: currentPositionInSeconds }); + request.execute(); + } } /** * handle new share entires and already shared entries */ function handleShare() { - const link = document.querySelector(':is(a, button)[data-share-status]'); - const title = document.querySelector("body > main > section > header > h1 > a"); - if (link.dataset.shareStatus === "shared") { - checkShareAPI(title, link.href); - } - if (link.dataset.shareStatus === "share") { - const request = new RequestBuilder(link.href); - request.withCallback((r) => { - checkShareAPI(title, r.url); - }); - request.withHttpMethod("GET"); - request.execute(); - } + const link = document.querySelector(":is(a, button)[data-share-status]"); + const title = document.querySelector( + "body > main > section > header > h1 > a", + ); + if (link.dataset.shareStatus === "shared") { + checkShareAPI(title, link.href); + } + if (link.dataset.shareStatus === "share") { + const request = new RequestBuilder(link.href); + request.withCallback((r) => { + checkShareAPI(title, r.url); + }); + request.withHttpMethod("GET"); + request.execute(); + } } /** -* wrapper for Web Share API -*/ + * wrapper for Web Share API + */ function checkShareAPI(title, url) { - if (!navigator.canShare) { - console.error("Your browser doesn't support the Web Share API."); - window.location = url; - return; - } - try { - navigator.share({ - title: title, - url: url - }); - window.location.reload(); - } catch (err) { - console.error(err); - window.location.reload(); - } + if (!navigator.canShare) { + console.error("Your browser doesn't support the Web Share API."); + window.location = url; + return; + } + try { + navigator.share({ + title: title, + url: url, + }); + window.location.reload(); + } catch (err) { + console.error(err); + window.location.reload(); + } } function getCsrfToken() { - const element = document.querySelector("body[data-csrf-token]"); - if (element !== null) { - return element.dataset.csrfToken; - } + const element = document.querySelector("body[data-csrf-token]"); + if (element !== null) { + return element.dataset.csrfToken; + } - return ""; + return ""; } /** @@ -751,34 +810,39 @@ function getCsrfToken() { * @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": - enclosure.currentTime = enclosure.currentTime + value > 0 ? enclosure.currentTime + value : 0; - break; - 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" click, and lower value with same property would be 0. - enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value); - speedIndicator.forEach((speedI) => { - // 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 - speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; - }); - break; - case "speed-reset": - enclosure.playbackRate = value ; - speedIndicator.forEach((speedI) => { - // 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 - speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; - }); - break; - } - }); + 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": + enclosure.currentTime = + enclosure.currentTime + value > 0 ? enclosure.currentTime + value : 0; + break; + 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" click, and lower value with same property would be 0. + enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value); + speedIndicator.forEach((speedI) => { + // 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 + speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; + }); + break; + case "speed-reset": + enclosure.playbackRate = value; + speedIndicator.forEach((speedI) => { + // 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 + speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`; + }); + break; + } + }); } diff --git a/internal/ui/static/js/service_worker.js b/internal/ui/static/js/service_worker.js index 37cce257a22..379b4136606 100644 --- a/internal/ui/static/js/service_worker.js +++ b/internal/ui/static/js/service_worker.js @@ -1,44 +1,66 @@ - // Incrementing OFFLINE_VERSION will kick off the install event and force // previously cached resources to be updated from the network. -const OFFLINE_VERSION = 1; +const OFFLINE_VERSION = 2; const CACHE_NAME = "offline"; +console.log(USE_CACHE); + self.addEventListener("install", (event) => { - event.waitUntil( - (async () => { - const cache = await caches.open(CACHE_NAME); + event.waitUntil( + (async () => { + const cache = await caches.open(CACHE_NAME); - // Setting {cache: 'reload'} in the new request will ensure that the - // response isn't fulfilled from the HTTP cache; i.e., it will be from - // the network. - await cache.add(new Request(OFFLINE_URL, { cache: "reload" })); - })() - ); + if (USE_CACHE) { + await cache.addAll(["/", "/unread", OFFLINE_URL]); + } else { + // Setting {cache: 'reload'} in the new request will ensure that the + // response isn't fulfilled from the HTTP cache; i.e., it will be from + // the network. + await cache.add(new Request(OFFLINE_URL, { cache: "reload" })); + } + })(), + ); - // Force the waiting service worker to become the active service worker. - self.skipWaiting(); + // Force the waiting service worker to become the active service worker. + self.skipWaiting(); }); self.addEventListener("fetch", (event) => { - // We proxify requests through fetch() only if we are offline because it's slower. - if (navigator.onLine === false && event.request.mode === "navigate") { - event.respondWith( - (async () => { - try { - // Always try the network first. - const networkResponse = await fetch(event.request); - return networkResponse; - } catch (error) { - // catch is only triggered if an exception is thrown, which is likely - // due to a network error. - // If fetch() returns a valid HTTP response with a response code in - // the 4xx or 5xx range, the catch() will NOT be called. - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(OFFLINE_URL); - return cachedResponse; - } - })() - ); - } + // We proxify requests through fetch() only if we are offline because it's slower. + if ( + USE_CACHE || + (navigator.onLine === false && event.request.mode === "navigate") + ) { + event.respondWith( + (async () => { + try { + // Always try the network first. + const networkResponse = await fetch(event.request); + if (USE_CACHE) { + const cache = await caches.open(CACHE_NAME); + cache.put(event.request, networkResponse.clone()); + } + return networkResponse; + } catch (error) { + // catch is only triggered if an exception is thrown, which is likely + // due to a network error. + // If fetch() returns a valid HTTP response with a response code in + // the 4xx or 5xx range, the catch() will NOT be called. + const cache = await caches.open(CACHE_NAME); + + if (!USE_CACHE) { + return await cache.match(OFFLINE_URL); + } + + const cachedResponse = await cache.match(event.request); + + if (cachedResponse) { + return cachedResponse; + } + + return await cache.match(OFFLINE_URL); + } + })(), + ); + } }); diff --git a/internal/ui/static_javascript.go b/internal/ui/static_javascript.go index f2fb89fc125..1348921da16 100644 --- a/internal/ui/static_javascript.go +++ b/internal/ui/static_javascript.go @@ -27,7 +27,19 @@ func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) { contents := static.JavascriptBundles[filename] if filename == "service-worker" { - variables := fmt.Sprintf(`const OFFLINE_URL=%q;`, route.Path(h.router, "offline")) + user, err := h.store.UserByID(request.UserID(r)) + if err != nil { + html.ServerError(w, r, err) + return + } + + cacheForOffline := 0 + if user.CacheForOffline { + cacheForOffline = 1 + } + + variables := fmt.Sprintf(`const OFFLINE_URL=%q;const USE_CACHE=%d;`, route.Path(h.router, "offline"), cacheForOffline) + contents = append([]byte(variables), contents...) }