From 562abeb997aed83be9e51c7405f7ceb54aaf526d Mon Sep 17 00:00:00 2001 From: Kelly Norton Date: Sat, 17 May 2025 13:39:25 -0400 Subject: [PATCH] feat: read external links --- client/model.go | 114 +++++++++--------- internal/database/migrations.go | 4 + internal/locale/translations/de_DE.json | 15 +-- internal/locale/translations/el_EL.json | 15 +-- internal/locale/translations/en_US.json | 15 +-- internal/locale/translations/es_ES.json | 15 +-- internal/locale/translations/fi_FI.json | 15 +-- internal/locale/translations/fr_FR.json | 15 +-- internal/locale/translations/hi_IN.json | 15 +-- internal/locale/translations/id_ID.json | 13 +- internal/locale/translations/it_IT.json | 15 +-- internal/locale/translations/ja_JP.json | 13 +- .../locale/translations/nan_Latn_pehoeji.json | 13 +- internal/locale/translations/nl_NL.json | 15 +-- internal/locale/translations/pl_PL.json | 17 +-- internal/locale/translations/pt_BR.json | 15 +-- internal/locale/translations/ro_RO.json | 17 +-- internal/locale/translations/ru_RU.json | 23 ++-- internal/locale/translations/tr_TR.json | 15 +-- internal/locale/translations/uk_UA.json | 17 +-- internal/locale/translations/zh_CN.json | 13 +- internal/locale/translations/zh_TW.json | 13 +- internal/model/user.go | 6 + internal/storage/user.go | 33 +++-- .../template/templates/views/settings.html | 2 + internal/ui/entry_bookmark.go | 5 + internal/ui/entry_category.go | 5 + internal/ui/entry_feed.go | 5 + internal/ui/entry_read.go | 5 + internal/ui/entry_search.go | 5 + internal/ui/entry_unread.go | 5 + internal/ui/form/settings.go | 63 +++++----- internal/ui/form/settings_test.go | 87 ++++++------- internal/ui/settings_show.go | 47 ++++---- internal/ui/unread_entry_category.go | 5 + internal/ui/unread_entry_feed.go | 5 + 36 files changed, 397 insertions(+), 303 deletions(-) diff --git a/client/model.go b/client/model.go index df49283a..ed9b4994 100644 --- a/client/model.go +++ b/client/model.go @@ -17,35 +17,36 @@ const ( // User represents a user in the system. type User struct { - ID int64 `json:"id"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - IsAdmin bool `json:"is_admin"` - Theme string `json:"theme"` - Language string `json:"language"` - Timezone string `json:"timezone"` - EntryDirection string `json:"entry_sorting_direction"` - EntryOrder string `json:"entry_sorting_order"` - Stylesheet string `json:"stylesheet"` - CustomJS string `json:"custom_js"` - GoogleID string `json:"google_id"` - OpenIDConnectID string `json:"openid_connect_id"` - EntriesPerPage int `json:"entries_per_page"` - KeyboardShortcuts bool `json:"keyboard_shortcuts"` - ShowReadingTime bool `json:"show_reading_time"` - EntrySwipe bool `json:"entry_swipe"` - GestureNav string `json:"gesture_nav"` - LastLoginAt *time.Time `json:"last_login_at"` - DisplayMode string `json:"display_mode"` - DefaultReadingSpeed int `json:"default_reading_speed"` - CJKReadingSpeed int `json:"cjk_reading_speed"` - DefaultHomePage string `json:"default_home_page"` - CategoriesSortingOrder string `json:"categories_sorting_order"` - MarkReadOnView bool `json:"mark_read_on_view"` - MediaPlaybackRate float64 `json:"media_playback_rate"` - BlockFilterEntryRules string `json:"block_filter_entry_rules"` - KeepFilterEntryRules string `json:"keep_filter_entry_rules"` - ExternalFontHosts string `json:"external_font_hosts"` + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + IsAdmin bool `json:"is_admin"` + Theme string `json:"theme"` + Language string `json:"language"` + Timezone string `json:"timezone"` + EntryDirection string `json:"entry_sorting_direction"` + EntryOrder string `json:"entry_sorting_order"` + Stylesheet string `json:"stylesheet"` + CustomJS string `json:"custom_js"` + GoogleID string `json:"google_id"` + OpenIDConnectID string `json:"openid_connect_id"` + EntriesPerPage int `json:"entries_per_page"` + KeyboardShortcuts bool `json:"keyboard_shortcuts"` + ShowReadingTime bool `json:"show_reading_time"` + EntrySwipe bool `json:"entry_swipe"` + GestureNav string `json:"gesture_nav"` + LastLoginAt *time.Time `json:"last_login_at"` + DisplayMode string `json:"display_mode"` + DefaultReadingSpeed int `json:"default_reading_speed"` + CJKReadingSpeed int `json:"cjk_reading_speed"` + DefaultHomePage string `json:"default_home_page"` + CategoriesSortingOrder string `json:"categories_sorting_order"` + MarkReadOnView bool `json:"mark_read_on_view"` + MediaPlaybackRate float64 `json:"media_playback_rate"` + BlockFilterEntryRules string `json:"block_filter_entry_rules"` + KeepFilterEntryRules string `json:"keep_filter_entry_rules"` + ExternalFontHosts string `json:"external_font_hosts"` + AlwaysOpenExternalLinks bool `json:"always_open_external_links"` } func (u User) String() string { @@ -63,33 +64,34 @@ type UserCreationRequest struct { // UserModificationRequest represents the request to update a user. type UserModificationRequest struct { - Username *string `json:"username"` - Password *string `json:"password"` - IsAdmin *bool `json:"is_admin"` - Theme *string `json:"theme"` - Language *string `json:"language"` - Timezone *string `json:"timezone"` - EntryDirection *string `json:"entry_sorting_direction"` - EntryOrder *string `json:"entry_sorting_order"` - Stylesheet *string `json:"stylesheet"` - CustomJS *string `json:"custom_js"` - GoogleID *string `json:"google_id"` - OpenIDConnectID *string `json:"openid_connect_id"` - EntriesPerPage *int `json:"entries_per_page"` - KeyboardShortcuts *bool `json:"keyboard_shortcuts"` - ShowReadingTime *bool `json:"show_reading_time"` - EntrySwipe *bool `json:"entry_swipe"` - GestureNav *string `json:"gesture_nav"` - DisplayMode *string `json:"display_mode"` - DefaultReadingSpeed *int `json:"default_reading_speed"` - CJKReadingSpeed *int `json:"cjk_reading_speed"` - DefaultHomePage *string `json:"default_home_page"` - CategoriesSortingOrder *string `json:"categories_sorting_order"` - MarkReadOnView *bool `json:"mark_read_on_view"` - MediaPlaybackRate *float64 `json:"media_playback_rate"` - BlockFilterEntryRules *string `json:"block_filter_entry_rules"` - KeepFilterEntryRules *string `json:"keep_filter_entry_rules"` - ExternalFontHosts *string `json:"external_font_hosts"` + Username *string `json:"username"` + Password *string `json:"password"` + IsAdmin *bool `json:"is_admin"` + Theme *string `json:"theme"` + Language *string `json:"language"` + Timezone *string `json:"timezone"` + EntryDirection *string `json:"entry_sorting_direction"` + EntryOrder *string `json:"entry_sorting_order"` + Stylesheet *string `json:"stylesheet"` + CustomJS *string `json:"custom_js"` + GoogleID *string `json:"google_id"` + OpenIDConnectID *string `json:"openid_connect_id"` + EntriesPerPage *int `json:"entries_per_page"` + KeyboardShortcuts *bool `json:"keyboard_shortcuts"` + ShowReadingTime *bool `json:"show_reading_time"` + EntrySwipe *bool `json:"entry_swipe"` + GestureNav *string `json:"gesture_nav"` + DisplayMode *string `json:"display_mode"` + DefaultReadingSpeed *int `json:"default_reading_speed"` + CJKReadingSpeed *int `json:"cjk_reading_speed"` + DefaultHomePage *string `json:"default_home_page"` + CategoriesSortingOrder *string `json:"categories_sorting_order"` + MarkReadOnView *bool `json:"mark_read_on_view"` + MediaPlaybackRate *float64 `json:"media_playback_rate"` + BlockFilterEntryRules *string `json:"block_filter_entry_rules"` + KeepFilterEntryRules *string `json:"keep_filter_entry_rules"` + ExternalFontHosts *string `json:"external_font_hosts"` + AlwaysOpenExternalLinks *bool `json:"always_open_external_links"` } // Users represents a list of users. diff --git a/internal/database/migrations.go b/internal/database/migrations.go index c4b20358..327336d5 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -1073,4 +1073,8 @@ var migrations = []func(tx *sql.Tx, driver string) error{ _, err = tx.Exec(sql) return }, + func(tx *sql.Tx, _ string) (err error) { + _, err = tx.Exec(`ALTER TABLE users ADD COLUMN always_open_external_links bool default 'f'`) + return err + }, } diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 2d4c32cb..0c40a220 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden", "form.prefs.label.mark_read_on_view_or_media_completion": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden. Audio/Video bei 90%% Wiedergabe als gelesen markieren", "form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen", "form.prefs.label.theme": "Thema", "form.prefs.label.timezone": "Zeitzone", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Zuletzt verwendeten", "page.api_keys.table.token": "Zeichen", "page.api_keys.title": "API-Schlüssel", - "page.categories_count": [ - "%d Kategorie", - "%d Kategorien" - ], "page.categories.entries": "Artikel", "page.categories.feed_count": [ "Es gibt %d Abonnement.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Abonnements", "page.categories.no_feed": "Kein Abonnement.", "page.categories.title": "Kategorien", + "page.categories_count": [ + "%d Kategorie", + "%d Kategorien" + ], "page.category_label": "Kategorie: %s", "page.edit_category.title": "Kategorie bearbeiten: %s", "page.edit_feed.etag_header": "ETag-Kopfzeile:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Hauptschlüssel registrieren", "page.settings.webauthn.register.error": "Hauptschlüssel kann nicht registriert werden", + "page.shared_entries.title": "Geteilte Artikel", "page.shared_entries_count": [ "%d geteilter Artikel", "%d geteilte Artikel" ], - "page.shared_entries.title": "Geteilte Artikel", + "page.starred.title": "Lesezeichen", "page.starred_entry_count": [ "%d Lesezeichen", "%d Lesezeichen" ], - "page.starred.title": "Lesezeichen", "page.total_entry_count": [ "%d Artikel insgesamt", "%d Artikel insgesamt" ], + "page.unread.title": "Ungelesen", "page.unread_entry_count": [ "%d ungelesener Artikel", "%d ungelesene Artikel" ], - "page.unread.title": "Ungelesen", "page.users.actions": "Aktionen", "page.users.admin.no": "Nein", "page.users.admin.yes": "Ja", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 919def75..851b4233 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα", "form.prefs.label.theme": "Θέμα", "form.prefs.label.timezone": "Ζώνη Ώρας", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Τελευταία Χρήση", "page.api_keys.table.token": "Token", "page.api_keys.title": "Κλειδιά API", - "page.categories_count": [ - "%d category", - "%d categories" - ], "page.categories.entries": "Άρθρα", "page.categories.feed_count": [ "Υπάρχει μία %d ροή.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Συνδρομές", "page.categories.no_feed": "Καμία ροή.", "page.categories.title": "Κατηγορίες", + "page.categories_count": [ + "%d category", + "%d categories" + ], "page.category_label": "Category: %s", "page.edit_category.title": "Επεξεργασία κατηγορίας: % s", "page.edit_feed.etag_header": "Κεφαλίδα ETag:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Εγγραφή κωδικού πρόσβασης", "page.settings.webauthn.register.error": "Δεν είναι δυνατή η εγγραφή του κωδικού πρόσβασης", + "page.shared_entries.title": "Κοινόχρηστες Καταχωρήσεις", "page.shared_entries_count": [ "%d shared entry", "%d shared entries" ], - "page.shared_entries.title": "Κοινόχρηστες Καταχωρήσεις", + "page.starred.title": "Αγαπημένo", "page.starred_entry_count": [ "%d starred entry", "%d starred entries" ], - "page.starred.title": "Αγαπημένo", "page.total_entry_count": [ "%d entry in total", "%d entries in total" ], + "page.unread.title": "Μη αναγνωσμένα", "page.unread_entry_count": [ "%d unread entry", "%d unread entries" ], - "page.unread.title": "Μη αναγνωσμένα", "page.users.actions": "Eνέργειες", "page.users.admin.no": "Όχι", "page.users.admin.yes": "Ναι.", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index bfdd86d8..b8d53339 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Playback speed of the audio/video", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Show estimated reading time for entries", "form.prefs.label.theme": "Theme", "form.prefs.label.timezone": "Timezone", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Last Used", "page.api_keys.table.token": "Token", "page.api_keys.title": "API Keys", - "page.categories_count": [ - "%d category", - "%d categories" - ], "page.categories.entries": "Entries", "page.categories.feed_count": [ "There is %d feed.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Feeds", "page.categories.no_feed": "No feed.", "page.categories.title": "Categories", + "page.categories_count": [ + "%d category", + "%d categories" + ], "page.category_label": "Category: %s", "page.edit_category.title": "Edit Category: %s", "page.edit_feed.etag_header": "ETag header:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Register passkey", "page.settings.webauthn.register.error": "Unable to register passkey", + "page.shared_entries.title": "Shared entries", "page.shared_entries_count": [ "%d shared entry", "%d shared entries" ], - "page.shared_entries.title": "Shared entries", + "page.starred.title": "Starred", "page.starred_entry_count": [ "%d starred entry", "%d starred entries" ], - "page.starred.title": "Starred", "page.total_entry_count": [ "%d entry in total", "%d entries in total" ], + "page.unread.title": "Unread", "page.unread_entry_count": [ "%d unread entry", "%d unread entries" ], - "page.unread.title": "Unread", "page.users.actions": "Actions", "page.users.admin.no": "No", "page.users.admin.yes": "Yes", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index 65f41dcc..a9da1bfc 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean", "form.prefs.label.mark_read_on_view_or_media_completion": "Marcar las entradas como leídas cuando se vean. Para audio/video, marcar como leído al 90%% de finalización", "form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos", "form.prefs.label.theme": "Tema", "form.prefs.label.timezone": "Zona horaria", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Último utilizado", "page.api_keys.table.token": "simbólico", "page.api_keys.title": "Claves API", - "page.categories_count": [ - "%d categoría", - "%d categorías" - ], "page.categories.entries": "Artículos", "page.categories.feed_count": [ "Hay %d fuente.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Fuentes", "page.categories.no_feed": "Sin fuente.", "page.categories.title": "Categorías", + "page.categories_count": [ + "%d categoría", + "%d categorías" + ], "page.category_label": "Categoría: %s", "page.edit_category.title": "Editar categoría: %s", "page.edit_feed.etag_header": "Cabecera de ETag:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Claves de acceso", "page.settings.webauthn.register": "Registrar clave de acceso", "page.settings.webauthn.register.error": "No se puede registrar la clave de acceso", + "page.shared_entries.title": "Artículos compartidos", "page.shared_entries_count": [ "%d artículo compartido", "%d artículos compartidos" ], - "page.shared_entries.title": "Artículos compartidos", + "page.starred.title": "Marcadores", "page.starred_entry_count": [ "%d artículo marcado", "%d artículos marcados" ], - "page.starred.title": "Marcadores", "page.total_entry_count": [ "%d artículo en total", "%d artículos en total" ], + "page.unread.title": "No leídos", "page.unread_entry_count": [ "%d artículo no leído", "%d artículos no leídos" ], - "page.unread.title": "No leídos", "page.users.actions": "Acciones", "page.users.admin.no": "No", "page.users.admin.yes": "Sí", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index dd9e105d..778e8dbe 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika", "form.prefs.label.theme": "Teema", "form.prefs.label.timezone": "Aikavyöhyke", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Viimeksi käytetty", "page.api_keys.table.token": "Tunnus", "page.api_keys.title": "API-avaimet", - "page.categories_count": [ - "%d category", - "%d categories" - ], "page.categories.entries": "Artikkelit", "page.categories.feed_count": [ "On %d syöte.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Tilaukset", "page.categories.no_feed": "Ei syötettä.", "page.categories.title": "Kategoriat", + "page.categories_count": [ + "%d category", + "%d categories" + ], "page.category_label": "Category: %s", "page.edit_category.title": "Muokkaa kategoria: %s", "page.edit_feed.etag_header": "ETag-otsikko:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Rekisteröi salasana", "page.settings.webauthn.register.error": "Salasanaa ei voi rekisteröidä", + "page.shared_entries.title": "Jaetut artikkelit", "page.shared_entries_count": [ "%d shared entry", "%d shared entries" ], - "page.shared_entries.title": "Jaetut artikkelit", + "page.starred.title": "Suosikit", "page.starred_entry_count": [ "%d starred entry", "%d starred entries" ], - "page.starred.title": "Suosikit", "page.total_entry_count": [ "%d entry in total", "%d entries in total" ], + "page.unread.title": "Lukemattomat", "page.unread_entry_count": [ "%d unread entry", "%d unread entries" ], - "page.unread.title": "Lukemattomat", "page.users.actions": "Toiminnot", "page.users.admin.no": "Ei", "page.users.admin.yes": "Kyllä", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 92a6cd1d..e8cf8f27 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées", "form.prefs.label.mark_read_on_view_or_media_completion": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées. Pour l'audio/vidéo, marquer comme lues après 90%%", "form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles", "form.prefs.label.theme": "Thème", "form.prefs.label.timezone": "Fuseau horaire", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Dernière utilisation", "page.api_keys.table.token": "Jeton", "page.api_keys.title": "Clés d'API", - "page.categories_count": [ - "%d catégorie", - "%d catégories" - ], "page.categories.entries": "Articles", "page.categories.feed_count": [ "Il y a %d abonnement.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Abonnements", "page.categories.no_feed": "Aucun abonnement.", "page.categories.title": "Catégories", + "page.categories_count": [ + "%d catégorie", + "%d catégories" + ], "page.category_label": "Catégorie : %s", "page.edit_category.title": "Modification de la catégorie : %s", "page.edit_feed.etag_header": "En-tête ETag :", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Clés d’accès", "page.settings.webauthn.register": "Enregister une nouvelle clé d’accès", "page.settings.webauthn.register.error": "Impossible d'enregistrer la clé d’accès", + "page.shared_entries.title": "Articles partagés", "page.shared_entries_count": [ "%d article partagé", "%d articles partagés" ], - "page.shared_entries.title": "Articles partagés", + "page.starred.title": "Favoris", "page.starred_entry_count": [ "%d favori", "%d favoris" ], - "page.starred.title": "Favoris", "page.total_entry_count": [ "%d article au total", "%d articles au total" ], + "page.unread.title": "Non lus", "page.unread_entry_count": [ "%d article non lu", "%d articles non lus" ], - "page.unread.title": "Non lus", "page.users.actions": "Actions", "page.users.admin.no": "Non", "page.users.admin.yes": "Oui", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index 67efbf9b..49f4b354 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं", "form.prefs.label.theme": "थीम", "form.prefs.label.timezone": "समय क्षेत्र", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "आखरी इस्त्तमाल किया गया", "page.api_keys.table.token": "टोकन", "page.api_keys.title": "एपीआई कुंजी", - "page.categories_count": [ - "%d category", - "%d categories" - ], "page.categories.entries": "विषयवस्तुया", "page.categories.feed_count": [ "%d फ़ीड बाकी है।", @@ -440,6 +437,10 @@ "page.categories.feeds": "सदस्यता ले", "page.categories.no_feed": "कोई फ़ीड नहीं है।", "page.categories.title": "श्रेणियाँ", + "page.categories_count": [ + "%d category", + "%d categories" + ], "page.category_label": "Category: %s", "page.edit_category.title": "%s श्रेणी संपाद करे", "page.edit_feed.etag_header": "ईटाग हैडर:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "रजिस्टर पासकी", "page.settings.webauthn.register.error": "पासकी पंजीकृत करने में असमर्थ", + "page.shared_entries.title": "साझा किया हुआ प्रविष्टि", "page.shared_entries_count": [ "%d shared entry", "%d shared entries" ], - "page.shared_entries.title": "साझा किया हुआ प्रविष्टि", + "page.starred.title": "तारांकित", "page.starred_entry_count": [ "%d starred entry", "%d starred entries" ], - "page.starred.title": "तारांकित", "page.total_entry_count": [ "%d entry in total", "%d entries in total" ], + "page.unread.title": "अपठित", "page.unread_entry_count": [ "%d unread entry", "%d unread entries" ], - "page.unread.title": "अपठित", "page.users.actions": "कार्रवाई", "page.users.admin.no": "नहीं", "page.users.admin.yes": "हां", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 4034a6d7..c7e31481 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -346,6 +346,7 @@ "form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel", "form.prefs.label.theme": "Tema", "form.prefs.label.timezone": "Zona Waktu", @@ -426,9 +427,6 @@ "page.api_keys.table.last_used_at": "Terakhir Digunakan", "page.api_keys.table.token": "Token", "page.api_keys.title": "Kunci API", - "page.categories_count": [ - "%d category" - ], "page.categories.entries": "Artikel", "page.categories.feed_count": [ "Ada %d umpan." @@ -436,6 +434,9 @@ "page.categories.feeds": "Langganan", "page.categories.no_feed": "Tidak ada umpan.", "page.categories.title": "Kategori", + "page.categories_count": [ + "%d category" + ], "page.category_label": "Category: %s", "page.edit_category.title": "Sunting Kategori: %s", "page.edit_feed.etag_header": "Tajuk ETag:", @@ -538,21 +539,21 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Register passkey", "page.settings.webauthn.register.error": "Unable to register passkey", + "page.shared_entries.title": "Entri yang Dibagikan", "page.shared_entries_count": [ "%d shared entry" ], - "page.shared_entries.title": "Entri yang Dibagikan", + "page.starred.title": "Markah", "page.starred_entry_count": [ "%d starred entry" ], - "page.starred.title": "Markah", "page.total_entry_count": [ "%d entry in total" ], + "page.unread.title": "Belum Dibaca", "page.unread_entry_count": [ "%d unread entry" ], - "page.unread.title": "Belum Dibaca", "page.users.actions": "Tindakan", "page.users.admin.no": "Tidak", "page.users.admin.yes": "Ya", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index 58fb947d..503f612b 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli", "form.prefs.label.theme": "Tema", "form.prefs.label.timezone": "Fuso orario", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Ultimo uso", "page.api_keys.table.token": "Gettone", "page.api_keys.title": "Chiavi API", - "page.categories_count": [ - "%d category", - "%d categories" - ], "page.categories.entries": "Articoli", "page.categories.feed_count": [ "C'è %d feed.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Abbonamenti", "page.categories.no_feed": "Nessun feed.", "page.categories.title": "Categorie", + "page.categories_count": [ + "%d category", + "%d categories" + ], "page.category_label": "Category: %s", "page.edit_category.title": "Modifica categoria: %s", "page.edit_feed.etag_header": "Header ETag:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Registra la chiave di accesso", "page.settings.webauthn.register.error": "Impossibile registrare la passkey", + "page.shared_entries.title": "Voci condivise", "page.shared_entries_count": [ "%d shared entry", "%d shared entries" ], - "page.shared_entries.title": "Voci condivise", + "page.starred.title": "Preferiti", "page.starred_entry_count": [ "%d starred entry", "%d starred entries" ], - "page.starred.title": "Preferiti", "page.total_entry_count": [ "%d entry in total", "%d entries in total" ], + "page.unread.title": "Da leggere", "page.unread_entry_count": [ "%d unread entry", "%d unread entries" ], - "page.unread.title": "Da leggere", "page.users.actions": "Azioni", "page.users.admin.no": "No", "page.users.admin.yes": "Sì", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index 12527066..56b5c85c 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -346,6 +346,7 @@ "form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する", "form.prefs.label.theme": "テーマ", "form.prefs.label.timezone": "タイムゾーン", @@ -426,9 +427,6 @@ "page.api_keys.table.last_used_at": "最終使用", "page.api_keys.table.token": "トークン", "page.api_keys.title": "API キー", - "page.categories_count": [ - "%d category" - ], "page.categories.entries": "記事一覧", "page.categories.feed_count": [ "%d 件のフィードがあります。" @@ -436,6 +434,9 @@ "page.categories.feeds": "フィード一覧", "page.categories.no_feed": "フィードはありません。", "page.categories.title": "カテゴリ", + "page.categories_count": [ + "%d category" + ], "page.category_label": "Category: %s", "page.edit_category.title": "カテゴリを編集: %s", "page.edit_feed.etag_header": "ETag ヘッダー:", @@ -538,21 +539,21 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "パスキーを登録する", "page.settings.webauthn.register.error": "パスキーを登録できません", + "page.shared_entries.title": "共有エントリ", "page.shared_entries_count": [ "%d shared entry" ], - "page.shared_entries.title": "共有エントリ", + "page.starred.title": "星付き", "page.starred_entry_count": [ "%d starred entry" ], - "page.starred.title": "星付き", "page.total_entry_count": [ "%d entry in total" ], + "page.unread.title": "未読", "page.unread_entry_count": [ "%d unread entry" ], - "page.unread.title": "未読", "page.users.actions": "アクション", "page.users.admin.no": "非管理者", "page.users.admin.yes": "管理者", diff --git a/internal/locale/translations/nan_Latn_pehoeji.json b/internal/locale/translations/nan_Latn_pehoeji.json index a572cdd8..6b4c27e3 100644 --- a/internal/locale/translations/nan_Latn_pehoeji.json +++ b/internal/locale/translations/nan_Latn_pehoeji.json @@ -346,6 +346,7 @@ "form.prefs.label.mark_read_on_view": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè", "form.prefs.label.mark_read_on_view_or_media_completion": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè, m̄-koh nā-sī im-sìn, sī-sìn tio̍h tī hòng-sàng kàu 90%% ê si-chun chiah lâi chù", "form.prefs.label.media_playback_rate": "Im-sìn, sī-sìn pàng ê sok-tō͘", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Hián-sī siau-sit àn-sǹg ài gōa-kú lâi tha̍k", "form.prefs.label.theme": "Chú-tôe", "form.prefs.label.timezone": "Sî-khu", @@ -426,9 +427,6 @@ "page.api_keys.table.last_used_at": "Siōng-bóe pái sú-iōng", "page.api_keys.table.token": "Só-sî", "page.api_keys.title": "API só-sî", - "page.categories_count": [ - "%d ê lūi-pia̍t" - ], "page.categories.entries": "Siau-sit", "page.categories.feed_count": [ "Ū %d ê Siau-sit lâi-goân" @@ -436,6 +434,9 @@ "page.categories.feeds": "Siau-sit lâi-goân", "page.categories.no_feed": "Ah-bô siau-sit lâi-goân", "page.categories.title": "Lūi-pia̍t", + "page.categories_count": [ + "%d ê lūi-pia̍t" + ], "page.category_label": "Lūi-pia̍t: %s", "page.edit_category.title": "Pian-chi̍p lūi-pia̍t: %s", "page.edit_feed.etag_header": "ETag piau-thâu:", @@ -538,21 +539,21 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Chù-chheh Passkey", "page.settings.webauthn.register.error": "Bô-hoat-tō͘ chù-chheh Passkey", + "page.shared_entries.title": "Hun-hióng kè ê siau-sit", "page.shared_entries_count": [ "Í-keng hun-hióng %d ê siau-sit" ], - "page.shared_entries.title": "Hun-hióng kè ê siau-sit", + "page.starred.title": "Siu-chông", "page.starred_entry_count": [ "%d ê siu-chông ê siau-sit" ], - "page.starred.title": "Siu-chông", "page.total_entry_count": [ "Lóng-chóng %d ê siau-sit" ], + "page.unread.title": "Ah-bōe tha̍k", "page.unread_entry_count": [ "%d ê siau-sit ah-bōe tha̍k" ], - "page.unread.title": "Ah-bōe tha̍k", "page.users.actions": "chhau-chok", "page.users.admin.no": "Hóⁿ", "page.users.admin.yes": "Sī", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index ccebd388..ae236e1c 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Markeer artikelen automatisch als gelezen wanneer ze worden bekeken", "form.prefs.label.mark_read_on_view_or_media_completion": "Markeer artikelen als gelezen wanneer ze worden bekeken. Voor audio/video, markeer als gelezen bij 90%% voltooiing", "form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen", "form.prefs.label.theme": "Thema", "form.prefs.label.timezone": "Tijdzone", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Laatst gebruikt", "page.api_keys.table.token": "Token", "page.api_keys.title": "API-sleutels", - "page.categories_count": [ - "%d categorie", - "%d categorieën" - ], "page.categories.entries": "Artikelen", "page.categories.feed_count": [ "Er is %d feed.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Feeds", "page.categories.no_feed": "Geen feed.", "page.categories.title": "Categorieën", + "page.categories_count": [ + "%d categorie", + "%d categorieën" + ], "page.category_label": "Categorie: %s", "page.edit_category.title": "Bewerk categorie: %s", "page.edit_feed.etag_header": "ETAG header:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Passkey registreren", "page.settings.webauthn.register.error": "Kan passkey niet registreren", + "page.shared_entries.title": "Gedeelde artikelen", "page.shared_entries_count": [ "%d gedeeld artikel", "%d gedeelde artikelen" ], - "page.shared_entries.title": "Gedeelde artikelen", + "page.starred.title": "Favorieten", "page.starred_entry_count": [ "%d favoriet artikel", "%d favoriete artikelen" ], - "page.starred.title": "Favorieten", "page.total_entry_count": [ "%d artikel totaal", "%d artikelen totaal" ], + "page.unread.title": "Ongelezen", "page.unread_entry_count": [ "%d ongelezen artikel", "%d ongelezen artikelen" ], - "page.unread.title": "Ongelezen", "page.users.actions": "Acties", "page.users.admin.no": "Nee", "page.users.admin.yes": "Ja", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index 2d060a0c..485ef2e6 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -350,6 +350,7 @@ "form.prefs.label.mark_read_on_view": "Automatycznie oznacz wpisy jako przeczytane podczas przeglądania", "form.prefs.label.mark_read_on_view_or_media_completion": "Oznacz wpisy jako przeczytane po wyświetleniu. W przypadku audio i wideo oznacz jako przeczytane po ukończeniu 90%%", "form.prefs.label.media_playback_rate": "Szybkość odtwarzania audio i wideo", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania wpisów", "form.prefs.label.theme": "Wygląd", "form.prefs.label.timezone": "Strefa czasowa", @@ -430,11 +431,6 @@ "page.api_keys.table.last_used_at": "Ostatnio używane", "page.api_keys.table.token": "Token", "page.api_keys.title": "Klucze API", - "page.categories_count": [ - "%d kategoria", - "%d kategorie", - "%d kategorii" - ], "page.categories.entries": "Wpisy", "page.categories.feed_count": [ "Jest %d kanał.", @@ -444,6 +440,11 @@ "page.categories.feeds": "Kanały", "page.categories.no_feed": "Brak kanałów.", "page.categories.title": "Kategorie", + "page.categories_count": [ + "%d kategoria", + "%d kategorie", + "%d kategorii" + ], "page.category_label": "Kategoria: %s", "page.edit_category.title": "Edytuj kategorię: %s", "page.edit_feed.etag_header": "Nagłówek ETag:", @@ -552,29 +553,29 @@ "page.settings.webauthn.passkeys": "Klucze dostępu", "page.settings.webauthn.register": "Zarejestruj klucz dostępu", "page.settings.webauthn.register.error": "Nie można zarejestrować klucza dostępu", + "page.shared_entries.title": "Udostępnione wpisy", "page.shared_entries_count": [ "%d udostępniony wpis", "%d udostępnione wpisy", "%d udostępnionych wpisów" ], - "page.shared_entries.title": "Udostępnione wpisy", + "page.starred.title": "Ulubione", "page.starred_entry_count": [ "%d ulubiony wpis", "%d ulubione wpisy", "%d ulubionych wpisów" ], - "page.starred.title": "Ulubione", "page.total_entry_count": [ "%d wpis łącznie", "%d wpisy łącznie", "%d wpisów łącznie" ], + "page.unread.title": "Nieprzeczytane", "page.unread_entry_count": [ "%d nieprzeczytany wpis", "%d nieprzeczytane wpisy", "%d nieprzeczytanych wpisów" ], - "page.unread.title": "Nieprzeczytane", "page.users.actions": "Działania", "page.users.admin.no": "Nie", "page.users.admin.yes": "Tak", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index 762dae17..fb588649 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos", "form.prefs.label.theme": "Tema", "form.prefs.label.timezone": "Fuso horário", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Ultima utilização", "page.api_keys.table.token": "Token", "page.api_keys.title": "Chaves de API", - "page.categories_count": [ - "%d category", - "%d categories" - ], "page.categories.entries": "Itens", "page.categories.feed_count": [ "Existe %d fonte.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Inscrições", "page.categories.no_feed": "Sem fonte.", "page.categories.title": "Categorias", + "page.categories_count": [ + "%d category", + "%d categories" + ], "page.category_label": "Category: %s", "page.edit_category.title": "Editar categoria: %s", "page.edit_feed.etag_header": "Cabeçalho 'ETag':", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Registrar senha", "page.settings.webauthn.register.error": "Não foi possível registrar a senha", + "page.shared_entries.title": "Itens compartilhados", "page.shared_entries_count": [ "%d shared entry", "%d shared entries" ], - "page.shared_entries.title": "Itens compartilhados", + "page.starred.title": "Favoritos", "page.starred_entry_count": [ "%d starred entry", "%d starred entries" ], - "page.starred.title": "Favoritos", "page.total_entry_count": [ "%d entry in total", "%d entries in total" ], + "page.unread.title": "Não lidos", "page.unread_entry_count": [ "%d unread entry", "%d unread entries" ], - "page.unread.title": "Não lidos", "page.users.actions": "Ações", "page.users.admin.no": "Não", "page.users.admin.yes": "Sim", diff --git a/internal/locale/translations/ro_RO.json b/internal/locale/translations/ro_RO.json index cf53a60b..40d1b7b4 100644 --- a/internal/locale/translations/ro_RO.json +++ b/internal/locale/translations/ro_RO.json @@ -350,6 +350,7 @@ "form.prefs.label.mark_read_on_view": "Marchează intrările ca citite la vizualizare", "form.prefs.label.mark_read_on_view_or_media_completion": "Marchează intrările ca citite la vizualizare. Pentru audio/video, marchează ca citit la redarea a 90%% de conținut", "form.prefs.label.media_playback_rate": "Viteza de rulare audio/video", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Afișare timp estimat de citire pentru înregistrări", "form.prefs.label.theme": "Temă", "form.prefs.label.timezone": "Fus orar", @@ -430,11 +431,6 @@ "page.api_keys.table.last_used_at": "Utilizat ultima dată", "page.api_keys.table.token": "Token", "page.api_keys.title": "Chei API", - "page.categories_count": [ - "%d categorie", - "%d categorii", - "%d categorie găsită" - ], "page.categories.entries": "Intrări", "page.categories.feed_count": [ "Este %d flux.", @@ -444,6 +440,11 @@ "page.categories.feeds": "Fluxuri", "page.categories.no_feed": "Nici un flux.", "page.categories.title": "Categorii", + "page.categories_count": [ + "%d categorie", + "%d categorii", + "%d categorie găsită" + ], "page.category_label": "Categorie: %s", "page.edit_category.title": "Editare Categorie: %s", "page.edit_feed.etag_header": "Antet ETag:", @@ -552,29 +553,29 @@ "page.settings.webauthn.passkeys": "Chei Acces", "page.settings.webauthn.register": "Înregistrare cheie acces", "page.settings.webauthn.register.error": "Eroare la înregistrarea cheii de acces", + "page.shared_entries.title": "Înregistrări partajate", "page.shared_entries_count": [ "%d înregistrare partajată", "%d înregistrări partajate", "%d înregistrări partajate" ], - "page.shared_entries.title": "Înregistrări partajate", + "page.starred.title": "Marcate", "page.starred_entry_count": [ "%d înregistrare marcată", "%d Înregistrări marcate", "%d Înregistrări marcate" ], - "page.starred.title": "Marcate", "page.total_entry_count": [ "%d intrare în total", "%d intrări în total", "%d intrări în total" ], + "page.unread.title": "Necitite", "page.unread_entry_count": [ "%d înregistrare necitită", "%d înregistrări necitite", "%d înregistrări necitite" ], - "page.unread.title": "Necitite", "page.users.actions": "Acțiuni", "page.users.admin.no": "Nu", "page.users.admin.yes": "Da", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 8c0540d8..f69fdeb8 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -30,9 +30,9 @@ "alert.pocket_linked": "Ваш Pocket аккаунт теперь привязан!", "alert.prefs_saved": "Предпочтения сохранены!", "alert.too_many_feeds_refresh": [ - "Вы запустили слишком много обновлений подписок. Подождите %d минуту для нового запуска", - "Вы запустили слишком много обновлений подписок. Подождите %d минут для нового запуска", - "Вы запустили слишком много обновлений подписок. Подождите %d минут для нового запуска" + "Вы запустили слишком много обновлений подписок. Подождите %d минуту для нового запуска", + "Вы запустили слишком много обновлений подписок. Подождите %d минут для нового запуска", + "Вы запустили слишком много обновлений подписок. Подождите %d минут для нового запуска" ], "confirm.loading": "В процессе…", "confirm.no": "нет", @@ -350,6 +350,7 @@ "form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре", "form.prefs.label.mark_read_on_view_or_media_completion": "Отмечать статьи как прочитанные при просмотре. Для аудио/видео - при 90%% завершения воспроизведения", "form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Показать примерное время чтения статей", "form.prefs.label.theme": "Тема", "form.prefs.label.timezone": "Часовой пояс", @@ -430,11 +431,6 @@ "page.api_keys.table.last_used_at": "Последнее использование", "page.api_keys.table.token": "Токен", "page.api_keys.title": "API-ключи", - "page.categories_count": [ - "%d категория", - "%d категории", - "%d категорий" - ], "page.categories.entries": "Статьи", "page.categories.feed_count": [ "Есть %d подписка.", @@ -444,6 +440,11 @@ "page.categories.feeds": "Подписки", "page.categories.no_feed": "Нет подписок.", "page.categories.title": "Категории", + "page.categories_count": [ + "%d категория", + "%d категории", + "%d категорий" + ], "page.category_label": "Категории: %s", "page.edit_category.title": "Изменить категорию: %s", "page.edit_feed.etag_header": "Заголовок ETag:", @@ -552,29 +553,29 @@ "page.settings.webauthn.passkeys": "Ключи доступа", "page.settings.webauthn.register": "Зарегистрировать пароль", "page.settings.webauthn.register.error": "Не удается зарегистрировать пароль", + "page.shared_entries.title": "Общедоступные статьи", "page.shared_entries_count": [ "%d общедоступная статья", "%d общедоступных статьи", "%d общедоступных статей" ], - "page.shared_entries.title": "Общедоступные статьи", + "page.starred.title": "Избранное", "page.starred_entry_count": [ "%d избранная статья", "%d избранные статьи", "%d избранных статей" ], - "page.starred.title": "Избранное", "page.total_entry_count": [ "%d статья всего", "%d статьи всего", "%d статей всего" ], + "page.unread.title": "Непрочитанное", "page.unread_entry_count": [ "%d непрочитанная статья", "%d непрочитанных статьи", "%d непрочитанных статей" ], - "page.unread.title": "Непрочитанное", "page.users.actions": "Действия", "page.users.admin.no": "Нет", "page.users.admin.yes": "Да", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index c129016a..b3250e71 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -348,6 +348,7 @@ "form.prefs.label.mark_read_on_view": "Makaleler görüntülendiğinde otomatik olarak okundu olarak işaretle", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Ses/video oynatma hızı", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster", "form.prefs.label.theme": "Tema", "form.prefs.label.timezone": "Saat Dilimi", @@ -428,10 +429,6 @@ "page.api_keys.table.last_used_at": "Son Kullanılma", "page.api_keys.table.token": "Token", "page.api_keys.title": "API Anahtarları", - "page.categories_count": [ - "%d kategori", - "%d kategori" - ], "page.categories.entries": "Makaleler", "page.categories.feed_count": [ "%d besleme var.", @@ -440,6 +437,10 @@ "page.categories.feeds": "Beslemeler", "page.categories.no_feed": "Besleme yok.", "page.categories.title": "Kategoriler", + "page.categories_count": [ + "%d kategori", + "%d kategori" + ], "page.category_label": "Kategori: %s", "page.edit_category.title": "Kategoriyi Düzenle: %s", "page.edit_feed.etag_header": "ETag başlığı:", @@ -545,25 +546,25 @@ "page.settings.webauthn.passkeys": "Passkeyler", "page.settings.webauthn.register": "Passkey'i kaydet", "page.settings.webauthn.register.error": "Passkey kaydedilemiyor", + "page.shared_entries.title": "Paylaşılan makaleler", "page.shared_entries_count": [ "%d paylaşılan makaleler", "%d paylaşılan makaleler" ], - "page.shared_entries.title": "Paylaşılan makaleler", + "page.starred.title": "Yıldızlı", "page.starred_entry_count": [ "%d yıldızlanmış makale", "%d yıldızlanmış makale" ], - "page.starred.title": "Yıldızlı", "page.total_entry_count": [ "Toplamda %d makale", "Toplamda %d makale" ], + "page.unread.title": "Okunmadı", "page.unread_entry_count": [ "Toplamda %d okunmamış makale", "Toplamda %d okunmamış makale" ], - "page.unread.title": "Okunmadı", "page.users.actions": "Eylemler", "page.users.admin.no": "Hayır", "page.users.admin.yes": "Evet", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index 929adde4..b55fe438 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -350,6 +350,7 @@ "form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду", "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion", "form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів", "form.prefs.label.theme": "Тема", "form.prefs.label.timezone": "Часовий пояс", @@ -430,11 +431,6 @@ "page.api_keys.table.last_used_at": "Дата останнього використання", "page.api_keys.table.token": "Токен", "page.api_keys.title": "Ключі API", - "page.categories_count": [ - "%d category", - "%d categories", - "%d categories" - ], "page.categories.entries": "Статті", "page.categories.feed_count": [ "Містить %d стрічку.", @@ -444,6 +440,11 @@ "page.categories.feeds": "Підписки", "page.categories.no_feed": "Немає стрічки.", "page.categories.title": "Категорії", + "page.categories_count": [ + "%d category", + "%d categories", + "%d categories" + ], "page.category_label": "Категорія: %s", "page.edit_category.title": "Редагування категорії: %s", "page.edit_feed.etag_header": "Заголовок ETag:", @@ -552,29 +553,29 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "Зареєструвати пароль", "page.settings.webauthn.register.error": "Не вдалося зареєструвати ключ доступу", + "page.shared_entries.title": "Спільні записи", "page.shared_entries_count": [ "%d shared entry", "%d shared entries", "%d shared entries" ], - "page.shared_entries.title": "Спільні записи", + "page.starred.title": "З зірочкою", "page.starred_entry_count": [ "%d starred entry", "%d starred entries", "%d starred entries" ], - "page.starred.title": "З зірочкою", "page.total_entry_count": [ "%d entry in total", "%d entries in total", "%d entries in total" ], + "page.unread.title": "Непрочитане", "page.unread_entry_count": [ "%d unread entry", "%d unread entries", "%d unread entries" ], - "page.unread.title": "Непрочитане", "page.users.actions": "Дії", "page.users.admin.no": "Ні", "page.users.admin.yes": "Так", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 94915ea8..065f0732 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -346,6 +346,7 @@ "form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读", "form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读", "form.prefs.label.media_playback_rate": "音频/视频的播放速度", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "显示文章的预计阅读时间", "form.prefs.label.theme": "主题", "form.prefs.label.timezone": "时区", @@ -426,9 +427,6 @@ "page.api_keys.table.last_used_at": "最后使用", "page.api_keys.table.token": "令牌", "page.api_keys.title": "API 密钥", - "page.categories_count": [ - "%d 分类" - ], "page.categories.entries": "查看内容", "page.categories.feed_count": [ "有 %d 个源" @@ -436,6 +434,9 @@ "page.categories.feeds": "查看源", "page.categories.no_feed": "没有源", "page.categories.title": "分类", + "page.categories_count": [ + "%d 分类" + ], "page.category_label": "分类: %s", "page.edit_category.title": "编辑分类 : %s", "page.edit_feed.etag_header": "ETag 标题:", @@ -538,21 +539,21 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "注册 Passkey", "page.settings.webauthn.register.error": "无法注册 Passkey", + "page.shared_entries.title": "已分享的文章", "page.shared_entries_count": [ "%d 已分享的文章" ], - "page.shared_entries.title": "已分享的文章", + "page.starred.title": "收藏", "page.starred_entry_count": [ "%d 收藏的文章" ], - "page.starred.title": "收藏", "page.total_entry_count": [ "%d 文章总数" ], + "page.unread.title": "未读", "page.unread_entry_count": [ "%d 未读的文章" ], - "page.unread.title": "未读", "page.users.actions": "操作", "page.users.admin.no": "否", "page.users.admin.yes": "是", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index 3ce05a10..5f7ea975 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -346,6 +346,7 @@ "form.prefs.label.mark_read_on_view": "檢視時自動將文章標記為已讀", "form.prefs.label.mark_read_on_view_or_media_completion": "檢視文章即標記為已讀;若是音訊/視訊則在 90% 播放完成時標記", "form.prefs.label.media_playback_rate": "音訊/視訊播放速度", + "form.prefs.label.always_open_external_links": "Read articles by opening external links", "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間", "form.prefs.label.theme": "主題", "form.prefs.label.timezone": "時區", @@ -426,9 +427,6 @@ "page.api_keys.table.last_used_at": "最後使用", "page.api_keys.table.token": "金鑰", "page.api_keys.title": "API 金鑰", - "page.categories_count": [ - "%d 個分類" - ], "page.categories.entries": "檢視內容", "page.categories.feed_count": [ "有 %d 個 Feed" @@ -436,6 +434,9 @@ "page.categories.feeds": "檢視 Feeds", "page.categories.no_feed": "沒有 Feed", "page.categories.title": "分類", + "page.categories_count": [ + "%d 個分類" + ], "page.category_label": "分類:%s", "page.edit_category.title": "編輯分類 : %s", "page.edit_feed.etag_header": "ETag 標頭:", @@ -538,21 +539,21 @@ "page.settings.webauthn.passkeys": "Passkeys", "page.settings.webauthn.register": "註冊 Passkey", "page.settings.webauthn.register.error": "無法註冊 Passkey", + "page.shared_entries.title": "已分享的文章", "page.shared_entries_count": [ "已分享 %d 篇文章" ], - "page.shared_entries.title": "已分享的文章", + "page.starred.title": "收藏", "page.starred_entry_count": [ "%d 篇收藏文章" ], - "page.starred.title": "收藏", "page.total_entry_count": [ "總共 %d 篇文章" ], + "page.unread.title": "未讀", "page.unread_entry_count": [ "%d 篇未讀文章" ], - "page.unread.title": "未讀", "page.users.actions": "操作", "page.users.admin.no": "否", "page.users.admin.yes": "是", diff --git a/internal/model/user.go b/internal/model/user.go index ad070904..5c1fa911 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -41,6 +41,7 @@ type User struct { MediaPlaybackRate float64 `json:"media_playback_rate"` BlockFilterEntryRules string `json:"block_filter_entry_rules"` KeepFilterEntryRules string `json:"keep_filter_entry_rules"` + AlwaysOpenExternalLinks bool `json:"always_open_external_links"` } // UserCreationRequest represents the request to create a user. @@ -82,6 +83,7 @@ type UserModificationRequest struct { MediaPlaybackRate *float64 `json:"media_playback_rate"` BlockFilterEntryRules *string `json:"block_filter_entry_rules"` KeepFilterEntryRules *string `json:"keep_filter_entry_rules"` + AlwaysOpenExternalLinks *bool `json:"always_open_external_links"` } // Patch updates the User object with the modification request. @@ -197,6 +199,10 @@ func (u *UserModificationRequest) Patch(user *User) { if u.KeepFilterEntryRules != nil { user.KeepFilterEntryRules = *u.KeepFilterEntryRules } + + if u.AlwaysOpenExternalLinks != nil { + user.AlwaysOpenExternalLinks = *u.AlwaysOpenExternalLinks + } } // UseTimezone converts last login date to the given timezone. diff --git a/internal/storage/user.go b/internal/storage/user.go index 64b6d503..c1c3f00d 100644 --- a/internal/storage/user.go +++ b/internal/storage/user.go @@ -96,7 +96,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m mark_read_on_view, media_playback_rate, block_filter_entry_rules, - keep_filter_entry_rules + keep_filter_entry_rules, + always_open_external_links ` tx, err := s.db.Begin() @@ -140,6 +141,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m &user.MediaPlaybackRate, &user.BlockFilterEntryRules, &user.KeepFilterEntryRules, + &user.AlwaysOpenExternalLinks, ) if err != nil { tx.Rollback() @@ -204,9 +206,10 @@ func (s *Storage) UpdateUser(user *model.User) error { mark_read_on_media_player_completion=$25, media_playback_rate=$26, block_filter_entry_rules=$27, - keep_filter_entry_rules=$28 + keep_filter_entry_rules=$28, + always_open_external_links=$29 WHERE - id=$29 + id=$30 ` _, err = s.db.Exec( @@ -239,6 +242,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.MediaPlaybackRate, user.BlockFilterEntryRules, user.KeepFilterEntryRules, + user.AlwaysOpenExternalLinks, user.ID, ) if err != nil { @@ -273,9 +277,10 @@ func (s *Storage) UpdateUser(user *model.User) error { mark_read_on_media_player_completion=$24, media_playback_rate=$25, block_filter_entry_rules=$26, - keep_filter_entry_rules=$27 + keep_filter_entry_rules=$27, + always_open_external_links=$28 WHERE - id=$28 + id=$29 ` _, err := s.db.Exec( @@ -307,6 +312,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.MediaPlaybackRate, user.BlockFilterEntryRules, user.KeepFilterEntryRules, + user.AlwaysOpenExternalLinks, user.ID, ) @@ -360,7 +366,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) { mark_read_on_media_player_completion, media_playback_rate, block_filter_entry_rules, - keep_filter_entry_rules + keep_filter_entry_rules, + always_open_external_links FROM users WHERE @@ -401,7 +408,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) { mark_read_on_media_player_completion, media_playback_rate, block_filter_entry_rules, - keep_filter_entry_rules + keep_filter_entry_rules, + always_open_external_links FROM users WHERE @@ -442,7 +450,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) { mark_read_on_media_player_completion, media_playback_rate, block_filter_entry_rules, - keep_filter_entry_rules + keep_filter_entry_rules, + always_open_external_links FROM users WHERE @@ -490,7 +499,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) { u.mark_read_on_media_player_completion, media_playback_rate, u.block_filter_entry_rules, - u.keep_filter_entry_rules + u.keep_filter_entry_rules, + u.always_open_external_links, FROM users u LEFT JOIN @@ -533,6 +543,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err &user.MediaPlaybackRate, &user.BlockFilterEntryRules, &user.KeepFilterEntryRules, + &user.AlwaysOpenExternalLinks, ) if err == sql.ErrNoRows { @@ -646,7 +657,8 @@ func (s *Storage) Users() (model.Users, error) { mark_read_on_media_player_completion, media_playback_rate, block_filter_entry_rules, - keep_filter_entry_rules + keep_filter_entry_rules, + always_open_external_links FROM users ORDER BY username ASC @@ -690,6 +702,7 @@ func (s *Storage) Users() (model.Users, error) { &user.MediaPlaybackRate, &user.BlockFilterEntryRules, &user.KeepFilterEntryRules, + &user.AlwaysOpenExternalLinks, ) if err != nil { diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html index 144014b3..8cb8dc03 100644 --- a/internal/template/templates/views/settings.html +++ b/internal/template/templates/views/settings.html @@ -207,6 +207,8 @@ + + diff --git a/internal/ui/entry_bookmark.go b/internal/ui/entry_bookmark.go index 7b9aeab5..616dc73f 100644 --- a/internal/ui/entry_bookmark.go +++ b/internal/ui/entry_bookmark.go @@ -48,6 +48,11 @@ func (h *handler) showStarredEntryPage(w http.ResponseWriter, r *http.Request) { entry.Status = model.EntryStatusRead } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection) entryPaginationBuilder.WithStarred() prevEntry, nextEntry, err := entryPaginationBuilder.Entries() diff --git a/internal/ui/entry_category.go b/internal/ui/entry_category.go index 057e137d..a135cc79 100644 --- a/internal/ui/entry_category.go +++ b/internal/ui/entry_category.go @@ -51,6 +51,11 @@ func (h *handler) showCategoryEntryPage(w http.ResponseWriter, r *http.Request) entry.Status = model.EntryStatusRead } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection) entryPaginationBuilder.WithCategoryID(categoryID) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() diff --git a/internal/ui/entry_feed.go b/internal/ui/entry_feed.go index b16a777c..a41d004a 100644 --- a/internal/ui/entry_feed.go +++ b/internal/ui/entry_feed.go @@ -51,6 +51,11 @@ func (h *handler) showFeedEntryPage(w http.ResponseWriter, r *http.Request) { entry.Status = model.EntryStatusRead } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection) entryPaginationBuilder.WithFeedID(feedID) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() diff --git a/internal/ui/entry_read.go b/internal/ui/entry_read.go index 48acac02..65f02ebd 100644 --- a/internal/ui/entry_read.go +++ b/internal/ui/entry_read.go @@ -38,6 +38,11 @@ func (h *handler) showReadEntryPage(w http.ResponseWriter, r *http.Request) { return } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, "changed_at", "desc") entryPaginationBuilder.WithStatus(model.EntryStatusRead) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() diff --git a/internal/ui/entry_search.go b/internal/ui/entry_search.go index c1354e66..85288206 100644 --- a/internal/ui/entry_search.go +++ b/internal/ui/entry_search.go @@ -50,6 +50,11 @@ func (h *handler) showSearchEntryPage(w http.ResponseWriter, r *http.Request) { entry.Status = model.EntryStatusRead } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection) entryPaginationBuilder.WithSearchQuery(searchQuery) prevEntry, nextEntry, err := entryPaginationBuilder.Entries() diff --git a/internal/ui/entry_unread.go b/internal/ui/entry_unread.go index 07d9007c..5cf35604 100644 --- a/internal/ui/entry_unread.go +++ b/internal/ui/entry_unread.go @@ -79,6 +79,11 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) { } } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + sess := session.New(h.store, request.SessionID(r)) view := view.New(h.tpl, r, sess) view.Set("entry", entry) diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go index 34f3a281..bd94560f 100644 --- a/internal/ui/form/settings.go +++ b/internal/ui/form/settings.go @@ -48,10 +48,11 @@ type SettingsForm struct { CategoriesSortingOrder string MarkReadOnView bool // MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together - MarkReadBehavior MarkReadBehavior - MediaPlaybackRate float64 - BlockFilterEntryRules string - KeepFilterEntryRules string + MarkReadBehavior MarkReadBehavior + MediaPlaybackRate float64 + BlockFilterEntryRules string + KeepFilterEntryRules string + AlwaysOpenExternalLinks bool } // MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values. @@ -114,6 +115,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User { user.MediaPlaybackRate = s.MediaPlaybackRate user.BlockFilterEntryRules = s.BlockFilterEntryRules user.KeepFilterEntryRules = s.KeepFilterEntryRules + user.AlwaysOpenExternalLinks = s.AlwaysOpenExternalLinks MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior) user.MarkReadOnView = MarkReadOnView @@ -179,31 +181,32 @@ func NewSettingsForm(r *http.Request) *SettingsForm { mediaPlaybackRate = 1 } return &SettingsForm{ - Username: r.FormValue("username"), - Password: r.FormValue("password"), - Confirmation: r.FormValue("confirmation"), - Theme: r.FormValue("theme"), - Language: r.FormValue("language"), - Timezone: r.FormValue("timezone"), - EntryDirection: r.FormValue("entry_direction"), - EntryOrder: r.FormValue("entry_order"), - EntriesPerPage: int(entriesPerPage), - KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1", - ShowReadingTime: r.FormValue("show_reading_time") == "1", - CustomCSS: r.FormValue("custom_css"), - CustomJS: r.FormValue("custom_js"), - ExternalFontHosts: r.FormValue("external_font_hosts"), - EntrySwipe: r.FormValue("entry_swipe") == "1", - GestureNav: r.FormValue("gesture_nav"), - DisplayMode: r.FormValue("display_mode"), - DefaultReadingSpeed: int(defaultReadingSpeed), - CJKReadingSpeed: int(cjkReadingSpeed), - DefaultHomePage: r.FormValue("default_home_page"), - CategoriesSortingOrder: r.FormValue("categories_sorting_order"), - MarkReadOnView: r.FormValue("mark_read_on_view") == "1", - MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")), - MediaPlaybackRate: mediaPlaybackRate, - BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"), - KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"), + Username: r.FormValue("username"), + Password: r.FormValue("password"), + Confirmation: r.FormValue("confirmation"), + Theme: r.FormValue("theme"), + Language: r.FormValue("language"), + Timezone: r.FormValue("timezone"), + EntryDirection: r.FormValue("entry_direction"), + EntryOrder: r.FormValue("entry_order"), + EntriesPerPage: int(entriesPerPage), + KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1", + ShowReadingTime: r.FormValue("show_reading_time") == "1", + CustomCSS: r.FormValue("custom_css"), + CustomJS: r.FormValue("custom_js"), + ExternalFontHosts: r.FormValue("external_font_hosts"), + EntrySwipe: r.FormValue("entry_swipe") == "1", + GestureNav: r.FormValue("gesture_nav"), + DisplayMode: r.FormValue("display_mode"), + DefaultReadingSpeed: int(defaultReadingSpeed), + CJKReadingSpeed: int(cjkReadingSpeed), + DefaultHomePage: r.FormValue("default_home_page"), + CategoriesSortingOrder: r.FormValue("categories_sorting_order"), + MarkReadOnView: r.FormValue("mark_read_on_view") == "1", + MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")), + MediaPlaybackRate: mediaPlaybackRate, + BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"), + KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"), + AlwaysOpenExternalLinks: r.FormValue("always_open_external_links") == "1", } } diff --git a/internal/ui/form/settings_test.go b/internal/ui/form/settings_test.go index 84bbd9b7..f6b978e5 100644 --- a/internal/ui/form/settings_test.go +++ b/internal/ui/form/settings_test.go @@ -9,20 +9,21 @@ import ( func TestValid(t *testing.T) { settings := &SettingsForm{ - Username: "user", - Password: "hunter2", - Confirmation: "hunter2", - Theme: "default", - Language: "en_US", - Timezone: "UTC", - EntryDirection: "asc", - EntriesPerPage: 50, - DisplayMode: "standalone", - GestureNav: "tap", - DefaultReadingSpeed: 35, - CJKReadingSpeed: 25, - DefaultHomePage: "unread", - MediaPlaybackRate: 1.25, + Username: "user", + Password: "hunter2", + Confirmation: "hunter2", + Theme: "default", + Language: "en_US", + Timezone: "UTC", + EntryDirection: "asc", + EntriesPerPage: 50, + DisplayMode: "standalone", + GestureNav: "tap", + DefaultReadingSpeed: 35, + CJKReadingSpeed: 25, + DefaultHomePage: "unread", + MediaPlaybackRate: 1.25, + AlwaysOpenExternalLinks: true, } err := settings.Validate() @@ -33,20 +34,21 @@ func TestValid(t *testing.T) { func TestConfirmationEmpty(t *testing.T) { settings := &SettingsForm{ - Username: "user", - Password: "hunter2", - Confirmation: "", - Theme: "default", - Language: "en_US", - Timezone: "UTC", - EntryDirection: "asc", - EntriesPerPage: 50, - DisplayMode: "standalone", - GestureNav: "tap", - DefaultReadingSpeed: 35, - CJKReadingSpeed: 25, - DefaultHomePage: "unread", - MediaPlaybackRate: 1.25, + Username: "user", + Password: "hunter2", + Confirmation: "", + Theme: "default", + Language: "en_US", + Timezone: "UTC", + EntryDirection: "asc", + EntriesPerPage: 50, + DisplayMode: "standalone", + GestureNav: "tap", + DefaultReadingSpeed: 35, + CJKReadingSpeed: 25, + DefaultHomePage: "unread", + MediaPlaybackRate: 1.25, + AlwaysOpenExternalLinks: true, } err := settings.Validate() @@ -61,20 +63,21 @@ func TestConfirmationEmpty(t *testing.T) { func TestConfirmationIncorrect(t *testing.T) { settings := &SettingsForm{ - Username: "user", - Password: "hunter2", - Confirmation: "unter2", - Theme: "default", - Language: "en_US", - Timezone: "UTC", - EntryDirection: "asc", - EntriesPerPage: 50, - DisplayMode: "standalone", - GestureNav: "tap", - DefaultReadingSpeed: 35, - CJKReadingSpeed: 25, - DefaultHomePage: "unread", - MediaPlaybackRate: 1.25, + Username: "user", + Password: "hunter2", + Confirmation: "unter2", + Theme: "default", + Language: "en_US", + Timezone: "UTC", + EntryDirection: "asc", + EntriesPerPage: 50, + DisplayMode: "standalone", + GestureNav: "tap", + DefaultReadingSpeed: 35, + CJKReadingSpeed: 25, + DefaultHomePage: "unread", + MediaPlaybackRate: 1.25, + AlwaysOpenExternalLinks: true, } err := settings.Validate() diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go index 6f2b92af..60448a28 100644 --- a/internal/ui/settings_show.go +++ b/internal/ui/settings_show.go @@ -23,29 +23,30 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { } settingsForm := form.SettingsForm{ - Username: user.Username, - Theme: user.Theme, - Language: user.Language, - Timezone: user.Timezone, - EntryDirection: user.EntryDirection, - EntryOrder: user.EntryOrder, - EntriesPerPage: user.EntriesPerPage, - KeyboardShortcuts: user.KeyboardShortcuts, - ShowReadingTime: user.ShowReadingTime, - CustomCSS: user.Stylesheet, - CustomJS: user.CustomJS, - ExternalFontHosts: user.ExternalFontHosts, - EntrySwipe: user.EntrySwipe, - GestureNav: user.GestureNav, - DisplayMode: user.DisplayMode, - DefaultReadingSpeed: user.DefaultReadingSpeed, - CJKReadingSpeed: user.CJKReadingSpeed, - DefaultHomePage: user.DefaultHomePage, - CategoriesSortingOrder: user.CategoriesSortingOrder, - MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion), - MediaPlaybackRate: user.MediaPlaybackRate, - BlockFilterEntryRules: user.BlockFilterEntryRules, - KeepFilterEntryRules: user.KeepFilterEntryRules, + Username: user.Username, + Theme: user.Theme, + Language: user.Language, + Timezone: user.Timezone, + EntryDirection: user.EntryDirection, + EntryOrder: user.EntryOrder, + EntriesPerPage: user.EntriesPerPage, + KeyboardShortcuts: user.KeyboardShortcuts, + ShowReadingTime: user.ShowReadingTime, + CustomCSS: user.Stylesheet, + CustomJS: user.CustomJS, + ExternalFontHosts: user.ExternalFontHosts, + EntrySwipe: user.EntrySwipe, + GestureNav: user.GestureNav, + DisplayMode: user.DisplayMode, + DefaultReadingSpeed: user.DefaultReadingSpeed, + CJKReadingSpeed: user.CJKReadingSpeed, + DefaultHomePage: user.DefaultHomePage, + CategoriesSortingOrder: user.CategoriesSortingOrder, + MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion), + MediaPlaybackRate: user.MediaPlaybackRate, + BlockFilterEntryRules: user.BlockFilterEntryRules, + KeepFilterEntryRules: user.KeepFilterEntryRules, + AlwaysOpenExternalLinks: user.AlwaysOpenExternalLinks, } timezones, err := h.store.Timezones() diff --git a/internal/ui/unread_entry_category.go b/internal/ui/unread_entry_category.go index 5e80f4b4..31f91748 100644 --- a/internal/ui/unread_entry_category.go +++ b/internal/ui/unread_entry_category.go @@ -88,6 +88,11 @@ func (h *handler) showUnreadCategoryEntryPage(w http.ResponseWriter, r *http.Req } } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + sess := session.New(h.store, request.SessionID(r)) view := view.New(h.tpl, r, sess) view.Set("entry", entry) diff --git a/internal/ui/unread_entry_feed.go b/internal/ui/unread_entry_feed.go index e8a1c8cd..3867a709 100644 --- a/internal/ui/unread_entry_feed.go +++ b/internal/ui/unread_entry_feed.go @@ -88,6 +88,11 @@ func (h *handler) showUnreadFeedEntryPage(w http.ResponseWriter, r *http.Request } } + if user.AlwaysOpenExternalLinks { + html.Redirect(w, r, entry.URL) + return + } + sess := session.New(h.store, request.SessionID(r)) view := view.New(h.tpl, r, sess) view.Set("entry", entry)