Browse Source

feat: add option to always open articles externally

Kelly Norton 10 months ago
parent
commit
09fb05aaaf
36 changed files with 397 additions and 303 deletions
  1. 58 56
      client/model.go
  2. 4 0
      internal/database/migrations.go
  3. 8 7
      internal/locale/translations/de_DE.json
  4. 8 7
      internal/locale/translations/el_EL.json
  5. 8 7
      internal/locale/translations/en_US.json
  6. 8 7
      internal/locale/translations/es_ES.json
  7. 8 7
      internal/locale/translations/fi_FI.json
  8. 8 7
      internal/locale/translations/fr_FR.json
  9. 8 7
      internal/locale/translations/hi_IN.json
  10. 7 6
      internal/locale/translations/id_ID.json
  11. 8 7
      internal/locale/translations/it_IT.json
  12. 7 6
      internal/locale/translations/ja_JP.json
  13. 7 6
      internal/locale/translations/nan_Latn_pehoeji.json
  14. 8 7
      internal/locale/translations/nl_NL.json
  15. 9 8
      internal/locale/translations/pl_PL.json
  16. 8 7
      internal/locale/translations/pt_BR.json
  17. 9 8
      internal/locale/translations/ro_RO.json
  18. 12 11
      internal/locale/translations/ru_RU.json
  19. 8 7
      internal/locale/translations/tr_TR.json
  20. 9 8
      internal/locale/translations/uk_UA.json
  21. 7 6
      internal/locale/translations/zh_CN.json
  22. 7 6
      internal/locale/translations/zh_TW.json
  23. 6 0
      internal/model/user.go
  24. 23 10
      internal/storage/user.go
  25. 2 0
      internal/template/templates/views/settings.html
  26. 5 0
      internal/ui/entry_bookmark.go
  27. 5 0
      internal/ui/entry_category.go
  28. 5 0
      internal/ui/entry_feed.go
  29. 5 0
      internal/ui/entry_read.go
  30. 5 0
      internal/ui/entry_search.go
  31. 5 0
      internal/ui/entry_unread.go
  32. 33 30
      internal/ui/form/settings.go
  33. 45 42
      internal/ui/form/settings_test.go
  34. 24 23
      internal/ui/settings_show.go
  35. 5 0
      internal/ui/unread_entry_category.go
  36. 5 0
      internal/ui/unread_entry_feed.go

+ 58 - 56
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.

+ 4 - 0
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
+	},
 }

+ 8 - 7
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",

+ 8 - 7
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": "Ναι.",

+ 8 - 7
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",

+ 8 - 7
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í",

+ 8 - 7
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ä",

+ 8 - 7
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",

+ 8 - 7
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": "हां",

+ 7 - 6
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",

+ 8 - 7
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ì",

+ 7 - 6
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": "管理者",

+ 7 - 6
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ī",

+ 8 - 7
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",

+ 9 - 8
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",

+ 8 - 7
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",

+ 9 - 8
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",

+ 12 - 11
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": "Да",

+ 8 - 7
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",

+ 9 - 8
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": "Так",

+ 7 - 6
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": "是",

+ 7 - 6
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": "是",

+ 6 - 0
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.

+ 23 - 10
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 {

+ 2 - 0
internal/template/templates/views/settings.html

@@ -207,6 +207,8 @@
 
         <label><input type="checkbox" name="entry_swipe" value="1" {{ if .form.EntrySwipe }}checked{{ end }}> {{ t "form.prefs.label.entry_swipe" }}</label>
 
+        <label><input type="checkbox" name="always_open_external_links" value="1" {{ if .form.AlwaysOpenExternalLinks }}checked{{ end }}> {{ t "form.prefs.label.always_open_external_links" }}</label>
+
         <label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label>
         <textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
 

+ 5 - 0
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()

+ 5 - 0
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()

+ 5 - 0
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()

+ 5 - 0
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()

+ 5 - 0
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()

+ 5 - 0
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)

+ 33 - 30
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",
 	}
 }

+ 45 - 42
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()

+ 24 - 23
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()

+ 5 - 0
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)

+ 5 - 0
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)