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

@ -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)