1
0
Fork 0
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:
Loïc Doubinine 2024-07-28 21:29:45 +02:00 committed by GitHub
parent 37309adbc0
commit 4f55361f5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 278 additions and 76 deletions

View file

@ -937,4 +937,9 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql)
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
},
}

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Standard-Startseite",
"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_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.authentication_settings": "Authentifizierungseinstellungen",
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
"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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Default home page",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"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.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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"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.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.authentication_settings": "Paramètres d'authentification",
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
"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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -384,6 +384,9 @@
"form.prefs.label.default_home_page": "Beranda Baku",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -384,6 +384,9 @@
"form.prefs.label.default_home_page": "デフォルトのトップページ",
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
"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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Standaard startpagina",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -404,6 +404,9 @@
"form.prefs.label.default_home_page": "Domyślna strona główna",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -394,6 +394,9 @@
"form.prefs.label.default_home_page": "Página inicial predefinida",
"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_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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -404,6 +404,9 @@
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
"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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -304,6 +304,9 @@
"form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
"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_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.show_reading_time": "Makaleler için tahmini okuma süresini göster",
"form.prefs.label.theme": "Tema",

View file

@ -404,6 +404,9 @@
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
"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.authentication_settings": "Authentication Settings",
"form.prefs.fieldset.reader_settings": "Reader Settings",

View file

@ -384,6 +384,9 @@
"form.prefs.label.default_home_page": "默认主页",
"form.prefs.label.categories_sorting_order": "分类排序",
"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.authentication_settings": "用户认证设置",
"form.prefs.fieldset.reader_settings": "阅读器设置",

View file

