diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json index 64184b99..c5eccd8c 100644 --- a/locale/translations/de_DE.json +++ b/locale/translations/de_DE.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Aktionen", "page.api_keys.never_used": "Nie benutzt", "page.new_api_key.title": "Neuer API-Schlüssel", + "page.offline.title": "Offline-Modus", + "page.offline.message": "Du bist offline", + "page.offline.refresh_page": "Versuchen Sie, die Seite zu aktualisieren", "alert.no_shared_entry": "Es existieren derzeit keine geteilten Artikel.", "alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.", "alert.no_category": "Es ist keine Kategorie vorhanden.", diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json index 6cd654a6..53dc9efb 100644 --- a/locale/translations/en_US.json +++ b/locale/translations/en_US.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Actions", "page.api_keys.never_used": "Never Used", "page.new_api_key.title": "New API Key", + "page.offline.title": "Offline Mode", + "page.offline.message": "You are offline", + "page.offline.refresh_page": "Try to refresh the page", "alert.no_shared_entry": "There is no shared entry.", "alert.no_bookmark": "There is no bookmark at the moment.", "alert.no_category": "There is no category.", diff --git a/locale/translations/es_ES.json b/locale/translations/es_ES.json index 1944f31a..4716a1d6 100644 --- a/locale/translations/es_ES.json +++ b/locale/translations/es_ES.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Acciones", "page.api_keys.never_used": "Nunca usado", "page.new_api_key.title": "Nueva clave API", + "page.offline.title": "Modo offline", + "page.offline.message": "Estas desconectado", + "page.offline.refresh_page": "Intenta actualizar la página", "alert.no_shared_entry": "No hay entrada compartida.", "alert.no_bookmark": "No hay marcador en este momento.", "alert.no_category": "No hay categoría.", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index 09dd3b10..4b990173 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Actions", "page.api_keys.never_used": "Jamais utilisé", "page.new_api_key.title": "Nouvelle clé d'API", + "page.offline.title": "Mode Hors-Ligne", + "page.offline.message": "Vous n'êtes pas connecté", + "page.offline.refresh_page": "Essayez de rafraîchir la page", "alert.no_shared_entry": "Il n'y a pas d'article partagé.", "alert.no_bookmark": "Il n'y a aucun favoris pour le moment.", "alert.no_category": "Il n'y a aucune catégorie.", diff --git a/locale/translations/it_IT.json b/locale/translations/it_IT.json index 34e10156..3003c4d8 100644 --- a/locale/translations/it_IT.json +++ b/locale/translations/it_IT.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Azioni", "page.api_keys.never_used": "Mai usato", "page.new_api_key.title": "Nuova chiave API", + "page.offline.title": "Modalità offline", + "page.offline.message": "Sei offline", + "page.offline.refresh_page": "Prova ad aggiornare la pagina", "alert.no_shared_entry": "Non ci sono voci condivise.", "alert.no_bookmark": "Nessun preferito disponibile.", "alert.no_category": "Nessuna categoria disponibile.", diff --git a/locale/translations/ja_JP.json b/locale/translations/ja_JP.json index a4a8fb79..235035b6 100644 --- a/locale/translations/ja_JP.json +++ b/locale/translations/ja_JP.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "アクション", "page.api_keys.never_used": "使われたことがない", "page.new_api_key.title": "新しいAPIキー", + "page.offline.title": "オフラインモード", + "page.offline.message": "オフラインです", + "page.offline.refresh_page": "ページを更新してみてください", "alert.no_shared_entry": "共有エントリはありません。", "alert.no_bookmark": "現在星付きはありません。", "alert.no_category": "カテゴリが存在しません。", diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json index aafb6955..71c547b3 100644 --- a/locale/translations/nl_NL.json +++ b/locale/translations/nl_NL.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Acties", "page.api_keys.never_used": "Nooit gebruikt", "page.new_api_key.title": "Nieuwe API-sleutel", + "page.offline.title": "Offline modus", + "page.offline.message": "Je bent offline", + "page.offline.refresh_page": "Probeer de pagina te vernieuwen", "alert.no_shared_entry": "Er is geen gedeelde toegang.", "alert.no_bookmark": "Er zijn op dit moment geen favorieten.", "alert.no_category": "Er zijn geen categorieën.", diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json index fd3f1b90..ac8313c0 100644 --- a/locale/translations/pl_PL.json +++ b/locale/translations/pl_PL.json @@ -202,6 +202,9 @@ "page.api_keys.table.actions": "Działania", "page.api_keys.never_used": "Nigdy nie używany", "page.new_api_key.title": "Nowy klucz API", + "page.offline.title": "Tryb offline", + "page.offline.message": "Jesteś odłączony od sieci", + "page.offline.refresh_page": "Spróbuj odświeżyć stronę", "alert.no_shared_entry": "Brak wspólnego wpisu.", "alert.no_bookmark": "Obecnie nie ma żadnych zakładek.", "alert.no_category": "Nie ma żadnej kategorii!", diff --git a/locale/translations/pt_BR.json b/locale/translations/pt_BR.json index 360f1f3c..3f0da761 100644 --- a/locale/translations/pt_BR.json +++ b/locale/translations/pt_BR.json @@ -200,6 +200,9 @@ "page.api_keys.table.actions": "Ações", "page.api_keys.never_used": "Nunca usado", "page.new_api_key.title": "Nova chave de API", + "page.offline.title": "Modo offline", + "page.offline.message": "Você está offline", + "page.offline.refresh_page": "Tente atualizar a página", "alert.no_shared_entry": "Não há itens compartilhados.", "alert.no_bookmark": "Não há favorito neste momento.", "alert.no_category": "Não há categoria.", diff --git a/locale/translations/ru_RU.json b/locale/translations/ru_RU.json index 0a5a3822..03f96fc0 100644 --- a/locale/translations/ru_RU.json +++ b/locale/translations/ru_RU.json @@ -202,6 +202,9 @@ "page.api_keys.table.actions": "Действия", "page.api_keys.never_used": "Никогда не использовался", "page.new_api_key.title": "Новый API-ключ", + "page.offline.title": "Автономный режим", + "page.offline.message": "Ты не в сети", + "page.offline.refresh_page": "Попробуйте обновить страницу", "alert.no_shared_entry": "Общедоступные записи отсутствуют.", "alert.no_bookmark": "Избранное отсутствует.", "alert.no_category": "Категории отсутствуют.", diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json index eeb6f312..0f4f8838 100644 --- a/locale/translations/zh_CN.json +++ b/locale/translations/zh_CN.json @@ -198,6 +198,9 @@ "page.api_keys.table.actions": "操作", "page.api_keys.never_used": "没用过", "page.new_api_key.title": "新的API密钥", + "page.offline.title": "离线模式", + "page.offline.message": "您离线", + "page.offline.refresh_page": "尝试刷新页面", "alert.no_shared_entry": "没有共享条目。", "alert.no_bookmark": "目前没有书签", "alert.no_category": "目前没有分类", diff --git a/template/engine.go b/template/engine.go index 5f4d71e2..55eb7e31 100644 --- a/template/engine.go +++ b/template/engine.go @@ -24,6 +24,9 @@ var commonTemplateFiles embed.FS //go:embed templates/views/*.html var viewTemplateFiles embed.FS +//go:embed templates/standalone/*.html +var standaloneTemplateFiles embed.FS + // Engine handles the templating system. type Engine struct { templates map[string]*template.Template @@ -75,6 +78,22 @@ func (e *Engine) ParseTemplates() error { e.templates[templateName] = template.Must(template.New("main").Funcs(e.funcMap.Map()).Parse(templateContents.String())) } + dirEntries, err = standaloneTemplateFiles.ReadDir("templates/standalone") + if err != nil { + return err + } + + for _, dirEntry := range dirEntries { + templateName := dirEntry.Name() + fileData, err := standaloneTemplateFiles.ReadFile("templates/standalone/" + dirEntry.Name()) + if err != nil { + return err + } + + logger.Debug("[Template] Parsing: %s", templateName) + e.templates[templateName] = template.Must(template.New("base").Funcs(e.funcMap.Map()).Parse(string(fileData))) + } + return nil } diff --git a/template/templates/standalone/offline.html b/template/templates/standalone/offline.html new file mode 100644 index 00000000..60eecb4e --- /dev/null +++ b/template/templates/standalone/offline.html @@ -0,0 +1,13 @@ + + + + + {{ t "page.offline.title" }} - Miniflux + + + + + +

