- {{ .entry.Title }} + {{ .entry.Title }}
{{ if .user }}
@@ -79,7 +79,7 @@
@@ -98,7 +98,7 @@
@@ -232,7 +232,7 @@
{{ end }}
diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html
index c584e02a994..6197d729232 100644
--- a/internal/template/templates/views/settings.html
+++ b/internal/template/templates/views/settings.html
@@ -123,6 +123,7 @@ class="page-link" target="_blank" rel="noopener noreferrer" - referrerpolicy="no-referrer" + referrerpolicy="strict-origin" data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}{{ t "entry.external_link.label" }}
title="{{ t "entry.comments.title" }}" target="_blank" rel="noopener noreferrer" - referrerpolicy="no-referrer" + referrerpolicy="strict-origin" data-comments-link="true" >{{ icon "comment" }}{{ t "entry.comments.label" }}
{{ end }}
- {{ .URL | safeURL }}
+ {{ .URL | safeURL }}
{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}
{{ t "page.settings.title" }}
{{ if eq .form.MarkReadBehavior .const.MarkAsReadOnViewButWaitForPlayerCompletion }}checked{{end}}> {{ t "form.prefs.label.mark_read_on_view_or_media_completion" }} +
diff --git a/internal/ui/entry_unread.go b/internal/ui/entry_unread.go
index 07d9007c207..ece9c8af6bc 100644
--- a/internal/ui/entry_unread.go
+++ b/internal/ui/entry_unread.go
@@ -66,7 +66,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
}
- if entry.ShouldMarkAsReadOnView(user) {
+ if entry.ShouldMarkAsReadOnView(user) && !request.IsServiceWorker(r) {
entry.Status = model.EntryStatusRead
}
@@ -90,6 +90,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
view.Set("user", user)
view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
+ view.Set("useCachedVersion", r.Header.Get("X-Cache-Hit") != "")
// Fetching the counter here avoid to be off by one.
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go
index 1b9e48ddeae..8423e6cf99b 100644
--- a/internal/ui/form/settings.go
+++ b/internal/ui/form/settings.go
@@ -52,6 +52,7 @@ type SettingsForm struct {
MediaPlaybackRate float64
BlockFilterEntryRules string
KeepFilterEntryRules string
+ CacheForOffline bool
}
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
@@ -119,6 +120,8 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.MarkReadOnView = MarkReadOnView
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
+ user.CacheForOffline = s.CacheForOffline
+
if s.Password != "" {
user.Password = s.Password
}
@@ -205,5 +208,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
MediaPlaybackRate: mediaPlaybackRate,
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
+ CacheForOffline: r.FormValue("cache_for_offline") == "1",
}
}
diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go
index eae72a7fb2f..e04ad9715b5 100644
--- a/internal/ui/settings_show.go
+++ b/internal/ui/settings_show.go
@@ -46,6 +46,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
MediaPlaybackRate: user.MediaPlaybackRate,
BlockFilterEntryRules: user.BlockFilterEntryRules,
KeepFilterEntryRules: user.KeepFilterEntryRules,
+ CacheForOffline: user.CacheForOffline,
}
timezones, err := h.store.Timezones()
diff --git a/internal/ui/static/css/common.css b/internal/ui/static/css/common.css
index c8e08412ea2..d0087cdee25 100644
--- a/internal/ui/static/css/common.css
+++ b/internal/ui/static/css/common.css
@@ -1,417 +1,443 @@
/* Layout */
* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
}
html {
- -webkit-text-size-adjust: 100%;
- text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
}
body {
- font-family: var(--font-family);
- text-rendering: optimizeLegibility;
- color: var(--body-color);
- background: var(--body-background);
+ font-family: var(--font-family);
+ text-rendering: optimizeLegibility;
+ color: var(--body-color);
+ background: var(--body-background);
}
hr {
- border: 0;
- height: 0;
- border-top: 1px dotted var(--hr-border-color);
- padding-bottom: 10px;
+ border: 0;
+ height: 0;
+ border-top: 1px dotted var(--hr-border-color);
+ padding-bottom: 10px;
}
-h1, h2, h3 {
- color: var(--title-color);
+h1,
+h2,
+h3 {
+ color: var(--title-color);
}
main {
- padding-left: 3px;
- padding-right: 3px;
- margin-bottom: 30px;
+ padding-left: 3px;
+ padding-right: 3px;
+ margin-bottom: 30px;
}
a {
- color: var(--link-color);
+ color: var(--link-color);
}
a:focus {
- outline: 0;
- color: var(--link-focus-color);
- text-decoration: none;
- outline: 1px dotted #aaa;
+ outline: 0;
+ color: var(--link-focus-color);
+ text-decoration: none;
+ outline: 1px dotted #aaa;
}
a:hover {
- color: var(--link-hover-color);
- text-decoration: none;
+ color: var(--link-hover-color);
+ text-decoration: none;
}
.sr-only {
- border: 0 !important;
- clip: rect(1px, 1px, 1px, 1px) !important;
- clip-path: inset(50%) !important;
- height: 1px !important;
- overflow: hidden !important;
- margin: -1px !important;
- padding: 0 !important;
- position: absolute !important;
- width: 1px !important;
- white-space: nowrap !important;
+ border: 0 !important;
+ clip: rect(1px, 1px, 1px, 1px) !important;
+ clip-path: inset(50%) !important;
+ height: 1px !important;
+ overflow: hidden !important;
+ margin: -1px !important;
+ padding: 0 !important;
+ position: absolute !important;
+ width: 1px !important;
+ white-space: nowrap !important;
}
.skip-to-content-link {
- --padding-size: 8px;
- --border-size: 1px;
+ --padding-size: 8px;
+ --border-size: 1px;
- background-color: var(--category-background-color);
- color: var(--category-color);
- border: var(--border-size) solid var(--category-border-color);
- border-radius: 5px;
- inset-inline-start: 50%;
- padding: var(--padding-size);
- position: absolute;
- transition: translate 0.3s;
- translate: -50% calc(-100% - calc(var(--padding-size) * 2) - calc(var(--border-size) * 2));
+ background-color: var(--category-background-color);
+ color: var(--category-color);
+ border: var(--border-size) solid var(--category-border-color);
+ border-radius: 5px;
+ inset-inline-start: 50%;
+ padding: var(--padding-size);
+ position: absolute;
+ transition: translate 0.3s;
+ translate: -50%
+ calc(-100% - calc(var(--padding-size) * 2) - calc(var(--border-size) * 2));
}
.skip-to-content-link:focus {
- translate: -50% 0;
+ translate: -50% 0;
}
/* Header and main menu */
.header {
- margin-top: 10px;
- margin-bottom: 20px;
+ margin-top: 10px;
+ margin-bottom: 20px;
}
.header nav {
- display: flex;
- align-items: stretch;
- flex-direction: column;
+ display: flex;
+ align-items: stretch;
+ flex-direction: column;
}
.header nav .logo svg {
- padding: 5px;
- inline-size: 24px;
- block-size: 24px;
+ padding: 5px;
+ inline-size: 24px;
+ block-size: 24px;
}
.header nav .logo[aria-expanded="true"] svg {
- rotate: 180deg;
+ rotate: 180deg;
}
.header ul.js-menu-show {
- display: initial;
+ display: initial;
}
.header ul:not(.js-menu-show) {
- display: none;
+ display: none;
}
.header li {
- cursor: pointer;
- padding-left: 10px;
- line-height: 2.1em;
- font-size: 1.2em;
- border-bottom: 1px dotted var(--header-list-border-color);
+ cursor: pointer;
+ padding-left: 10px;
+ line-height: 2.1em;
+ font-size: 1.2em;
+ border-bottom: 1px dotted var(--header-list-border-color);
}
.header li a:hover {
- color: #888;
+ color: #888;
}
.header :is(a, summary) {
- font-size: 0.9em;
- color: var(--header-link-color);
- text-decoration: none;
- border: none;
- font-weight: 400;
+ font-size: 0.9em;
+ color: var(--header-link-color);
+ text-decoration: none;
+ border: none;
+ font-weight: 400;
}
.header .active a {
- color: var(--header-active-link-color);
- /* Note: Firefox on Windows does not show the link as bold if the value is under 600 */
- font-weight: 600;
+ color: var(--header-active-link-color);
+ /* Note: Firefox on Windows does not show the link as bold if the value is under 600 */
+ font-weight: 600;
}
.header a:focus {
- color: var(--header-link-focus-color);
+ color: var(--header-link-focus-color);
}
/* Page header and footer*/
.page-header {
- padding-inline: 3px;
- margin-bottom: 25px;
+ padding-inline: 3px;
+ margin-bottom: 25px;
}
.page-footer {
- margin-bottom: 10px;
+ margin-bottom: 10px;
}
.page-header h1 {
- font-weight: 500;
- border-bottom: 1px dotted var(--page-header-title-border-color);
- font-size: 1.5rem;
+ font-weight: 500;
+ border-bottom: 1px dotted var(--page-header-title-border-color);
+ font-size: 1.5rem;
}
.page-header h1 a {
- text-decoration: none;
- color: var(--page-header-title-color);
+ text-decoration: none;
+ color: var(--page-header-title-color);
}
.page-header h1 a:hover,
.page-header h1 a:focus {
- color: #666;
+ color: #666;
}
.page-header li,
.page-footer li {
- list-style-type: none;
- line-height: 1.8em;
- white-space: nowrap;
+ list-style-type: none;
+ line-height: 1.8em;
+ white-space: nowrap;
}
.page-header ul a .icon {
- padding-bottom: 2px;
+ padding-bottom: 2px;
}
.page-header-action-form {
- display: inline-flex;
+ display: inline-flex;
}
:is(.page-button, .page-link) {
- color: var(--link-color);
- border: none;
- background-color: transparent;
- font-size: 1rem;
- cursor: pointer;
+ color: var(--link-color);
+ border: none;
+ background-color: transparent;
+ font-size: 1rem;
+ cursor: pointer;
- &:is(:hover, :focus) {
- color: var(--link-hover-color);
- }
+ &:is(:hover, :focus) {
+ color: var(--link-hover-color);
+ }
}
.page-button:active {
- translate: 1px 1px;
+ translate: 1px 1px;
}
/* Logo */
.logo {
- text-align: center;
- display: flex;
- justify-content: center;
+ text-align: center;
+ display: flex;
+ justify-content: center;
}
.logo a {
- color: var(--logo-color);
- letter-spacing: 1px;
- display: flex;
- align-items: center;
+ color: var(--logo-color);
+ letter-spacing: 1px;
+ display: flex;
+ align-items: center;
}
.logo a:hover {
- color: #339966;
+ color: #339966;
}
.logo a span {
- color: #339966;
+ color: #339966;
}
.logo a:hover span {
- color: var(--logo-hover-color-span);
+ color: var(--logo-hover-color-span);
}
/* PWA prompt */
#prompt-home-screen {
- display: none;
- position: fixed;
- bottom: 0;
- right: 0;
- width: 100%;
- text-align: center;
- background: #000;
- opacity: 85%;
+ display: none;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ width: 100%;
+ text-align: center;
+ background: #000;
+ opacity: 85%;
}
#btn-add-to-home-screen {
- text-decoration: none;
- line-height: 30px;
- color: #fff;
- background-color: transparent;
- border: 0;
+ text-decoration: none;
+ line-height: 30px;
+ color: #fff;
+ background-color: transparent;
+ border: 0;
}
#btn-add-to-home-screen:hover {
- color: red;
+ color: red;
}
/* Notification - "Toast" */
#toast-wrapper {
- visibility: hidden;
- opacity: 0;
- position: fixed;
- left: 0;
- bottom: 10%;
- color: #fff;
- width: 100%;
- text-align: center;
+ visibility: hidden;
+ opacity: 0;
+ position: fixed;
+ left: 0;
+ bottom: 10%;
+ color: #fff;
+ width: 100%;
+ text-align: center;
}
#toast-msg {
- background-color: rgba(0,0,0,0.7);
- padding-bottom: 4px;
- padding-left: 4px;
- padding-right: 5px;
- border-radius: 5px;
+ background-color: rgba(0, 0, 0, 0.7);
+ padding-bottom: 4px;
+ padding-left: 4px;
+ padding-right: 5px;
+ border-radius: 5px;
}
.toast-animate {
- animation: toastKeyFrames 2s;
+ animation: toastKeyFrames 2s;
}
@keyframes toastKeyFrames {
- 0% {visibility: hidden; opacity: 0;}
- 25% {visibility: visible; opacity: 1; z-index: 9999}
- 50% {visibility: visible; opacity: 1; z-index: 9999}
- 75% {visibility: visible; opacity: 1; z-index: 9999}
- 100% {visibility: hidden; opacity: 0; z-index: 0}
+ 0% {
+ visibility: hidden;
+ opacity: 0;
+ }
+ 25% {
+ visibility: visible;
+ opacity: 1;
+ z-index: 9999;
+ }
+ 50% {
+ visibility: visible;
+ opacity: 1;
+ z-index: 9999;
+ }
+ 75% {
+ visibility: visible;
+ opacity: 1;
+ z-index: 9999;
+ }
+ 100% {
+ visibility: hidden;
+ opacity: 0;
+ z-index: 0;
+ }
}
/* Hide the logo when there is not enough space to display menus when using languages more verbose than English */
@media (min-width: 620px) and (max-width: 830px) {
- .logo {
- display: none;
- }
+ .logo {
+ display: none;
+ }
}
@media (min-width: 830px) {
- .logo {
- padding-right: 8px;
- }
+ .logo {
+ padding-right: 8px;
+ }
}
@media (min-width: 620px) {
- body {
- margin: auto;
- max-width: 820px;
- }
-
- .header {
- padding-left: 3px;
- }
-
- .header li {
- display: inline-block;
- padding: 0;
- padding-right: 12px;
- line-height: normal;
- border: none;
- font-size: 1.0em;
- }
-
- .header nav {
- align-items: end;
- flex-direction: row;
- }
-
- .header .logo svg {
- display: none;
- }
-
- .header ul:not(.js-menu-show), .header ul.js-menu-show {
- display: revert;
- }
-
- .header :is(a, summary):hover {
- color: var(--header-link-hover-color);
- }
-
- .page-header li,
- .page-footer li {
- display: inline;
- padding-right: 15px;
- }
-
- .pagination-backward,
- .pagination-forward {
- display: flex;
- }
+ body {
+ margin: auto;
+ max-width: 820px;
+ }
+
+ .header {
+ padding-left: 3px;
+ }
+
+ .header li {
+ display: inline-block;
+ padding: 0;
+ padding-right: 12px;
+ line-height: normal;
+ border: none;
+ font-size: 1em;
+ }
+
+ .header nav {
+ align-items: end;
+ flex-direction: row;
+ }
+
+ .header .logo svg {
+ display: none;
+ }
+
+ .header ul:not(.js-menu-show),
+ .header ul.js-menu-show {
+ display: revert;
+ }
+
+ .header :is(a, summary):hover {
+ color: var(--header-link-hover-color);
+ }
+
+ .page-header li,
+ .page-footer li {
+ display: inline;
+ padding-right: 15px;
+ }
+
+ .pagination-backward,
+ .pagination-forward {
+ display: flex;
+ }
}
/* Tables */
table {
- width: 100%;
- border-collapse: collapse;
+ width: 100%;
+ border-collapse: collapse;
}
-table, th, td {
- border: 1px solid var(--table-border-color);
+table,
+th,
+td {
+ border: 1px solid var(--table-border-color);
}
-th, td {
- padding: 5px;
- text-align: left;
+th,
+td {
+ padding: 5px;
+ text-align: left;
}
td {
- vertical-align: top;
+ vertical-align: top;
}
th {
- background: var(--table-th-background);
- color: var(--table-th-color);
- font-weight: 400;
+ background: var(--table-th-background);
+ color: var(--table-th-color);
+ font-weight: 400;
}
tr:hover {
- color: var(--table-tr-hover-color);
- background-color: var(--table-tr-hover-background-color);
+ color: var(--table-tr-hover-color);
+ background-color: var(--table-tr-hover-background-color);
}
.column-40 {
- width: 40%;
+ width: 40%;
}
.column-25 {
- width: 25%;
+ width: 25%;
}
.column-20 {
- width: 20%;
+ width: 20%;
}
/* Forms */
fieldset {
- border: 1px dotted #ddd;
- padding: 8px;
- margin-bottom: 20px;
+ border: 1px dotted #ddd;
+ padding: 8px;
+ margin-bottom: 20px;
}
legend {
- font-weight: 500;
- padding-left: 3px;
- padding-right: 3px;
+ font-weight: 500;
+ padding-left: 3px;
+ padding-right: 3px;
}
label {
- cursor: pointer;
- display: block;
+ cursor: pointer;
+ display: block;
}
.radio-group {
- line-height: 1.9em;
+ line-height: 1.9em;
}
div.radio-group label {
- display: inline-block;
+ display: inline-block;
}
select {
- margin-bottom: 15px;
+ margin-bottom: 15px;
}
input[type="search"],
@@ -419,15 +445,15 @@ input[type="url"],
input[type="password"],
input[type="text"],
input[type="number"] {
- color: var(--input-color);
- background: var(--input-background);
- border: var(--input-border);
- padding: 3px;
- line-height: 20px;
- width: 250px;
- font-size: 99%;
- margin-top: 5px;
- appearance: none;
+ color: var(--input-color);
+ background: var(--input-background);
+ border: var(--input-border);
+ padding: 3px;
+ line-height: 20px;
+ width: 250px;
+ font-size: 99%;
+ margin-top: 5px;
+ appearance: none;
}
input[type="search"]:focus,
@@ -435,865 +461,872 @@ input[type="url"]:focus,
input[type="password"]:focus,
input[type="text"]:focus,
input[type="number"]:focus {
- color: var(--input-focus-color);
- border-color: var(--input-focus-border-color);
- outline: 0;
- box-shadow: var(--input-focus-box-shadow);
+ color: var(--input-focus-color);
+ border-color: var(--input-focus-border-color);
+ outline: 0;
+ box-shadow: var(--input-focus-box-shadow);
}
#form-entries-per-page {
- max-width: 80px;
+ max-width: 80px;
}
input[type="checkbox"] {
- margin-top: 10px;
- margin-bottom: 10px;
+ margin-top: 10px;
+ margin-bottom: 10px;
}
textarea {
- width: 350px;
- color: var(--input-color);
- background: var(--input-background);
- border: var(--input-border);
- padding: 3px;
- margin-bottom: 10px;
- margin-top: 5px;
- appearance: none;
+ width: 350px;
+ color: var(--input-color);
+ background: var(--input-background);
+ border: var(--input-border);
+ padding: 3px;
+ margin-bottom: 10px;
+ margin-top: 5px;
+ appearance: none;
}
textarea:focus {
- color: var(--input-focus-color);
- border-color: var(--input-focus-border-color);
- outline: 0;
- box-shadow: var(--input-focus-box-shadow);
+ color: var(--input-focus-color);
+ border-color: var(--input-focus-border-color);
+ outline: 0;
+ box-shadow: var(--input-focus-box-shadow);
}
input::placeholder {
- color: var(--input-placeholder-color);
- padding-top: 2px;
+ color: var(--input-placeholder-color);
+ padding-top: 2px;
}
.form-label-row {
- display: flex;
+ display: flex;
}
.form-help {
- font-size: 0.9em;
- color: brown;
- margin-bottom: 15px;
+ font-size: 0.9em;
+ color: brown;
+ margin-bottom: 15px;
}
.form-section {
- border-left: 2px dotted #ddd;
- padding-left: 20px;
- margin-left: 10px;
+ border-left: 2px dotted #ddd;
+ padding-left: 20px;
+ margin-left: 10px;
}
details > summary {
- outline: none;
- cursor: pointer;
+ outline: none;
+ cursor: pointer;
}
.details-content {
- margin-top: 15px;
+ margin-top: 15px;
}
/* Buttons */
a.button {
- text-decoration: none;
+ text-decoration: none;
}
.button {
- display: inline-block;
- appearance: none;
- font-size: 1.1em;
- cursor: pointer;
- padding: 3px 10px;
- border: 1px solid;
- border-radius: unset;
+ display: inline-block;
+ appearance: none;
+ font-size: 1.1em;
+ cursor: pointer;
+ padding: 3px 10px;
+ border: 1px solid;
+ border-radius: unset;
}
.button-primary {
- border-color: var(--button-primary-border-color);
- background: var(--button-primary-background);
- color: var(--button-primary-color);
+ border-color: var(--button-primary-border-color);
+ background: var(--button-primary-background);
+ color: var(--button-primary-color);
}
a.button-primary:hover,
a.button-primary:focus,
.button-primary:hover,
.button-primary:focus {
- border-color: var(--button-primary-focus-border-color);
- background: var(--button-primary-focus-background);
- color: var(--button-primary-color);
+ border-color: var(--button-primary-focus-border-color);
+ background: var(--button-primary-focus-background);
+ color: var(--button-primary-color);
}
.button-danger {
- border-color: #b0281a;
- background: #d14836;
- color: #fff;
+ border-color: #b0281a;
+ background: #d14836;
+ color: #fff;
}
.button-danger:hover,
.button-danger:focus {
- color: #fff;
- background: #c53727;
+ color: #fff;
+ background: #c53727;
}
.button:disabled {
- color: #ccc;
- background: #f7f7f7;
- border-color: #ccc;
+ color: #ccc;
+ background: #f7f7f7;
+ border-color: #ccc;
}
.buttons {
- margin-top: 10px;
- margin-bottom: 20px;
+ margin-top: 10px;
+ margin-bottom: 20px;
}
fieldset .buttons {
- margin-bottom: 0;
+ margin-bottom: 0;
}
/* Alerts */
.alert {
- padding: 8px 35px 8px 14px;
- margin-bottom: 20px;
- color: var(--alert-color);
- background-color: var(--alert-background-color);
- border: 1px solid var(--alert-border-color);
- border-radius: 4px;
- overflow: auto;
+ padding: 8px 35px 8px 14px;
+ margin-bottom: 20px;
+ color: var(--alert-color);
+ background-color: var(--alert-background-color);
+ border: 1px solid var(--alert-border-color);
+ border-radius: 4px;
+ overflow: auto;
}
.alert h3 {
- margin-top: 0;
- margin-bottom: 15px;
+ margin-top: 0;
+ margin-bottom: 15px;
}
.alert-success {
- color: var(--alert-success-color);
- background-color: var(--alert-success-background-color);
- border-color: var(--alert-success-border-color);
+ color: var(--alert-success-color);
+ background-color: var(--alert-success-background-color);
+ border-color: var(--alert-success-border-color);
}
.alert-error {
- color: var(--alert-error-color);
- background-color: var(--alert-error-background-color);
- border-color: var(--alert-error-border-color);
+ color: var(--alert-error-color);
+ background-color: var(--alert-error-background-color);
+ border-color: var(--alert-error-border-color);
}
.alert-error h3,
.alert-error a {
- color: var(--alert-error-color);
+ color: var(--alert-error-color);
}
.alert-info {
- color: var(--alert-info-color);
- background-color: var(--alert-info-background-color);
- border-color: var(--alert-info-border-color);
+ color: var(--alert-info-color);
+ background-color: var(--alert-info-background-color);
+ border-color: var(--alert-info-border-color);
}
/* Panel */
.panel {
- color: var(--panel-color);
- background-color: var(--panel-background);
- border: 1px solid var(--panel-border-color);
- border-radius: 5px;
- padding: 10px;
- margin-bottom: 15px;
+ color: var(--panel-color);
+ background-color: var(--panel-background);
+ border: 1px solid var(--panel-border-color);
+ border-radius: 5px;
+ padding: 10px;
+ margin-bottom: 15px;
}
.panel h3 {
- font-weight: 500;
- margin-top: 0;
- margin-bottom: 20px;
+ font-weight: 500;
+ margin-top: 0;
+ margin-bottom: 20px;
}
.panel ul {
- margin-left: 30px;
+ margin-left: 30px;
}
/* Modals */
template {
- display: none;
+ display: none;
}
#modal-left {
- position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
- width: 380px;
- overflow: auto;
- color: var(--modal-color);
- background: var(--modal-background);
- box-shadow: var(--modal-box-shadow);
- padding: 5px;
- padding-top: 30px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 380px;
+ overflow: auto;
+ color: var(--modal-color);
+ background: var(--modal-background);
+ box-shadow: var(--modal-box-shadow);
+ padding: 5px;
+ padding-top: 30px;
}
#modal-left h3 {
- font-weight: 400;
- margin: 0;
+ font-weight: 400;
+ margin: 0;
}
.btn-close-modal {
- position: absolute;
- top: 0;
- right: 0;
- font-size: 1.7em;
- color: #ccc;
- padding:0 .2em;
- margin: 10px;
- text-decoration: none;
- background-color: transparent;
- border: none;
+ position: absolute;
+ top: 0;
+ right: 0;
+ font-size: 1.7em;
+ color: #ccc;
+ padding: 0 0.2em;
+ margin: 10px;
+ text-decoration: none;
+ background-color: transparent;
+ border: none;
}
.btn-close-modal:hover {
- color: #999;
+ color: #999;
}
/* Keyboard Shortcuts */
.keyboard-shortcuts li {
- margin-left: 25px;
- list-style-type: square;
- color: var(--keyboard-shortcuts-li-color);
- font-size: 0.95em;
- line-height: 1.45em;
+ margin-left: 25px;
+ list-style-type: square;
+ color: var(--keyboard-shortcuts-li-color);
+ font-size: 0.95em;
+ line-height: 1.45em;
}
.keyboard-shortcuts p {
- line-height: 1.9em;
+ line-height: 1.9em;
}
/* Login form */
.login-form {
- margin: 50px auto 0;
- max-width: 300px;
+ margin: 50px auto 0;
+ max-width: 300px;
}
.webauthn {
- margin-bottom: 20px;
+ margin-bottom: 20px;
}
/* Counters */
.unread-counter-wrapper,
.error-feeds-counter-wrapper {
- font-size: 0.9em;
- font-weight: 300;
- color: var(--counter-color);
+ font-size: 0.9em;
+ font-weight: 300;
+ color: var(--counter-color);
}
/* Category label */
.category {
- font-size: 0.75em;
- background-color: var(--category-background-color);
- border: 1px solid var(--category-border-color);
- border-radius: 5px;
- margin-left: 0.25em;
- padding: 1px 0.4em 1px 0.4em;
- white-space: nowrap;
- color: var(--category-color);
+ font-size: 0.75em;
+ background-color: var(--category-background-color);
+ border: 1px solid var(--category-border-color);
+ border-radius: 5px;
+ margin-left: 0.25em;
+ padding: 1px 0.4em 1px 0.4em;
+ white-space: nowrap;
+ color: var(--category-color);
}
.category a {
- color: var(--category-link-color);
- text-decoration: none;
+ color: var(--category-link-color);
+ text-decoration: none;
}
.category a:hover,
.category a:focus {
- color: var(--category-link-hover-color);
+ color: var(--category-link-hover-color);
}
-
.category-item-total {
- color: var(--body-color);
+ color: var(--body-color);
}
/* Pagination */
.pagination {
- font-size: 1.1em;
- display: flex;
- align-items: center;
- justify-content: space-between;
+ font-size: 1.1em;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
}
.pagination-top {
- padding-bottom: 8px;
+ padding-bottom: 8px;
}
.pagination-bottom {
- padding-top: 8px;
+ padding-top: 8px;
}
.pagination-entry-top {
- padding-top: 8px;
+ padding-top: 8px;
}
.pagination-entry-bottom {
- border-top: 1px dotted var(--pagination-border-color);
- margin-bottom: 15px;
- margin-top: 50px;
- padding-top: 8px;
+ border-top: 1px dotted var(--pagination-border-color);
+ margin-bottom: 15px;
+ margin-top: 50px;
+ padding-top: 8px;
}
.pagination > div.pagination-backward > div {
- padding-right: 15px;
+ padding-right: 15px;
}
.pagination > div.pagination-forward > div {
- padding-left: 15px;
+ padding-left: 15px;
}
-.pagination-next, .pagination-last {
- text-align: right;
+.pagination-next,
+.pagination-last {
+ text-align: right;
}
.pagination-next:after {
- content: " ›";
+ content: " ›";
}
.pagination-last:after {
- content: " »";
+ content: " »";
}
.pagination-prev:before {
- content: "‹ ";
+ content: "‹ ";
}
.pagination-first:before {
- content: "« ";
+ content: "« ";
}
.pagination a {
- color: var(--pagination-link-color);
+ color: var(--pagination-link-color);
}
.pagination a:hover,
.pagination a:focus {
- text-decoration: none;
+ text-decoration: none;
}
/* List view */
.item {
- border: 1px dotted var(--item-border-color);
- margin-bottom: 20px;
- padding: var(--item-padding);
- overflow: hidden;
+ border: 1px dotted var(--item-border-color);
+ margin-bottom: 20px;
+ padding: var(--item-padding);
+ overflow: hidden;
}
.item.current-item {
- border: var(--current-item-border-width) solid var(--current-item-border-color);
- padding: 3px;
- box-shadow: var(--current-item-box-shadow);
+ border: var(--current-item-border-width) solid
+ var(--current-item-border-color);
+ padding: 3px;
+ box-shadow: var(--current-item-box-shadow);
}
.item.current-item:focus {
- outline: none;
+ outline: none;
}
-
.item-header {
- font-size: 1rem;
+ font-size: 1rem;
}
.item-header span {
- font-weight: normal;
- display: inline-block;
+ font-weight: normal;
+ display: inline-block;
}
.item-title {
- font-size: 1rem;
- margin: 0;
- display: inline;
+ font-size: 1rem;
+ margin: 0;
+ display: inline;
}
.item-title a {
- text-decoration: none;
- font-weight: var(--item-title-link-font-weight);
- font-size: inherit;
+ text-decoration: none;
+ font-weight: var(--item-title-link-font-weight);
+ font-size: inherit;
}
.feed-entries-counter {
- display: inline-flex;
- gap: 2px;
- align-items: center;
+ display: inline-flex;
+ gap: 2px;
+ align-items: center;
}
.item-status-read .item-title a {
- color: var(--item-status-read-title-link-color);
+ color: var(--item-status-read-title-link-color);
}
.item-meta {
- color: var(--item-meta-focus-color);
- font-size: 0.8em;
+ color: var(--item-meta-focus-color);
+ font-size: 0.8em;
}
.item-meta a {
- color: #777;
- text-decoration: none;
+ color: #777;
+ text-decoration: none;
}
.item-meta :is(a:is(:focus, :hover), button:is(:focus, :hover)) {
- color: #333;
+ color: #333;
}
.item-meta ul {
- margin-top: 5px;
+ margin-top: 5px;
}
.item-meta li {
- display: inline-block;
+ display: inline-block;
}
.item-meta-info {
- font-size: 0.85em;
+ font-size: 0.85em;
}
.item-meta-info li:not(:last-child):after {
- content: "|";
- color: var(--item-meta-li-color);
+ content: "|";
+ color: var(--item-meta-li-color);
}
.item-meta-icons li {
- margin-right: 8px;
- margin-top: 4px;
+ margin-right: 8px;
+ margin-top: 4px;
}
.item-meta-icons li:last-child {
- margin-right: 0;
+ margin-right: 0;
}
.item-meta-icons li > :is(a, button) {
- color: #777;
- text-decoration: none;
- font-size: 0.8rem;
- border: none;
- background-color: transparent;
- cursor: pointer;
+ color: #777;
+ text-decoration: none;
+ font-size: 0.8rem;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
}
.item-meta-icons a span {
- text-decoration: underline;
+ text-decoration: underline;
}
.item-meta-icons button:active {
- translate: 1px 1px;
+ translate: 1px 1px;
}
.items {
- overflow-x: hidden;
- touch-action: pan-y;
+ overflow-x: hidden;
+ touch-action: pan-y;
}
.hide-read-items .item-status-read:not(.current-item) {
- display: none;
+ display: none;
}
.entry-swipe {
- transition-property: transform;
- transition-duration: 0s;
- transition-timing-function: ease-out;
+ transition-property: transform;
+ transition-duration: 0s;
+ transition-timing-function: ease-out;
}
/* Feeds list */
article.feed-parsing-error {
- background-color: var(--feed-parsing-error-background-color);
- border-style: var(--feed-parsing-error-border-style);
- border-color: var(--feed-parsing-error-border-color);
+ background-color: var(--feed-parsing-error-background-color);
+ border-style: var(--feed-parsing-error-border-style);
+ border-color: var(--feed-parsing-error-border-color);
}
article.feed-has-unread {
- background-color: var(--feed-has-unread-background-color);
- border-style: var(--feed-has-unread-border-style);
- border-color: var(--feed-has-unread-border-color);
+ background-color: var(--feed-has-unread-background-color);
+ border-style: var(--feed-has-unread-border-style);
+ border-color: var(--feed-has-unread-border-color);
}
.parsing-error {
- font-size: 0.85em;
- margin-top: 2px;
- color: var(--parsing-error-color);
+ font-size: 0.85em;
+ margin-top: 2px;
+ color: var(--parsing-error-color);
}
.parsing-error-count {
- cursor: pointer;
+ cursor: pointer;
}
/* Categories list */
article.category-has-unread {
- background-color: var(--category-has-unread-background-color);
- border-style: var(--category-has-unread-border-style);
- border-color: var(--category-has-unread-border-color);
+ background-color: var(--category-has-unread-background-color);
+ border-style: var(--category-has-unread-border-style);
+ border-color: var(--category-has-unread-border-color);
}
/* Icons */
.icon,
.icon-label {
- vertical-align: middle;
- display: inline-block;
- padding-right: 2px;
+ vertical-align: middle;
+ display: inline-block;
+ padding-right: 2px;
}
.icon {
- width: 16px;
- height: 16px;
+ width: 16px;
+ height: 16px;
}
/* Entry view */
.entry header {
- padding-bottom: 5px;
- padding-inline: 5px;
- border-bottom: 1px dotted var(--entry-header-border-color);
+ padding-bottom: 5px;
+ padding-inline: 5px;
+ border-bottom: 1px dotted var(--entry-header-border-color);
}
.entry header h1 {
- font-size: 2.0em;
- line-height: 1.25em;
- margin: 5px 0 30px 0;
- overflow-wrap: break-word;
+ font-size: 2em;
+ line-height: 1.25em;
+ margin: 5px 0 30px 0;
+ overflow-wrap: break-word;
}
.entry header h1 a {
- text-decoration: none;
- color: var(--entry-header-title-link-color);
+ text-decoration: none;
+ color: var(--entry-header-title-link-color);
}
.entry header h1 a:hover,
.entry header h1 a:focus {
- color: #666;
+ color: #666;
}
.entry-actions {
- margin-bottom: 20px;
+ margin-bottom: 20px;
}
.entry-actions a span {
- text-decoration: underline;
+ text-decoration: underline;
}
.entry-actions li {
- display: inline-block;
- margin-right: 15px;
- line-height: 1.7em;
+ display: inline-block;
+ margin-right: 15px;
+ line-height: 1.7em;
}
.entry-meta {
- font-size: 0.95em;
- margin: 0 0 20px;
- color: #666;
- overflow-wrap: break-word;
+ font-size: 0.95em;
+ margin: 0 0 20px;
+ color: #666;
+ overflow-wrap: break-word;
}
.entry-tags {
- margin-top: 20px;
- margin-bottom: 20px;
+ margin-top: 20px;
+ margin-bottom: 20px;
}
.entry-tags strong {
- font-weight: 600;
+ font-weight: 600;
}
.entry-website img {
- vertical-align: top;
+ vertical-align: top;
}
.entry-website a {
- color: #666;
- vertical-align: top;
- text-decoration: none;
+ color: #666;
+ vertical-align: top;
+ text-decoration: none;
}
.entry-website a:hover,
.entry-website a:focus {
- text-decoration: underline;
+ text-decoration: underline;
}
.entry-date {
- font-size: 0.65em;
- font-style: italic;
- color: #555;
+ font-size: 0.65em;
+ font-style: italic;
+ color: #555;
}
.entry-content {
- padding-top: 15px;
- font-size: 1.2em;
- font-weight: var(--entry-content-font-weight);
- font-family: var(--entry-content-font-family);
- color: var(--entry-content-color);
- line-height: 1.4em;
- overflow-wrap: break-word;
- touch-action: pan-y pinch-zoom;
-}
-
-.entry-content h1, h2, h3, h4, h5, h6 {
- margin-top: 15px;
- margin-bottom: 10px;
+ padding-top: 15px;
+ font-size: 1.2em;
+ font-weight: var(--entry-content-font-weight);
+ font-family: var(--entry-content-font-family);
+ color: var(--entry-content-color);
+ line-height: 1.4em;
+ overflow-wrap: break-word;
+ touch-action: pan-y pinch-zoom;
+}
+
+.entry-content h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin-top: 15px;
+ margin-bottom: 10px;
}
.entry-content iframe,
.entry-content video,
.entry-content img {
- max-width: 100%;
+ max-width: 100%;
}
.entry-content img {
- height: auto;
+ height: auto;
}
.entry-content figure {
- margin-top: 15px;
- margin-bottom: 15px;
+ margin-top: 15px;
+ margin-bottom: 15px;
}
.entry-content figure img {
- border: 1px solid #000;
+ border: 1px solid #000;
}
.entry-content figcaption {
- font-size: 0.75em;
- text-transform: uppercase;
- color: #777;
+ font-size: 0.75em;
+ text-transform: uppercase;
+ color: #777;
}
.entry-content p {
- margin-top: 10px;
- margin-bottom: 15px;
+ margin-top: 10px;
+ margin-bottom: 15px;
}
.entry-content a {
- overflow-wrap: break-word;
+ overflow-wrap: break-word;
}
.entry-content a:visited {
- color: var(--link-visited-color);
+ color: var(--link-visited-color);
}
.entry-content dt {
- font-weight: 500;
- margin-top: 15px;
- color: #555;
+ font-weight: 500;
+ margin-top: 15px;
+ color: #555;
}
.entry-content dd {
- margin-left: 15px;
- margin-top: 5px;
- padding-left: 20px;
- border-left: 3px solid #ddd;
- color: #777;
- font-weight: 300;
- line-height: 1.4em;
+ margin-left: 15px;
+ margin-top: 5px;
+ padding-left: 20px;
+ border-left: 3px solid #ddd;
+ color: #777;
+ font-weight: 300;
+ line-height: 1.4em;
}
.entry-content blockquote {
- border-left: 4px solid #ddd;
- padding-left: 25px;
- margin-left: 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- line-height: 1.4em;
- font-family: var(--entry-content-quote-font-family);
+ border-left: 4px solid #ddd;
+ padding-left: 25px;
+ margin-left: 20px;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ line-height: 1.4em;
+ font-family: var(--entry-content-quote-font-family);
}
.entry-content q {
- color: var(--entry-content-quote-color);
- font-family: var(--entry-content-quote-font-family);
- font-style: italic;
+ color: var(--entry-content-quote-color);
+ font-family: var(--entry-content-quote-font-family);
+ font-style: italic;
}
.entry-content q:before {
- content: "“";
+ content: "“";
}
.entry-content q:after {
- content: "”";
+ content: "”";
}
.entry-content pre {
- padding: 5px;
- overflow: auto;
- overflow-wrap: initial;
- border-width: 1px;
- border-style: solid;
+ padding: 5px;
+ overflow: auto;
+ overflow-wrap: initial;
+ border-width: 1px;
+ border-style: solid;
}
.entry-content pre,
.entry-content code {
- color: var(--entry-content-code-color);
- background: var(--entry-content-code-background);
- border-color: var(--entry-content-code-border-color);
+ color: var(--entry-content-code-color);
+ background: var(--entry-content-code-background);
+ border-color: var(--entry-content-code-border-color);
}
.entry-content table {
- max-width: 100%;
+ max-width: 100%;
}
.entry-content ul,
.entry-content ol {
- margin-left: 30px;
- margin-top: 15px;
- margin-bottom: 15px;
+ margin-left: 30px;
+ margin-top: 15px;
+ margin-bottom: 15px;
}
.entry-content li ul,
.entry-content li ol {
- margin-top: 0;
- margin-bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
}
.entry-content ul {
- list-style-type: square;
+ list-style-type: square;
}
.entry-content strong {
- font-weight: 600;
+ font-weight: 600;
}
.entry-content sub,
.entry-content sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
}
.entry-content sub {
- bottom: -0.25em;
+ bottom: -0.25em;
}
.entry-content sup {
- top: -0.5em;
+ top: -0.5em;
}
.entry-content abbr {
- cursor: pointer;
- text-decoration: none;
- border-bottom: 1px dashed var(--entry-content-abbr-border-color);
+ cursor: pointer;
+ text-decoration: none;
+ border-bottom: 1px dashed var(--entry-content-abbr-border-color);
}
.entry-content aside {
- width: 30%;
- padding: 1ch;
- margin-left: 15px;
- float: right;
- font-style: italic;
- border: dotted var(--entry-content-aside-border-color) 2px;
+ width: 30%;
+ padding: 1ch;
+ margin-left: 15px;
+ float: right;
+ font-style: italic;
+ border: dotted var(--entry-content-aside-border-color) 2px;
}
details.entry-enclosures {
- margin-top: 25px;
+ margin-top: 25px;
}
.entry-enclosures summary {
- font-weight: 500;
- font-size: 1.2em;
+ font-weight: 500;
+ font-size: 1.2em;
}
.entry-enclosure {
- border: 1px dotted var(--entry-enclosure-border-color);
- padding: 5px;
- margin-top: 10px;
- max-width: 100%;
+ border: 1px dotted var(--entry-enclosure-border-color);
+ padding: 5px;
+ margin-top: 10px;
+ max-width: 100%;
}
.entry-enclosure-download {
- font-size: 0.85em;
- overflow-wrap: break-word;
+ font-size: 0.85em;
+ overflow-wrap: break-word;
}
.enclosure-video video,
.enclosure-image img {
- max-width: 100%;
+ max-width: 100%;
}
/* Confirmation */
.confirm {
- font-weight: 500;
- color: #ed2d04;
+ font-weight: 500;
+ color: #ed2d04;
}
.confirm button {
- color: #ed2d04;
- border: none;
- background-color: transparent;
- cursor: pointer;
- font-size: inherit;
+ color: #ed2d04;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ font-size: inherit;
}
.loading {
- font-style: italic;
+ font-style: italic;
}
/* Bookmarlet */
.bookmarklet {
- border: 1px dashed #ccc;
- border-radius: 5px;
- padding: 15px;
- margin: 15px;
- text-align: center;
+ border: 1px dashed #ccc;
+ border-radius: 5px;
+ padding: 15px;
+ margin: 15px;
+ text-align: center;
}
.bookmarklet a {
- font-weight: 600;
- text-decoration: none;
- font-size: 1.2em;
+ font-weight: 600;
+ text-decoration: none;
+ font-size: 1.2em;
}
.disabled {
- opacity: 20%;
+ opacity: 20%;
}
-audio, video {
- width: 100%;
+audio,
+video {
+ width: 100%;
}
-.media-controls{
- font-size: .9em;
- display: flex;
- flex-wrap: wrap;
+.media-controls {
+ font-size: 0.9em;
+ display: flex;
+ flex-wrap: wrap;
}
-.media-controls .media-control-label{
- line-height: 1em;
+.media-controls .media-control-label {
+ line-height: 1em;
}
-.media-controls>div{
- display: flex;
- flex-wrap: nowrap;
- justify-content:center;
- min-width: 50%;
- align-items: center;
+.media-controls > div {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: center;
+ min-width: 50%;
+ align-items: center;
}
-.media-controls>div>*{
- padding-left:12px;
+.media-controls > div > * {
+ padding-left: 12px;
}
-.media-controls>div>*:first-child{
- padding-left:0;
+.media-controls > div > *:first-child {
+ padding-left: 0;
}
-.media-controls span.speed-indicator{
- /*monospace to ensure constant width even when value change. JS ensure the value is always on 4 characters (in most cases)
+.media-controls span.speed-indicator {
+ /*monospace to ensure constant width even when value change. JS ensure the value is always on 4 characters (in most cases)
This reduce ui flickering due to element moving around a bit
*/
- font-family: monospace;
+ font-family: monospace;
}
.integration-form summary {
- font-weight: 700;
+ font-weight: 700;
}
.integration-form details {
- margin-bottom: 15px;
+ margin-bottom: 15px;
}
.integration-form details .form-section {
- margin-top: 15px;
+ margin-top: 15px;
}
-.hidden {
- display: none;
+.hidden,
+.offline-hidden {
+ display: none;
}
diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js
index d2732fff863..da011545c5b 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,30 +732,40 @@ function goToAddSubscription() {
* @param {Element} playerElement
*/
function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(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
- }
- const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
- const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
- const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion); //completion percentage to mark as read
- 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 the mark as read on completion
- if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) {
- const completion = currentPositionInSeconds / playerElement.duration;
- if (completion >= markAsReadOnCompletion) {
- handleEntryStatus("none", document.querySelector(":is(a, button)[data-toggle-status]"), true);
- }
- }
+ if (!isPlayerPlaying(playerElement)) {
+ return; //If the player is not playing, we do not want to save the progression and mark as read on completion
+ }
+ const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
+ const lastKnownPositionInSeconds = parseInt(
+ playerElement.dataset.lastPosition,
+ 10,
+ );
+ const markAsReadOnCompletion = parseFloat(
+ playerElement.dataset.markReadOnCompletion,
+ ); //completion percentage to mark as read
+ 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 the mark as read on completion
+ if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) {
+ const completion = currentPositionInSeconds / playerElement.duration;
+ if (completion >= markAsReadOnCompletion) {
+ handleEntryStatus(
+ "none",
+ document.querySelector(":is(a, button)[data-toggle-status]"),
+ true,
+ );
+ }
}
+ }
}
/**
@@ -711,59 +774,63 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
* @returns {boolean}
*/
function isPlayerPlaying(element) {
- return element &&
- element.currentTime > 0 &&
- !element.paused &&
- !element.ended &&
- element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
+ return (
+ element &&
+ element.currentTime > 0 &&
+ !element.paused &&
+ !element.ended &&
+ element.readyState > 2
+ ); //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
}
/**
* 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
- });
- } 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,
+ });
+ } 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 "";
}
/**
@@ -774,34 +841,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..7141bc9fc59 100644
--- a/internal/ui/static/js/service_worker.js
+++ b/internal/ui/static/js/service_worker.js
@@ -1,44 +1,126 @@
-
// 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";
+const cachedPages = [
+ "/unread",
+ "/starred",
+ "/stylesheets",
+ "/app",
+ "/service-worker",
+ "/manifest.json",
+ "/feed/icon",
+ "/icon",
+];
+
self.addEventListener("install", (event) => {
- 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" }));
- })()
- );
+ event.waitUntil(
+ (async () => {
+ const cache = await caches.open(CACHE_NAME);
- // Force the waiting service worker to become the active service worker.
- self.skipWaiting();
+ if (USE_CACHE) {
+ await cache.addAll(["/", "/unread", "/starred", 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();
});
+async function cacheFirstWithRefresh(request) {
+ const cache = await caches.open(CACHE_NAME);
+
+ const fetchResponsePromise = fetch(request).then(async (networkResponse) => {
+ if (!networkResponse.ok) return networkResponse;
+
+ const contentType = networkResponse.headers.get("Content-Type");
+ if (!contentType || !contentType.includes("text/html")) {
+ cache.put(request, networkResponse.clone());
+ return networkResponse;
+ }
+
+ const text = await networkResponse.clone().text();
+
+ const modifiedHtml = text.replace(/offline-hidden/g, "offline-visibe");
+
+ const clonedResponse = new Response(modifiedHtml, {
+ status: networkResponse.status,
+ statusText: networkResponse.statusText,
+ headers: networkResponse.headers,
+ });
+
+ cache.put(request, clonedResponse.clone());
+ return networkResponse;
+ });
+
+ try {
+ return (await cache.match(request)) || (await fetchResponsePromise);
+ } catch (error) {
+ const cache = await caches.open(CACHE_NAME);
+ return await cache.match(OFFLINE_URL);
+ }
+}
+
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;
- }
- })()
- );
+ if (USE_CACHE) {
+ const url = new URL(event.request.url);
+ if (cachedPages.some((page) => url.pathname.startsWith(page))) {
+ return event.respondWith(cacheFirstWithRefresh(event.request));
+ }
+ }
+
+ // 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.
+ return await fetch(event.request);
+ } 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);
+ return await cache.match(OFFLINE_URL);
+ }
+ })(),
+ );
+ }
+});
+
+self.addEventListener("load", async (event) => {
+ if (
+ navigator.onLine === true &&
+ event.target.location.pathname === "/unread" &&
+ USE_CACHE
+ ) {
+ const cache = await caches.open(CACHE_NAME);
+
+ for (let article of document.getElementsByTagName("article")) {
+ const as = article.getElementsByTagName("a");
+ if (as.length > 0) {
+ const a = as[0];
+ const href = a.href;
+ cache
+ .add(
+ new Request(href, {
+ headers: new Headers({
+ "Client-Type": "service-worker",
+ }),
+ }),
+ )
+ .then(() => {
+ article;
+ });
+ }
}
+ }
});
diff --git a/internal/ui/static_javascript.go b/internal/ui/static_javascript.go
index 36061ce94ea..46b119181ff 100644
--- a/internal/ui/static_javascript.go
+++ b/internal/ui/static_javascript.go
@@ -31,7 +31,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...)
}