@ -384,6 +384,9 @@
"form.prefs.label.default_home_page": "預設主頁",
"form.prefs.label.categories_sorting_order": "分類排序",
"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.authentication_settings": "使用者認證設定",
"form.prefs.fieldset.reader_settings": "閱讀器設定",

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
package model // import "miniflux.app/v2/internal/model"
import "strings"
// Enclosure represents an attachment.
type Enclosure struct {
@ -24,3 +25,12 @@ func (e Enclosure) Html5MimeType() string {
// EnclosureList represents a list of attachments.
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
}

View file

@ -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.
type Entries []*Entry

View file

@ -11,33 +11,34 @@ import (
// User represents a user in the system.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
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"`
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"`
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
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"`
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"`
MarkReadOnMediaPlayerCompletion bool `json:"mark_read_on_media_player_completion"`
MediaPlaybackRate float64 `json:"media_playback_rate"`
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
}
// UserCreationRequest represents the request to create a user.
@ -51,31 +52,32 @@ type UserCreationRequest struct {
// UserModificationRequest represents the request to update a user.
type UserModificationRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
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"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
IsAdmin *bool `json:"is_admin"`
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"`
Username *string `json:"username"`
Password *string `json:"password"`
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"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
IsAdmin *bool `json:"is_admin"`
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"`
MarkReadOnMediaPlayerCompletion *bool `json:"mark_read_on_media_player_completion"`
MediaPlaybackRate *float64 `json:"media_playback_rate"`
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
}
// Patch updates the User object with the modification request.
@ -168,6 +170,10 @@ func (u *UserModificationRequest) Patch(user *User) {
user.MarkReadOnView = *u.MarkReadOnView
}
if u.MarkReadOnMediaPlayerCompletion != nil {
user.MarkReadOnMediaPlayerCompletion = *u.MarkReadOnMediaPlayerCompletion
}
if u.MediaPlaybackRate != nil {
user.MediaPlaybackRate = *u.MediaPlaybackRate
}

View file

@ -193,11 +193,12 @@ func (s *Storage) UpdateUser(user *model.User) error {
default_home_page=$20,
categories_sorting_order=$21,
mark_read_on_view=$22,
media_playback_rate=$23,
block_filter_entry_rules=$24,
keep_filter_entry_rules=$25
mark_read_on_media_player_completion=$23,
media_playback_rate=$24,
block_filter_entry_rules=$25,
keep_filter_entry_rules=$26
WHERE
id=$26
id=$27
`
_, err = s.db.Exec(
@ -224,6 +225,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.DefaultHomePage,
user.CategoriesSortingOrder,
user.MarkReadOnView,
user.MarkReadOnMediaPlayerCompletion,
user.MediaPlaybackRate,
user.BlockFilterEntryRules,
user.KeepFilterEntryRules,
@ -256,11 +258,12 @@ func (s *Storage) UpdateUser(user *model.User) error {
default_home_page=$19,
categories_sorting_order=$20,
mark_read_on_view=$21,
media_playback_rate=$22,
block_filter_entry_rules=$23,
keep_filter_entry_rules=$24
mark_read_on_media_player_completion=$22,
media_playback_rate=$23,
block_filter_entry_rules=$24,
keep_filter_entry_rules=$25
WHERE
id=$25
id=$26
`
_, err := s.db.Exec(
@ -286,6 +289,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.DefaultHomePage,
user.CategoriesSortingOrder,
user.MarkReadOnView,
user.MarkReadOnMediaPlayerCompletion,
user.MediaPlaybackRate,
user.BlockFilterEntryRules,
user.KeepFilterEntryRules,
@ -337,6 +341,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
default_home_page,
categories_sorting_order,
mark_read_on_view,
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
@ -375,6 +380,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
default_home_page,
categories_sorting_order,
mark_read_on_view,
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
@ -413,6 +419,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
default_home_page,
categories_sorting_order,
mark_read_on_view,
mark_read_on_media_player_completion,
media_playback_rate,
block_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.categories_sorting_order,
u.mark_read_on_view,
u.mark_read_on_media_player_completion,
media_playback_rate,
u.block_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.CategoriesSortingOrder,
&user.MarkReadOnView,
&user.MarkReadOnMediaPlayerCompletion,
&user.MediaPlaybackRate,
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
@ -608,6 +617,7 @@ func (s *Storage) Users() (model.Users, error) {
default_home_page,
categories_sorting_order,
mark_read_on_view,
mark_read_on_media_player_completion,
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules
@ -648,6 +658,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.DefaultHomePage,
&user.CategoriesSortingOrder,
&user.MarkReadOnView,
&user.MarkReadOnMediaPlayerCompletion,
&user.MediaPlaybackRate,
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,

View file

@ -173,6 +173,9 @@
<audio controls preload="metadata"
data-last-position="{{ .MediaProgression }}"
{{ 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-enclosure-id="{{.ID}}"
>
@ -189,6 +192,9 @@
<video controls preload="metadata"
data-last-position="{{ .MediaProgression }}"
{{ 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-enclosure-id="{{.ID}}"
>
@ -221,6 +227,9 @@
<audio controls preload="metadata"
data-last-position="{{ .MediaProgression }}"
{{ 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-enclosure-id="{{.ID}}"
>
@ -237,6 +246,9 @@
<video controls preload="metadata"
data-last-position="{{ .MediaProgression }}"
{{ 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-enclosure-id="{{.ID}}"
>

View file

@ -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="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">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>

View file

@ -38,7 +38,7 @@ func (h *handler) showStarredEntryPage(w http.ResponseWriter, r *http.Request) {
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)

View file

@ -41,7 +41,7 @@ func (h *handler) showCategoryEntryPage(w http.ResponseWriter, r *http.Request)
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)

View file

@ -41,7 +41,7 @@ func (h *handler) showFeedEntryPage(w http.ResponseWriter, r *http.Request) {
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)

View file

@ -40,7 +40,7 @@ func (h *handler) showSearchEntryPage(w http.ResponseWriter, r *http.Request) {
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)

View file

@ -46,7 +46,7 @@ func (h *handler) showTagEntryPage(w http.ResponseWriter, r *http.Request) {
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)

View file

@ -66,7 +66,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
}
if user.MarkReadOnView {
if entry.ShouldMarkAsReadOnView(user) {
entry.Status = model.EntryStatusRead
}

View file

@ -11,6 +11,16 @@ import (
"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.
type SettingsForm struct {
Username string
@ -33,9 +43,45 @@ type SettingsForm struct {
DefaultHomePage string
CategoriesSortingOrder string
MarkReadOnView bool
MediaPlaybackRate float64
BlockFilterEntryRules string
KeepFilterEntryRules string
// MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together
MarkReadBehavior MarkReadBehavior
MediaPlaybackRate float64
BlockFilterEntryRules 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.
@ -57,11 +103,14 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.DefaultReadingSpeed = s.DefaultReadingSpeed
user.DefaultHomePage = s.DefaultHomePage
user.CategoriesSortingOrder = s.CategoriesSortingOrder
user.MarkReadOnView = s.MarkReadOnView
user.MediaPlaybackRate = s.MediaPlaybackRate
user.BlockFilterEntryRules = s.BlockFilterEntryRules
user.KeepFilterEntryRules = s.KeepFilterEntryRules
MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior)
user.MarkReadOnView = MarkReadOnView
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
if s.Password != "" {
user.Password = s.Password
}
@ -136,6 +185,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
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"),

View file

@ -40,7 +40,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
CJKReadingSpeed: user.CJKReadingSpeed,
DefaultHomePage: user.DefaultHomePage,
CategoriesSortingOrder: user.CategoriesSortingOrder,
MarkReadOnView: user.MarkReadOnView,
MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
MediaPlaybackRate: user.MediaPlaybackRate,
BlockFilterEntryRules: user.BlockFilterEntryRules,
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))
view := view.New(h.tpl, r, sess)
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("languages", locale.AvailableLanguages())
view.Set("timezones", timezones)

View file

@ -678,9 +678,13 @@ function goToAddSubscription() {
* save player position to allow to resume playback later
* @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 lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion); //completion percentage to mark as read
const recordInterval = 10;
// we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds
@ -691,9 +695,29 @@ function handlePlayerProgressionSave(playerElement) {
const request = new RequestBuilder(playerElement.dataset.saveUrl);
request.withBody({ progression: currentPositionInSeconds });
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
*/

View file

@ -159,7 +159,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (element.dataset.lastPosition) {
element.currentTime = element.dataset.lastPosition;
}
element.ontimeupdate = () => handlePlayerProgressionSave(element);
element.ontimeupdate = () => handlePlayerProgressionSaveAndMarkAsReadOnCompletion(element);
});
// Set media playback rate

View file

@ -41,7 +41,7 @@ func (h *handler) showUnreadCategoryEntryPage(w http.ResponseWriter, r *http.Req
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)

View file

@ -41,7 +41,7 @@ func (h *handler) showUnreadFeedEntryPage(w http.ResponseWriter, r *http.Request
return
}
if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
if entry.ShouldMarkAsReadOnView(user) {
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
if err != nil {
html.ServerError(w, r, err)