{{ t "page.offline.message" }} ‐ {{ t "page.offline.refresh_page" }}.

+ + \ No newline at end of file diff --git a/ui/middleware.go b/ui/middleware.go index 067aa53d..21af40af 100644 --- a/ui/middleware.go +++ b/ui/middleware.go @@ -142,7 +142,8 @@ func (m *middleware) isPublicRoute(r *http.Request) bool { "webManifest", "robots", "sharedEntry", - "healthcheck": + "healthcheck", + "offline": return true default: return false diff --git a/ui/offline.go b/ui/offline.go new file mode 100644 index 00000000..b6c4dff7 --- /dev/null +++ b/ui/offline.go @@ -0,0 +1,20 @@ +// Copyright 2021 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package ui // import "miniflux.app/ui" + +import ( + "net/http" + + "miniflux.app/http/request" + "miniflux.app/http/response/html" + "miniflux.app/ui/session" + "miniflux.app/ui/view" +) + +func (h *handler) showOfflinePage(w http.ResponseWriter, r *http.Request) { + sess := session.New(h.store, request.SessionID(r)) + view := view.New(h.tpl, r, sess) + html.OK(w, r, view.Render("offline")) +} diff --git a/ui/static/js/.jshintrc b/ui/static/js/.jshintrc index 53b202cb..80fc4c09 100644 --- a/ui/static/js/.jshintrc +++ b/ui/static/js/.jshintrc @@ -1,3 +1,3 @@ { - "esversion": 6 + "esversion": 8 } \ No newline at end of file diff --git a/ui/static/js/service_worker.js b/ui/static/js/service_worker.js index 8e32fcbb..37cce257 100644 --- a/ui/static/js/service_worker.js +++ b/ui/static/js/service_worker.js @@ -1,14 +1,44 @@ + +// 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 CACHE_NAME = "offline"; + +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" })); + })() + ); + + // Force the waiting service worker to become the active service worker. + self.skipWaiting(); +}); + self.addEventListener("fetch", (event) => { - if (event.request.url.includes("/feed/icon/")) { + // We proxify requests through fetch() only if we are offline because it's slower. + if (navigator.onLine === false && event.request.mode === "navigate") { event.respondWith( - caches.open("feed_icons").then((cache) => { - return cache.match(event.request).then((response) => { - return response || fetch(event.request).then((response) => { - cache.put(event.request, response.clone()); - return response; - }); - }); - }) + (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; + } + })() ); } }); diff --git a/ui/static/static.go b/ui/static/static.go index 8e78a8dd..5f1bd003 100644 --- a/ui/static/static.go +++ b/ui/static/static.go @@ -127,8 +127,7 @@ func GenerateJavascriptBundles() error { } var prefixes = map[string]string{ - "app": "(function(){'use strict';", - "service-worker": "'use strict';", + "app": "(function(){'use strict';", } var suffixes = map[string]string{ diff --git a/ui/static_javascript.go b/ui/static_javascript.go index cbe95827..99255d84 100644 --- a/ui/static_javascript.go +++ b/ui/static_javascript.go @@ -5,12 +5,14 @@ package ui // import "miniflux.app/ui" import ( + "fmt" "net/http" "time" "miniflux.app/http/request" "miniflux.app/http/response" "miniflux.app/http/response/html" + "miniflux.app/http/route" "miniflux.app/ui/static" ) @@ -23,8 +25,15 @@ func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) { } response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) { + contents := static.JavascriptBundles[filename] + + if filename == "service-worker" { + variables := fmt.Sprintf(`const OFFLINE_URL="%s";`, route.Path(h.router, "offline")) + contents = append([]byte(variables)[:], contents[:]...) + } + b.WithHeader("Content-Type", "text/javascript; charset=utf-8") - b.WithBody(static.JavascriptBundles[filename]) + b.WithBody(contents) b.Write() }) } diff --git a/ui/ui.go b/ui/ui.go index f5339a24..a5af4910 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -145,6 +145,9 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { uiRouter.HandleFunc("/oauth2/{provider}/redirect", handler.oauth2Redirect).Name("oauth2Redirect").Methods(http.MethodGet) uiRouter.HandleFunc("/oauth2/{provider}/callback", handler.oauth2Callback).Name("oauth2Callback").Methods(http.MethodGet) + // Offline page + uiRouter.HandleFunc("/offline", handler.showOfflinePage).Name("offline").Methods(http.MethodGet) + // Authentication pages. uiRouter.HandleFunc("/login", handler.checkLogin).Name("checkLogin").Methods(http.MethodPost) uiRouter.HandleFunc("/logout", handler.logout).Name("logout").Methods(http.MethodGet)