mirror of
https://github.com/miniflux/v2.git
synced 2025-08-01 17:38:37 +00:00
feat: mark media as read when playback reaches 90%
This commit is contained in:
parent
37309adbc0
commit
4f55361f5f
37 changed files with 278 additions and 76 deletions
|
@ -937,4 +937,9 @@ var migrations = []func(tx *sql.Tx) error{
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
sql := `ALTER TABLE users ADD COLUMN mark_read_on_media_player_completion bool default 'f';`
|
||||||
|
_, err = tx.Exec(sql)
|
||||||
|
return err
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Standard-Startseite",
|
"form.prefs.label.default_home_page": "Standard-Startseite",
|
||||||
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
|
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
|
||||||
"form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
|
"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": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
|
"form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
|
"form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
|
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
|
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
|
||||||
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
|
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
|
||||||
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Default home page",
|
"form.prefs.label.default_home_page": "Default home page",
|
||||||
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
||||||
"form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Página de inicio por defecto",
|
"form.prefs.label.default_home_page": "Página de inicio por defecto",
|
||||||
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
|
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
|
||||||
"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": "Marcar automáticamente las entradas como leídas cuando se vean",
|
||||||
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
|
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
|
||||||
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
||||||
"form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Page d'accueil par défaut",
|
"form.prefs.label.default_home_page": "Page d'accueil par défaut",
|
||||||
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
|
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
|
||||||
"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": "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.mark_read_on_media_completion": "Marqué les entrées comme lues uniquement après 90%% de lecture de l'audio/vidéo",
|
||||||
|
"form.prefs.label.mark_read_manually": "Marqué les entrées comme lues manuellement",
|
||||||
"form.prefs.fieldset.application_settings": "Paramètres de l'application",
|
"form.prefs.fieldset.application_settings": "Paramètres de l'application",
|
||||||
"form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
|
"form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
|
||||||
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
|
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
|
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
|
||||||
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
||||||
"form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -384,6 +384,9 @@
|
||||||
"form.prefs.label.default_home_page": "Beranda Baku",
|
"form.prefs.label.default_home_page": "Beranda Baku",
|
||||||
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
|
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
|
||||||
"form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
|
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
|
||||||
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
||||||
"form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -384,6 +384,9 @@
|
||||||
"form.prefs.label.default_home_page": "デフォルトのトップページ",
|
"form.prefs.label.default_home_page": "デフォルトのトップページ",
|
||||||
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
||||||
"form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Standaard startpagina",
|
"form.prefs.label.default_home_page": "Standaard startpagina",
|
||||||
"form.prefs.label.categories_sorting_order": "Categorieën sorteren",
|
"form.prefs.label.categories_sorting_order": "Categorieën sorteren",
|
||||||
"form.prefs.label.mark_read_on_view": "Items automatisch markeren als gelezen wanneer ze worden bekeken",
|
"form.prefs.label.mark_read_on_view": "Items automatisch markeren als gelezen wanneer ze worden bekeken",
|
||||||
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -404,6 +404,9 @@
|
||||||
"form.prefs.label.default_home_page": "Domyślna strona główna",
|
"form.prefs.label.default_home_page": "Domyślna strona główna",
|
||||||
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
|
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
|
||||||
"form.prefs.label.mark_read_on_view": "Automatycznie oznaczaj wpisy jako przeczytane podczas przeglądania",
|
"form.prefs.label.mark_read_on_view": "Automatycznie oznaczaj wpisy jako przeczytane podczas przeglądania",
|
||||||
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -394,6 +394,9 @@
|
||||||
"form.prefs.label.default_home_page": "Página inicial predefinida",
|
"form.prefs.label.default_home_page": "Página inicial predefinida",
|
||||||
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
|
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
|
||||||
"form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -404,6 +404,9 @@
|
||||||
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
|
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
|
||||||
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
|
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
|
||||||
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -304,6 +304,9 @@
|
||||||
"form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
|
"form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
|
||||||
"form.prefs.label.language": "Dil",
|
"form.prefs.label.language": "Dil",
|
||||||
"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": "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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.label.media_playback_rate": "Ses/video oynatma hızı",
|
"form.prefs.label.media_playback_rate": "Ses/video oynatma hızı",
|
||||||
"form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
|
"form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
|
|
|
@ -404,6 +404,9 @@
|
||||||
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
|
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
|
||||||
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
||||||
"form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
|
|
|
@ -384,6 +384,9 @@
|
||||||
"form.prefs.label.default_home_page": "默认主页",
|
"form.prefs.label.default_home_page": "默认主页",
|
||||||
"form.prefs.label.categories_sorting_order": "分类排序",
|
"form.prefs.label.categories_sorting_order": "分类排序",
|
||||||
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "应用设置",
|
"form.prefs.fieldset.application_settings": "应用设置",
|
||||||
"form.prefs.fieldset.authentication_settings": "用户认证设置",
|
"form.prefs.fieldset.authentication_settings": "用户认证设置",
|
||||||
"form.prefs.fieldset.reader_settings": "阅读器设置",
|
"form.prefs.fieldset.reader_settings": "阅读器设置",
|
||||||
|
|
|
@ -384,6 +384,9 @@
|
||||||
"form.prefs.label.default_home_page": "預設主頁",
|
"form.prefs.label.default_home_page": "預設主頁",
|
||||||
"form.prefs.label.categories_sorting_order": "分類排序",
|
"form.prefs.label.categories_sorting_order": "分類排序",
|
||||||
"form.prefs.label.mark_read_on_view": "查看時自動將條目標記為已讀",
|
"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.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||||
|
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||||
"form.prefs.fieldset.application_settings": "應用程式設定",
|
"form.prefs.fieldset.application_settings": "應用程式設定",
|
||||||
"form.prefs.fieldset.authentication_settings": "使用者認證設定",
|
"form.prefs.fieldset.authentication_settings": "使用者認證設定",
|
||||||
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package model // import "miniflux.app/v2/internal/model"
|
package model // import "miniflux.app/v2/internal/model"
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// Enclosure represents an attachment.
|
// Enclosure represents an attachment.
|
||||||
type Enclosure struct {
|
type Enclosure struct {
|
||||||
|
@ -24,3 +25,12 @@ func (e Enclosure) Html5MimeType() string {
|
||||||
|
|
||||||
// EnclosureList represents a list of attachments.
|
// EnclosureList represents a list of attachments.
|
||||||
type EnclosureList []*Enclosure
|
type EnclosureList []*Enclosure
|
||||||
|
|
||||||
|
func (el EnclosureList) ContainsAudioOrVideo() bool {
|
||||||
|
for _, enclosure := range el {
|
||||||
|
if strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -50,6 +50,22 @@ func NewEntry() *Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldMarkAsReadOnView Return whether the entry should be marked as viewed considering all user settings and entry state.
|
||||||
|
func (e *Entry) ShouldMarkAsReadOnView(user *User) bool {
|
||||||
|
// Already read, no need to mark as read again. Removed entries are not marked as read
|
||||||
|
if e.Status != EntryStatusUnread {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is an enclosure, markAsRead will happen at enclosure completion time, no need to mark as read on view
|
||||||
|
if user.MarkReadOnMediaPlayerCompletion && e.Enclosures.ContainsAudioOrVideo() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user wants to mark as read on view
|
||||||
|
return user.MarkReadOnView
|
||||||
|
}
|
||||||
|
|
||||||
// Entries represents a list of entries.
|
// Entries represents a list of entries.
|
||||||
type Entries []*Entry
|
type Entries []*Entry
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ type User struct {
|
||||||
DefaultHomePage string `json:"default_home_page"`
|
DefaultHomePage string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||||
|
MarkReadOnMediaPlayerCompletion bool `json:"mark_read_on_media_player_completion"`
|
||||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
||||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||||
|
@ -73,6 +74,7 @@ type UserModificationRequest struct {
|
||||||
DefaultHomePage *string `json:"default_home_page"`
|
DefaultHomePage *string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||||
|
MarkReadOnMediaPlayerCompletion *bool `json:"mark_read_on_media_player_completion"`
|
||||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
||||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||||
|
@ -168,6 +170,10 @@ func (u *UserModificationRequest) Patch(user *User) {
|
||||||
user.MarkReadOnView = *u.MarkReadOnView
|
user.MarkReadOnView = *u.MarkReadOnView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.MarkReadOnMediaPlayerCompletion != nil {
|
||||||
|
user.MarkReadOnMediaPlayerCompletion = *u.MarkReadOnMediaPlayerCompletion
|
||||||
|
}
|
||||||
|
|
||||||
if u.MediaPlaybackRate != nil {
|
if u.MediaPlaybackRate != nil {
|
||||||
user.MediaPlaybackRate = *u.MediaPlaybackRate
|
user.MediaPlaybackRate = *u.MediaPlaybackRate
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,11 +193,12 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
default_home_page=$20,
|
default_home_page=$20,
|
||||||
categories_sorting_order=$21,
|
categories_sorting_order=$21,
|
||||||
mark_read_on_view=$22,
|
mark_read_on_view=$22,
|
||||||
media_playback_rate=$23,
|
mark_read_on_media_player_completion=$23,
|
||||||
block_filter_entry_rules=$24,
|
media_playback_rate=$24,
|
||||||
keep_filter_entry_rules=$25
|
block_filter_entry_rules=$25,
|
||||||
|
keep_filter_entry_rules=$26
|
||||||
WHERE
|
WHERE
|
||||||
id=$26
|
id=$27
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
|
@ -224,6 +225,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
user.DefaultHomePage,
|
user.DefaultHomePage,
|
||||||
user.CategoriesSortingOrder,
|
user.CategoriesSortingOrder,
|
||||||
user.MarkReadOnView,
|
user.MarkReadOnView,
|
||||||
|
user.MarkReadOnMediaPlayerCompletion,
|
||||||
user.MediaPlaybackRate,
|
user.MediaPlaybackRate,
|
||||||
user.BlockFilterEntryRules,
|
user.BlockFilterEntryRules,
|
||||||
user.KeepFilterEntryRules,
|
user.KeepFilterEntryRules,
|
||||||
|
@ -256,11 +258,12 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
default_home_page=$19,
|
default_home_page=$19,
|
||||||
categories_sorting_order=$20,
|
categories_sorting_order=$20,
|
||||||
mark_read_on_view=$21,
|
mark_read_on_view=$21,
|
||||||
media_playback_rate=$22,
|
mark_read_on_media_player_completion=$22,
|
||||||
block_filter_entry_rules=$23,
|
media_playback_rate=$23,
|
||||||
keep_filter_entry_rules=$24
|
block_filter_entry_rules=$24,
|
||||||
|
keep_filter_entry_rules=$25
|
||||||
WHERE
|
WHERE
|
||||||
id=$25
|
id=$26
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
|
@ -286,6 +289,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
user.DefaultHomePage,
|
user.DefaultHomePage,
|
||||||
user.CategoriesSortingOrder,
|
user.CategoriesSortingOrder,
|
||||||
user.MarkReadOnView,
|
user.MarkReadOnView,
|
||||||
|
user.MarkReadOnMediaPlayerCompletion,
|
||||||
user.MediaPlaybackRate,
|
user.MediaPlaybackRate,
|
||||||
user.BlockFilterEntryRules,
|
user.BlockFilterEntryRules,
|
||||||
user.KeepFilterEntryRules,
|
user.KeepFilterEntryRules,
|
||||||
|
@ -337,6 +341,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
||||||
default_home_page,
|
default_home_page,
|
||||||
categories_sorting_order,
|
categories_sorting_order,
|
||||||
mark_read_on_view,
|
mark_read_on_view,
|
||||||
|
mark_read_on_media_player_completion,
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules
|
keep_filter_entry_rules
|
||||||
|
@ -375,6 +380,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
|
||||||
default_home_page,
|
default_home_page,
|
||||||
categories_sorting_order,
|
categories_sorting_order,
|
||||||
mark_read_on_view,
|
mark_read_on_view,
|
||||||
|
mark_read_on_media_player_completion,
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules
|
keep_filter_entry_rules
|
||||||
|
@ -413,6 +419,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
|
||||||
default_home_page,
|
default_home_page,
|
||||||
categories_sorting_order,
|
categories_sorting_order,
|
||||||
mark_read_on_view,
|
mark_read_on_view,
|
||||||
|
mark_read_on_media_player_completion,
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules
|
keep_filter_entry_rules
|
||||||
|
@ -458,6 +465,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
|
||||||
u.default_home_page,
|
u.default_home_page,
|
||||||
u.categories_sorting_order,
|
u.categories_sorting_order,
|
||||||
u.mark_read_on_view,
|
u.mark_read_on_view,
|
||||||
|
u.mark_read_on_media_player_completion,
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
u.block_filter_entry_rules,
|
u.block_filter_entry_rules,
|
||||||
u.keep_filter_entry_rules
|
u.keep_filter_entry_rules
|
||||||
|
@ -497,6 +505,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
|
||||||
&user.DefaultHomePage,
|
&user.DefaultHomePage,
|
||||||
&user.CategoriesSortingOrder,
|
&user.CategoriesSortingOrder,
|
||||||
&user.MarkReadOnView,
|
&user.MarkReadOnView,
|
||||||
|
&user.MarkReadOnMediaPlayerCompletion,
|
||||||
&user.MediaPlaybackRate,
|
&user.MediaPlaybackRate,
|
||||||
&user.BlockFilterEntryRules,
|
&user.BlockFilterEntryRules,
|
||||||
&user.KeepFilterEntryRules,
|
&user.KeepFilterEntryRules,
|
||||||
|
@ -608,6 +617,7 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
default_home_page,
|
default_home_page,
|
||||||
categories_sorting_order,
|
categories_sorting_order,
|
||||||
mark_read_on_view,
|
mark_read_on_view,
|
||||||
|
mark_read_on_media_player_completion,
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules
|
keep_filter_entry_rules
|
||||||
|
@ -648,6 +658,7 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
&user.DefaultHomePage,
|
&user.DefaultHomePage,
|
||||||
&user.CategoriesSortingOrder,
|
&user.CategoriesSortingOrder,
|
||||||
&user.MarkReadOnView,
|
&user.MarkReadOnView,
|
||||||
|
&user.MarkReadOnMediaPlayerCompletion,
|
||||||
&user.MediaPlaybackRate,
|
&user.MediaPlaybackRate,
|
||||||
&user.BlockFilterEntryRules,
|
&user.BlockFilterEntryRules,
|
||||||
&user.KeepFilterEntryRules,
|
&user.KeepFilterEntryRules,
|
||||||
|
|
|
@ -173,6 +173,9 @@
|
||||||
<audio controls preload="metadata"
|
<audio controls preload="metadata"
|
||||||
data-last-position="{{ .MediaProgression }}"
|
data-last-position="{{ .MediaProgression }}"
|
||||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||||
|
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||||
|
data-mark-read-on-completion="0.9"
|
||||||
|
{{ end }}
|
||||||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||||||
data-enclosure-id="{{.ID}}"
|
data-enclosure-id="{{.ID}}"
|
||||||
>
|
>
|
||||||
|
@ -189,6 +192,9 @@
|
||||||
<video controls preload="metadata"
|
<video controls preload="metadata"
|
||||||
data-last-position="{{ .MediaProgression }}"
|
data-last-position="{{ .MediaProgression }}"
|
||||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||||
|
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||||
|
data-mark-read-on-completion="0.9"
|
||||||
|
{{ end }}
|
||||||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||||||
data-enclosure-id="{{.ID}}"
|
data-enclosure-id="{{.ID}}"
|
||||||
>
|
>
|
||||||
|
@ -221,6 +227,9 @@
|
||||||
<audio controls preload="metadata"
|
<audio controls preload="metadata"
|
||||||
data-last-position="{{ .MediaProgression }}"
|
data-last-position="{{ .MediaProgression }}"
|
||||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||||
|
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||||
|
data-mark-read-on-completion="0.9"
|
||||||
|
{{ end }}
|
||||||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||||||
data-enclosure-id="{{.ID}}"
|
data-enclosure-id="{{.ID}}"
|
||||||
>
|
>
|
||||||
|
@ -237,6 +246,9 @@
|
||||||
<video controls preload="metadata"
|
<video controls preload="metadata"
|
||||||
data-last-position="{{ .MediaProgression }}"
|
data-last-position="{{ .MediaProgression }}"
|
||||||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||||||
|
{{ if $.user.MarkReadOnMediaPlayerCompletion }}
|
||||||
|
data-mark-read-on-completion="0.9"
|
||||||
|
{{ end }}
|
||||||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||||||
data-enclosure-id="{{.ID}}"
|
data-enclosure-id="{{.ID}}"
|
||||||
>
|
>
|
||||||
|
|
|
@ -113,7 +113,14 @@
|
||||||
|
|
||||||
<label><input type="checkbox" name="show_reading_time" value="1" {{ if .form.ShowReadingTime }}checked{{ end }}> {{ t "form.prefs.label.show_reading_time" }}</label>
|
<label><input type="checkbox" name="show_reading_time" value="1" {{ if .form.ShowReadingTime }}checked{{ end }}> {{ t "form.prefs.label.show_reading_time" }}</label>
|
||||||
|
|
||||||
<label><input type="checkbox" name="mark_read_on_view" value="1" {{ if .form.MarkReadOnView }}checked{{ end }}> {{ t "form.prefs.label.mark_read_on_view" }}</label>
|
<label><input type="radio" name="mark_read_behavior" value="{{ .const.NoAutoMarkAsRead }}"
|
||||||
|
{{ if eq .form.MarkReadBehavior .const.NoAutoMarkAsRead }}checked{{end}} > {{ t "form.prefs.label.mark_read_manually" }}</label>
|
||||||
|
<label><input type="radio" name="mark_read_behavior" value="{{ .const.MarkAsReadOnView }}"
|
||||||
|
{{ if eq .form.MarkReadBehavior .const.MarkAsReadOnView }}checked{{end}} > {{ t "form.prefs.label.mark_read_on_view" }}</label>
|
||||||
|
<label><input type="radio" name="mark_read_behavior" value="{{ .const.MarkAsReadOnViewButWaitForPlayerCompletion }}"
|
||||||
|
{{ if eq .form.MarkReadBehavior .const.MarkAsReadOnViewButWaitForPlayerCompletion }}checked{{end}}> {{ t "form.prefs.label.mark_read_on_view_or_media_completion" }}</label>
|
||||||
|
<label><input type="radio" name="mark_read_behavior" value="{{ .const.MarkAsReadOnlyOnPlayerCompletion }}"
|
||||||
|
{{ if eq .form.MarkReadBehavior .const.MarkAsReadOnlyOnPlayerCompletion }}checked{{end}} > {{ t "form.prefs.label.mark_read_on_media_completion" }}</label>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (h *handler) showStarredEntryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (h *handler) showCategoryEntryPage(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (h *handler) showFeedEntryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (h *handler) showSearchEntryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
|
@ -46,7 +46,7 @@ func (h *handler) showTagEntryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
|
||||||
prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
|
prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
entry.Status = model.EntryStatusRead
|
entry.Status = model.EntryStatusRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,16 @@ import (
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MarkReadBehavior list all possible behaviors for automatically marking an entry as read
|
||||||
|
type MarkReadBehavior string
|
||||||
|
|
||||||
|
var (
|
||||||
|
NoAutoMarkAsRead MarkReadBehavior = "no-auto"
|
||||||
|
MarkAsReadOnView MarkReadBehavior = "on-view"
|
||||||
|
MarkAsReadOnViewButWaitForPlayerCompletion MarkReadBehavior = "on-view-but-wait-for-player-completion"
|
||||||
|
MarkAsReadOnlyOnPlayerCompletion MarkReadBehavior = "on-player-completion"
|
||||||
|
)
|
||||||
|
|
||||||
// SettingsForm represents the settings form.
|
// SettingsForm represents the settings form.
|
||||||
type SettingsForm struct {
|
type SettingsForm struct {
|
||||||
Username string
|
Username string
|
||||||
|
@ -33,11 +43,47 @@ type SettingsForm struct {
|
||||||
DefaultHomePage string
|
DefaultHomePage string
|
||||||
CategoriesSortingOrder string
|
CategoriesSortingOrder string
|
||||||
MarkReadOnView bool
|
MarkReadOnView bool
|
||||||
|
// MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together
|
||||||
|
MarkReadBehavior MarkReadBehavior
|
||||||
MediaPlaybackRate float64
|
MediaPlaybackRate float64
|
||||||
BlockFilterEntryRules string
|
BlockFilterEntryRules string
|
||||||
KeepFilterEntryRules string
|
KeepFilterEntryRules string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
|
||||||
|
// Useful to convert the values from the User model to the form
|
||||||
|
func MarkAsReadBehavior(markReadOnView, markReadOnMediaPlayerCompletion bool) MarkReadBehavior {
|
||||||
|
switch {
|
||||||
|
case markReadOnView && !markReadOnMediaPlayerCompletion:
|
||||||
|
return MarkAsReadOnView
|
||||||
|
case markReadOnView && markReadOnMediaPlayerCompletion:
|
||||||
|
return MarkAsReadOnViewButWaitForPlayerCompletion
|
||||||
|
case !markReadOnView && markReadOnMediaPlayerCompletion:
|
||||||
|
return MarkAsReadOnlyOnPlayerCompletion
|
||||||
|
case !markReadOnView && !markReadOnMediaPlayerCompletion:
|
||||||
|
fallthrough // Explicit defaulting
|
||||||
|
default:
|
||||||
|
return NoAutoMarkAsRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractMarkAsReadBehavior returns the MarkReadOnView and MarkReadOnMediaPlayerCompletion values from the given MarkReadBehavior.
|
||||||
|
// Useful to extract the values from the form to the User model
|
||||||
|
func ExtractMarkAsReadBehavior(behavior MarkReadBehavior) (markReadOnView, markReadOnMediaPlayerCompletion bool) {
|
||||||
|
switch behavior {
|
||||||
|
case MarkAsReadOnView:
|
||||||
|
return true, false
|
||||||
|
case MarkAsReadOnViewButWaitForPlayerCompletion:
|
||||||
|
return true, true
|
||||||
|
case MarkAsReadOnlyOnPlayerCompletion:
|
||||||
|
return false, true
|
||||||
|
case NoAutoMarkAsRead:
|
||||||
|
fallthrough // Explicit defaulting
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Merge updates the fields of the given user.
|
// Merge updates the fields of the given user.
|
||||||
func (s *SettingsForm) Merge(user *model.User) *model.User {
|
func (s *SettingsForm) Merge(user *model.User) *model.User {
|
||||||
user.Username = s.Username
|
user.Username = s.Username
|
||||||
|
@ -57,11 +103,14 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
|
||||||
user.DefaultReadingSpeed = s.DefaultReadingSpeed
|
user.DefaultReadingSpeed = s.DefaultReadingSpeed
|
||||||
user.DefaultHomePage = s.DefaultHomePage
|
user.DefaultHomePage = s.DefaultHomePage
|
||||||
user.CategoriesSortingOrder = s.CategoriesSortingOrder
|
user.CategoriesSortingOrder = s.CategoriesSortingOrder
|
||||||
user.MarkReadOnView = s.MarkReadOnView
|
|
||||||
user.MediaPlaybackRate = s.MediaPlaybackRate
|
user.MediaPlaybackRate = s.MediaPlaybackRate
|
||||||
user.BlockFilterEntryRules = s.BlockFilterEntryRules
|
user.BlockFilterEntryRules = s.BlockFilterEntryRules
|
||||||
user.KeepFilterEntryRules = s.KeepFilterEntryRules
|
user.KeepFilterEntryRules = s.KeepFilterEntryRules
|
||||||
|
|
||||||
|
MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior)
|
||||||
|
user.MarkReadOnView = MarkReadOnView
|
||||||
|
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
|
||||||
|
|
||||||
if s.Password != "" {
|
if s.Password != "" {
|
||||||
user.Password = s.Password
|
user.Password = s.Password
|
||||||
}
|
}
|
||||||
|
@ -136,6 +185,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
|
||||||
DefaultHomePage: r.FormValue("default_home_page"),
|
DefaultHomePage: r.FormValue("default_home_page"),
|
||||||
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
|
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
|
||||||
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
|
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
|
||||||
|
MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")),
|
||||||
MediaPlaybackRate: mediaPlaybackRate,
|
MediaPlaybackRate: mediaPlaybackRate,
|
||||||
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
||||||
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
CJKReadingSpeed: user.CJKReadingSpeed,
|
CJKReadingSpeed: user.CJKReadingSpeed,
|
||||||
DefaultHomePage: user.DefaultHomePage,
|
DefaultHomePage: user.DefaultHomePage,
|
||||||
CategoriesSortingOrder: user.CategoriesSortingOrder,
|
CategoriesSortingOrder: user.CategoriesSortingOrder,
|
||||||
MarkReadOnView: user.MarkReadOnView,
|
MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
|
||||||
MediaPlaybackRate: user.MediaPlaybackRate,
|
MediaPlaybackRate: user.MediaPlaybackRate,
|
||||||
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
||||||
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
||||||
|
@ -61,6 +61,13 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
sess := session.New(h.store, request.SessionID(r))
|
sess := session.New(h.store, request.SessionID(r))
|
||||||
view := view.New(h.tpl, r, sess)
|
view := view.New(h.tpl, r, sess)
|
||||||
view.Set("form", settingsForm)
|
view.Set("form", settingsForm)
|
||||||
|
// In order to keep the continuity between form and model, I pass adhoc constants to the view
|
||||||
|
view.Set("const", map[string]interface{}{
|
||||||
|
"NoAutoMarkAsRead": form.NoAutoMarkAsRead,
|
||||||
|
"MarkAsReadOnView": form.MarkAsReadOnView,
|
||||||
|
"MarkAsReadOnViewButWaitForPlayerCompletion": form.MarkAsReadOnViewButWaitForPlayerCompletion,
|
||||||
|
"MarkAsReadOnlyOnPlayerCompletion": form.MarkAsReadOnlyOnPlayerCompletion,
|
||||||
|
})
|
||||||
view.Set("themes", model.Themes())
|
view.Set("themes", model.Themes())
|
||||||
view.Set("languages", locale.AvailableLanguages())
|
view.Set("languages", locale.AvailableLanguages())
|
||||||
view.Set("timezones", timezones)
|
view.Set("timezones", timezones)
|
||||||
|
|
|
@ -678,9 +678,13 @@ function goToAddSubscription() {
|
||||||
* save player position to allow to resume playback later
|
* save player position to allow to resume playback later
|
||||||
* @param {Element} playerElement
|
* @param {Element} playerElement
|
||||||
*/
|
*/
|
||||||
function handlePlayerProgressionSave(playerElement) {
|
function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
|
||||||
|
if (!isPlayerPlaying(playerElement)) {
|
||||||
|
return; //If the player is not playing, we do not want to save the progression and mark as read on completion
|
||||||
|
}
|
||||||
const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
|
const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
|
||||||
const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
|
const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
|
||||||
|
const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion); //completion percentage to mark as read
|
||||||
const recordInterval = 10;
|
const recordInterval = 10;
|
||||||
|
|
||||||
// we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds
|
// we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds
|
||||||
|
@ -691,8 +695,28 @@ function handlePlayerProgressionSave(playerElement) {
|
||||||
const request = new RequestBuilder(playerElement.dataset.saveUrl);
|
const request = new RequestBuilder(playerElement.dataset.saveUrl);
|
||||||
request.withBody({ progression: currentPositionInSeconds });
|
request.withBody({ progression: currentPositionInSeconds });
|
||||||
request.execute();
|
request.execute();
|
||||||
|
// Handle the mark as read on completion
|
||||||
|
if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) {
|
||||||
|
const completion = currentPositionInSeconds / playerElement.duration;
|
||||||
|
if (completion >= markAsReadOnCompletion) {
|
||||||
|
handleEntryStatus("none", document.querySelector(":is(a, button)[data-toggle-status]"), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the player is actually playing a media
|
||||||
|
* @param element the player element itself
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isPlayerPlaying(element) {
|
||||||
|
return element &&
|
||||||
|
element.currentTime > 0 &&
|
||||||
|
!element.paused &&
|
||||||
|
!element.ended &&
|
||||||
|
element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle new share entires and already shared entries
|
* handle new share entires and already shared entries
|
||||||
|
|
2
internal/ui/static/js/bootstrap.js
vendored
2
internal/ui/static/js/bootstrap.js
vendored
|
@ -159,7 +159,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (element.dataset.lastPosition) {
|
if (element.dataset.lastPosition) {
|
||||||
element.currentTime = element.dataset.lastPosition;
|
element.currentTime = element.dataset.lastPosition;
|
||||||
}
|
}
|
||||||
element.ontimeupdate = () => handlePlayerProgressionSave(element);
|
element.ontimeupdate = () => handlePlayerProgressionSaveAndMarkAsReadOnCompletion(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set media playback rate
|
// Set media playback rate
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (h *handler) showUnreadCategoryEntryPage(w http.ResponseWriter, r *http.Req
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (h *handler) showUnreadFeedEntryPage(w http.ResponseWriter, r *http.Request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
|
if entry.ShouldMarkAsReadOnView(user) {
|
||||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue