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
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
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) {
|
||||
element.currentTime = element.dataset.lastPosition;
|
||||
}
|
||||
element.ontimeupdate = () => handlePlayerProgressionSave(element);
|
||||
element.ontimeupdate = () => handlePlayerProgressionSaveAndMarkAsReadOnCompletion(element);
|
||||
});
|
||||
|
||||
// Set media playback rate
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue