93bc9ce24d
When listening to podcast, it is usual to want to speed up the playback. https://github.com/miniflux/v2/pull/2521 was addressing the need globally, this PR allow to address it for just the current open enclosure media. (no save) Some Browser already include this control directly, but firefox does not (directly anyway). Also, it is often useful to be able to skip chunk of a podcast, to skip commercials for example, or get back a bit because we couldn't hear the last part. I added rudimentary seek controls with the usual +/-10 and 30 seconds chuck size. This is pretty handy when podcast are very long and using the seek bar is way too tricky to just skip 30s. As always, I'm French and could only provide English and French translation for the few text I added in the locale/translations files. Any help is welcome. Tested mostly on Firefox (121.0) and quickly on Vivaldi(6.5.3206.53), chrome based. Fixes: #1845 #1846
277 lines
13 KiB
HTML
277 lines
13 KiB
HTML
{{ define "title"}}{{ .entry.Title }}{{ end }}
|
||
|
||
{{ define "page_header"}}
|
||
<section class="entry" data-id="{{ .entry.ID }}" aria-labelledby="page-header-title">
|
||
<header class="entry-header">
|
||
<h1 id="page-header-title" dir="auto">
|
||
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
||
</h1>
|
||
{{ if .user }}
|
||
<div class="entry-actions">
|
||
<ul>
|
||
<li>
|
||
<button
|
||
class="page-button"
|
||
title="{{ t "entry.status.title" }}"
|
||
data-toggle-status="true"
|
||
data-label-loading="{{ t "entry.state.saving" }}"
|
||
data-label-unread="{{ t "entry.status.unread" }}"
|
||
data-label-read="{{ t "entry.status.read" }}"
|
||
data-toast-unread="{{ t "entry.status.toast.unread" }}"
|
||
data-toast-read="{{ t "entry.status.toast.read" }}"
|
||
data-value="{{ if eq .entry.Status "read" }}read{{ else }}unread{{ end }}"
|
||
>{{ if eq .entry.Status "unread" }}{{ icon "read" }}{{ else }}{{ icon "unread" }}{{ end }}<span class="icon-label">{{ if eq .entry.Status "unread" }}{{ t "entry.status.read" }}{{ else }}{{ t "entry.status.unread" }}{{ end }}</span></button>
|
||
</li>
|
||
<li>
|
||
<button
|
||
class="page-button"
|
||
data-toggle-bookmark="true"
|
||
data-bookmark-url="{{ route "toggleBookmark" "entryID" .entry.ID }}"
|
||
data-label-loading="{{ t "entry.state.saving" }}"
|
||
data-label-star="{{ t "entry.bookmark.toggle.on" }}"
|
||
data-label-unstar="{{ t "entry.bookmark.toggle.off" }}"
|
||
data-toast-star="{{ t "entry.bookmark.toast.on" }}"
|
||
data-toast-unstar="{{ t "entry.bookmark.toast.off" }}"
|
||
data-value="{{ if .entry.Starred }}star{{ else }}unstar{{ end }}"
|
||
>{{ if .entry.Starred }}{{ icon "unstar" }}{{ else }}{{ icon "star" }}{{ end }}<span class="icon-label">{{ if .entry.Starred }}{{ t "entry.bookmark.toggle.off" }}{{ else }}{{ t "entry.bookmark.toggle.on" }}{{ end }}</span></button>
|
||
</li>
|
||
{{ if .hasSaveEntry }}
|
||
<li>
|
||
<button
|
||
class="page-button"
|
||
title="{{ t "entry.save.title" }}"
|
||
data-save-entry="true"
|
||
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
|
||
data-label-loading="{{ t "entry.state.saving" }}"
|
||
data-label-done="{{ t "entry.save.completed" }}"
|
||
data-toast-done="{{ t "entry.save.toast.completed" }}"
|
||
>{{ icon "save" }}<span class="icon-label">{{ t "entry.save.label" }}</span></button>
|
||
</li>
|
||
{{ end }}
|
||
{{ if .entry.ShareCode }}
|
||
<li>
|
||
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
|
||
title="{{ t "entry.shared_entry.title" }}"
|
||
data-share-status="shared"
|
||
target="_blank">{{ icon "share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
|
||
</li>
|
||
<li>
|
||
<button
|
||
class="page-button"
|
||
data-confirm="true"
|
||
data-url="{{ route "unshareEntry" "entryID" .entry.ID }}"
|
||
data-label-question="{{ t "confirm.question" }}"
|
||
data-label-yes="{{ t "confirm.yes" }}"
|
||
data-label-no="{{ t "confirm.no" }}"
|
||
data-label-loading="{{ t "confirm.loading" }}">{{ icon "delete" }}<span class="icon-label">{{ t "entry.unshare.label" }}</span></button>
|
||
</li>
|
||
{{ else }}
|
||
<li>
|
||
<a href="{{ route "shareEntry" "entryID" .entry.ID }}"
|
||
class="page-link"
|
||
title="{{ t "entry.share.title" }}"
|
||
data-share-status="share"
|
||
target="_blank">{{ icon "share" }}<span class="icon-label">{{ t "entry.share.label" }}</span></a>
|
||
</li>
|
||
{{ end }}
|
||
<li>
|
||
<a href="{{ .entry.URL | safeURL }}"
|
||
class="page-link"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
referrerpolicy="no-referrer"
|
||
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
|
||
</li>
|
||
<li>
|
||
<button
|
||
class="page-button"
|
||
title="{{ t "entry.scraper.title" }}"
|
||
data-fetch-content-entry="true"
|
||
data-fetch-content-url="{{ route "fetchContent" "entryID" .entry.ID }}"
|
||
data-label-loading="{{ t "entry.state.loading" }}"
|
||
>{{ icon "scraper" }}<span class="icon-label">{{ t "entry.scraper.label" }}</span></button>
|
||
</li>
|
||
{{ if .entry.CommentsURL }}
|
||
<li>
|
||
<a href="{{ .entry.CommentsURL | safeURL }}"
|
||
class="page-link"
|
||
title="{{ t "entry.comments.title" }}"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
referrerpolicy="no-referrer"
|
||
data-comments-link="true"
|
||
>{{ icon "comment" }}<span class="icon-label">{{ t "entry.comments.label" }}</span></a>
|
||
</li>
|
||
{{ end }}
|
||
</ul>
|
||
</div>
|
||
{{ end }}
|
||
<div class="entry-meta" dir="auto">
|
||
<span class="entry-website">
|
||
{{ if and .user (ne .entry.Feed.Icon.IconID 0) }}
|
||
<img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .entry.Feed.Title }}">
|
||
{{ end }}
|
||
{{ if .user }}
|
||
<a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
|
||
{{ else }}
|
||
<a href="{{ .entry.Feed.SiteURL | safeURL }}">{{ .entry.Feed.Title }}</a>
|
||
{{ end }}
|
||
</span>
|
||
{{ if .entry.Author }}
|
||
<span class="entry-author">
|
||
{{ if isEmail .entry.Author }}
|
||
- <a href="mailto:{{ .entry.Author }}">{{ .entry.Author }}</a>
|
||
{{ else }}
|
||
– <em>{{ .entry.Author }}</em>
|
||
{{ end }}
|
||
</span>
|
||
{{ end }}
|
||
{{ if .user }}
|
||
<span class="category">
|
||
<a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
|
||
</span>
|
||
{{ end }}
|
||
</div>
|
||
{{ if .entry.Tags }}
|
||
<div class="entry-tags">
|
||
{{ t "entry.tags.label" }}
|
||
{{range $i, $e := .entry.Tags}}{{if $i}}, {{end}}<a href="{{ route "tagEntriesAll" "tagName" (urlEncode $e) }}"><strong>{{ $e }}</strong></a>{{end}}
|
||
</div>
|
||
{{ end }}
|
||
<div class="entry-date">
|
||
{{ if .user }}
|
||
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed $.user.Timezone .entry.Date }}</time>
|
||
{{ else }}
|
||
<time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed "UTC" .entry.Date }}</time>
|
||
{{ end }}
|
||
{{ if and .user.ShowReadingTime (gt .entry.ReadingTime 0) }}
|
||
·
|
||
<span class="entry-reading-time">
|
||
{{ plural "entry.estimated_reading_time" .entry.ReadingTime .entry.ReadingTime }}
|
||
</span>
|
||
{{ end }}
|
||
</div>
|
||
</header>
|
||
</section>
|
||
{{ end }}
|
||
|
||
|
||
{{ define "content"}}
|
||
{{ if gt (len .entry.Content) 120 }}
|
||
{{ if .user }}
|
||
<div class="pagination-entry-top">
|
||
{{ template "entry_pagination" . }}
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|
||
<article class="entry-content gesture-nav-{{ $.user.GestureNav }}" dir="auto">
|
||
{{ if (and .entry.Enclosures (not .entry.Feed.NoMediaPlayer)) }}
|
||
{{ range .entry.Enclosures }}
|
||
{{ if ne .URL "" }}
|
||
{{ if hasPrefix .MimeType "audio/" }}
|
||
<div class="enclosure-audio" >
|
||
<audio controls preload="metadata"
|
||
data-last-position="{{ .MediaProgression }}"
|
||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||
data-enclosure-id="{{.ID}}"
|
||
>
|
||
{{ if (and $.user (mustBeProxyfied "audio")) }}
|
||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||
{{ else }}
|
||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||
{{ end }}
|
||
</audio>
|
||
{{ template "enclosure_media_controls" . }}
|
||
</div>
|
||
{{ else if hasPrefix .MimeType "video/" }}
|
||
<div class="enclosure-video">
|
||
<video controls preload="metadata"
|
||
data-last-position="{{ .MediaProgression }}"
|
||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||
data-enclosure-id="{{.ID}}"
|
||
>
|
||
{{ if (and $.user (mustBeProxyfied "video")) }}
|
||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||
{{ else }}
|
||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||
{{ end }}
|
||
</video>
|
||
{{ template "enclosure_media_controls" . }}
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|
||
{{ end }}
|
||
{{end}}
|
||
{{ if .user }}
|
||
{{ noescape (proxyFilter .entry.Content) }}
|
||
{{ else }}
|
||
{{ noescape .entry.Content }}
|
||
{{ end }}
|
||
</article>
|
||
{{ if .entry.Enclosures }}
|
||
<details class="entry-enclosures">
|
||
<summary>{{ t "page.entry.attachments" }} ({{ len .entry.Enclosures }})</summary>
|
||
{{ range .entry.Enclosures }}
|
||
{{ if ne .URL "" }}
|
||
<div class="entry-enclosure">
|
||
{{ if hasPrefix .MimeType "audio/" }}
|
||
<div class="enclosure-audio">
|
||
<audio controls preload="metadata"
|
||
data-last-position="{{ .MediaProgression }}"
|
||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||
data-enclosure-id="{{.ID}}"
|
||
>
|
||
{{ if (and $.user (mustBeProxyfied "audio")) }}
|
||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||
{{ else }}
|
||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||
{{ end }}
|
||
</audio>
|
||
{{ template "enclosure_media_controls" . }}
|
||
</div>
|
||
{{ else if hasPrefix .MimeType "video/" }}
|
||
<div class="enclosure-video">
|
||
<video controls preload="metadata"
|
||
data-last-position="{{ .MediaProgression }}"
|
||
{{ if $.user.MediaPlaybackRate }}data-playback-rate="{{ $.user.MediaPlaybackRate }}"{{ end }}
|
||
data-save-url="{{ route "saveEnclosureProgression" "enclosureID" .ID }}"
|
||
data-enclosure-id="{{.ID}}"
|
||
>
|
||
{{ if (and $.user (mustBeProxyfied "video")) }}
|
||
<source src="{{ proxyURL .URL }}" type="{{ .Html5MimeType }}">
|
||
{{ else }}
|
||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||
{{ end }}
|
||
</video>
|
||
{{ template "enclosure_media_controls" . }}
|
||
</div>
|
||
{{ else if hasPrefix .MimeType "image/" }}
|
||
<div class="enclosure-image">
|
||
{{ if (and $.user (mustBeProxyfied "image")) }}
|
||
<img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||
{{ else }}
|
||
<img src="{{ .URL | safeURL }}" title="{{ .URL }} ({{ .MimeType }})" loading="lazy" alt="{{ .URL }} ({{ .MimeType }})">
|
||
{{ end }}
|
||
</div>
|
||
{{ end }}
|
||
|
||
<div class="entry-enclosure-download">
|
||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }} ({{ .MimeType }})" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
|
||
<small>{{ if gt .Size 0 }} - <strong>{{ formatFileSize .Size }}</strong>{{ end }}</small>
|
||
</div>
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|
||
</details>
|
||
{{ end }}
|
||
|
||
|
||
{{ if .user }}
|
||
<div class="pagination-entry-bottom">
|
||
{{ template "entry_pagination" . }}
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|