diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 3626669b..ee44d045 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -784,6 +784,7 @@ "page.offline.message": "You are offline", "page.offline.refresh_page": "Try to refresh the page", "page.webauthn_rename.title": "Rename Passkey", + "page.cache.warning": "You're viewing a cached version. Refresh to see the latest one.", "alert.no_shared_entry": "There is no shared entry.", "alert.no_bookmark": "There are no starred entries.", "alert.no_category": "There is no category.", diff --git a/internal/template/templates/common/layout.html b/internal/template/templates/common/layout.html index 03b3c7ab..da3b7b38 100644 --- a/internal/template/templates/common/layout.html +++ b/internal/template/templates/common/layout.html @@ -132,7 +132,7 @@ {{ if .flashErrorMessage }}
{{ end }} - + {{template "page_header" .}} diff --git a/internal/ui/entry_unread.go b/internal/ui/entry_unread.go index bd1dd2fd..ece9c8af 100644 --- a/internal/ui/entry_unread.go +++ b/internal/ui/entry_unread.go @@ -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/static/css/common.css b/internal/ui/static/css/common.css index 2b3be8dc..ff0e56a7 100644 --- a/internal/ui/static/css/common.css +++ b/internal/ui/static/css/common.css @@ -1,416 +1,442 @@ /* 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; - } + body { + margin: auto; + max-width: 820px; + } - .header { - padding-left: 3px; - } + .header { + padding-left: 3px; + } - .header li { - display: inline-block; - padding: 0 12px 0 0; - line-height: normal; - border: none; - font-size: 1.0em; - } + .header li { + display: inline-block; + padding: 0 12px 0 0; + line-height: normal; + border: none; + font-size: 1em; + } - .header nav { - align-items: end; - flex-direction: row; - } + .header nav { + align-items: end; + flex-direction: row; + } - .header .logo svg { - display: none; - } + .header .logo svg { + display: none; + } - .header ul:not(.js-menu-show), .header ul.js-menu-show { - display: revert; - } + .header ul:not(.js-menu-show), + .header ul.js-menu-show { + display: revert; + } - .header :is(a, summary):hover { - color: var(--header-link-hover-color); - } + .header :is(a, summary):hover { + color: var(--header-link-hover-color); + } - .page-header li, - .page-footer li { - display: inline; - padding-right: 15px; - } + .page-header li, + .page-footer li { + display: inline; + padding-right: 15px; + } - .pagination-backward, - .pagination-forward { - display: flex; - } + .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"], @@ -418,15 +444,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, @@ -434,863 +460,870 @@ 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: 30px 5px 5px; + 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: 30px 5px 5px; } #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; + 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 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 { - font-size: 0.9em; - padding: 1ch; - margin-bottom: 15px; - font-style: italic; - border: dotted var(--entry-content-aside-border-color) 2px; + font-size: 0.9em; + padding: 1ch; + margin-bottom: 15px; + 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/service_worker.js b/internal/ui/static/js/service_worker.js index 3a6f622b..95ea8905 100644 --- a/internal/ui/static/js/service_worker.js +++ b/internal/ui/static/js/service_worker.js @@ -23,39 +23,53 @@ self.addEventListener("install", (event) => { 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; + }); + + return (await cache.match(request)) || (await fetchResponsePromise); +} + self.addEventListener("fetch", (event) => { + if (USE_CACHE) { + return event.respondWith(cacheFirstWithRefresh(event.request)); + } + // We proxify requests through fetch() only if we are offline because it's slower. - if ( - USE_CACHE || - (navigator.onLine === false && event.request.mode === "navigate") - ) { + if (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; + 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); - - 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); } })(), @@ -91,8 +105,3 @@ self.addEventListener("load", async (event) => { } } }); - -self.addEventListener("DOMContentLoaded", function () { - const offlineFlag = document.getElementById("offline-flag"); - offlineFlag.classList.toggle("hidden", navigator.onLine); -});