mirror of https://github.com/miniflux/v2.git
Compare commits
3 Commits
fce4da79e1
...
71bf2695f2
Author | SHA1 | Date |
---|---|---|
John | 71bf2695f2 | |
Ztec | 93bc9ce24d | |
John | 19fb9675a4 |
|
@ -2109,3 +2109,21 @@ func TestParseConfigDumpOutput(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentSecurityPolicy(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("CONTENT_SECURITY_POLICY", "default-src 'self' fonts.googleapis.com fonts.gstatic.com; img-src * data:; media-src *; frame-src *; style-src 'self' fonts.googleapis.com fonts.gstatic.com 'nonce-%s'")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "default-src 'self' fonts.googleapis.com fonts.gstatic.com; img-src * data:; media-src *; frame-src *; style-src 'self' fonts.googleapis.com fonts.gstatic.com 'nonce-%s'"
|
||||
result := opts.ContentSecurityPolicy()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected CONTENT_SECURITY_POLICY value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ const (
|
|||
defaultWatchdog = true
|
||||
defaultInvidiousInstance = "yewtu.be"
|
||||
defaultWebAuthn = false
|
||||
defaultContentSecurityPolicy = "default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self' 'nonce-%s'; require-trusted-types-for 'script'; trusted-types ttpolicy;"
|
||||
)
|
||||
|
||||
var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
|
||||
|
@ -169,6 +170,7 @@ type Options struct {
|
|||
invidiousInstance string
|
||||
mediaProxyPrivateKey []byte
|
||||
webAuthn bool
|
||||
contentSecurityPolicy string
|
||||
}
|
||||
|
||||
// NewOptions returns Options with default values.
|
||||
|
@ -244,6 +246,7 @@ func NewOptions() *Options {
|
|||
invidiousInstance: defaultInvidiousInstance,
|
||||
mediaProxyPrivateKey: crypto.GenerateRandomBytes(16),
|
||||
webAuthn: defaultWebAuthn,
|
||||
contentSecurityPolicy: defaultContentSecurityPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -620,6 +623,11 @@ func (o *Options) FilterEntryMaxAgeDays() int {
|
|||
return o.filterEntryMaxAgeDays
|
||||
}
|
||||
|
||||
// ContentSecurityPolicy returns value for Content-Security-Policy meta tag.
|
||||
func (o *Options) ContentSecurityPolicy() string {
|
||||
return o.contentSecurityPolicy
|
||||
}
|
||||
|
||||
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
||||
func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
var keyValues = map[string]interface{}{
|
||||
|
@ -697,6 +705,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
|||
"WORKER_POOL_SIZE": o.workerPoolSize,
|
||||
"YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride,
|
||||
"WEBAUTHN": o.webAuthn,
|
||||
"CONTENT_SECURITY_POLICY": o.contentSecurityPolicy,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(keyValues))
|
||||
|
|
|
@ -271,6 +271,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
|
||||
case "WEBAUTHN":
|
||||
p.opts.webAuthn = parseBool(value, defaultWebAuthn)
|
||||
case "CONTENT_SECURITY_POLICY":
|
||||
p.opts.contentSecurityPolicy = parseString(value, defaultContentSecurityPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Abonnement kann nicht durch RSS-Bridge erkannt werden: %v.",
|
||||
"error.feed_format_not_detected": "Das Format des Abonnements kann nicht erkannt werden: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
|
||||
"error.settings_media_playback_rate_range": "Die Wiedergabegeschwindigkeit liegt außerhalb des Bereichs"
|
||||
"error.settings_media_playback_rate_range": "Die Wiedergabegeschwindigkeit liegt außerhalb des Bereichs",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
|
||||
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους"
|
||||
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
|
||||
"error.settings_media_playback_rate_range": "Playback speed is out of range"
|
||||
"error.settings_media_playback_rate_range": "Playback speed is out of range",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
|
||||
"error.settings_media_playback_rate_range": "La velocidad de reproducción está fuera de rango"
|
||||
"error.settings_media_playback_rate_range": "La velocidad de reproducción está fuera de rango",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
|
||||
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella"
|
||||
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Impossible de détecter un flux RSS en utilisant RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Impossible de détecter le format du flux : %v.",
|
||||
"form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
|
||||
"error.settings_media_playback_rate_range": "La vitesse de lecture est hors limites"
|
||||
"error.settings_media_playback_rate_range": "La vitesse de lecture est hors limites",
|
||||
"enclosure_media_controls.seek" : "Avancer/Reculer :",
|
||||
"enclosure_media_controls.seek.title" : "Avancer/Reculer de %s seconds",
|
||||
"enclosure_media_controls.speed" : "Vitesse :",
|
||||
"enclosure_media_controls.speed.faster" : "Accélérer",
|
||||
"enclosure_media_controls.speed.faster.title" : "Accélérer de %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Ralentir",
|
||||
"enclosure_media_controls.speed.slower.title" : "Ralentir de %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Réinitialiser",
|
||||
"enclosure_media_controls.speed.reset.title" : "Réinitialiser la vitesse de lecture à 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
|
||||
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है"
|
||||
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -512,5 +512,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
|
||||
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan"
|
||||
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
|
||||
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo"
|
||||
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -512,5 +512,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
|
||||
"error.settings_media_playback_rate_range": "再生速度が範囲外"
|
||||
"error.settings_media_playback_rate_range": "再生速度が範囲外",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video",
|
||||
"error.settings_media_playback_rate_range": "Afspeelsnelheid is buiten bereik"
|
||||
"error.settings_media_playback_rate_range": "Afspeelsnelheid is buiten bereik",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -546,5 +546,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Prędkość odtwarzania audio/wideo",
|
||||
"error.settings_media_playback_rate_range": "Prędkość odtwarzania jest poza zakresem"
|
||||
"error.settings_media_playback_rate_range": "Prędkość odtwarzania jest poza zakresem",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -529,5 +529,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
|
||||
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo"
|
||||
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -546,5 +546,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
|
||||
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона"
|
||||
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -496,5 +496,14 @@
|
|||
"time_elapsed.years": ["%d yıl önce", "%d yıl önce"],
|
||||
"time_elapsed.yesterday": "dün",
|
||||
"tooltip.keyboard_shortcuts": "Klavye Kısayolu: %s",
|
||||
"tooltip.logged_user": "%s olarak giriş yapıldı"
|
||||
"tooltip.logged_user": "%s olarak giriş yapıldı",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -546,5 +546,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
|
||||
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону"
|
||||
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -512,5 +512,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
|
||||
"error.settings_media_playback_rate_range": "播放速度超出范围"
|
||||
"error.settings_media_playback_rate_range": "播放速度超出范围",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -512,5 +512,14 @@
|
|||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "音訊/視訊的播放速度",
|
||||
"error.settings_media_playback_rate_range": "播放速度超出範圍"
|
||||
"error.settings_media_playback_rate_range": "播放速度超出範圍",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{{ define "enclosure_media_controls" }}
|
||||
<div class="media-controls">
|
||||
<div class="media-seek-control">
|
||||
<div class="media-control-label">{{ t "enclosure_media_controls.seek" }} </div>
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="-30" title="{{ t "enclosure_media_controls.seek.title" "-30" }}" ><span class="icon-label" >-30s</span></button>
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="-10" title="{{ t "enclosure_media_controls.seek.title" "-10" }}" ><span class="icon-label" >-10s</span></button>
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="+10" title="{{ t "enclosure_media_controls.seek.title" "+10" }}" ><span class="icon-label" >+10s</span></button>
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="seek" data-action-value="+30" title="{{ t "enclosure_media_controls.seek.title" "+30" }}" ><span class="icon-label" >+30s</span></button>
|
||||
</div>
|
||||
<div class="media-speed-control">
|
||||
|
||||
<div class="media-control-label">{{ t "enclosure_media_controls.speed" }} (<span class="speed-indicator" data-enclosure-id="{{.ID}}">x.xxx</span>)</div> <!-- Need JS to display the current speed unfortunately -->
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="speed" data-action-value="-0.25" title="{{ t "enclosure_media_controls.speed.slower.title" "0.25" }}"><span class="icon-label" >{{ t "enclosure_media_controls.speed.slower" }}</span></button>
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="speed-reset" data-action-value="1" title="{{ t "enclosure_media_controls.speed.reset.title"}}"><span class="icon-label" >{{ t "enclosure_media_controls.speed.reset" }}</span></button>
|
||||
<button class="page-button" data-enclosure-id="{{.ID}}" data-enclosure-action="speed" data-action-value="+0.25" title="{{ t "enclosure_media_controls.speed.faster.title" "0.25" }}"><span class="icon-label" >{{ t "enclosure_media_controls.speed.faster" }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -36,8 +36,13 @@
|
|||
|
||||
{{ if and .user .user.Stylesheet }}
|
||||
{{ $stylesheetNonce := nonce }}
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self' 'nonce-{{ $stylesheetNonce }}'; require-trusted-types-for 'script'; trusted-types ttpolicy;">
|
||||
<style nonce="{{ $stylesheetNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
|
||||
{{ $containsNonce := contains .contentSecurityPolicy "nonce-%s" }}
|
||||
{{ if $containsNonce }}
|
||||
{{ noescape ( printf "<meta http-equiv=\"Content-Security-Policy\" content=\"%s\">" (printf .contentSecurityPolicy $stylesheetNonce ) ) }}
|
||||
{{ else }}
|
||||
{{ noescape ( printf "<meta http-equiv=\"Content-Security-Policy\" content=\"%s\">" .contentSecurityPolicy ) }}
|
||||
{{ end }}
|
||||
<style {{ if $containsNonce }}nonce="{{ $stylesheetNonce }}"{{end}}>{{ .user.Stylesheet | safeCSS }}</style>
|
||||
{{ else }}
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types ttpolicy;">
|
||||
{{ end }}
|
||||
|
@ -58,7 +63,6 @@
|
|||
data-webauthn-delete-all-url="{{ route "webauthnDeleteAll" }}"
|
||||
{{ end }}
|
||||
{{ if .user }}{{ if not .user.KeyboardShortcuts }}data-disable-keyboard-shortcuts="true"{{ end }}{{ end }}>
|
||||
|
||||
{{ if .user }}
|
||||
<a class="skip-to-content-link" href="#main">{{ t "skip_to_content" }}</a>
|
||||
<header class="header">
|
||||
|
|
|
@ -174,6 +174,7 @@
|
|||
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 }}">
|
||||
|
@ -181,6 +182,7 @@
|
|||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</audio>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if hasPrefix .MimeType "video/" }}
|
||||
<div class="enclosure-video">
|
||||
|
@ -188,6 +190,7 @@
|
|||
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 }}">
|
||||
|
@ -195,6 +198,7 @@
|
|||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</video>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
@ -218,6 +222,7 @@
|
|||
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 }}">
|
||||
|
@ -225,6 +230,7 @@
|
|||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</audio>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if hasPrefix .MimeType "video/" }}
|
||||
<div class="enclosure-video">
|
||||
|
@ -232,6 +238,7 @@
|
|||
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 }}">
|
||||
|
@ -239,6 +246,7 @@
|
|||
<source src="{{ .URL | safeURL }}" type="{{ .Html5MimeType }}">
|
||||
{{ end }}
|
||||
</video>
|
||||
{{ template "enclosure_media_controls" . }}
|
||||
</div>
|
||||
{{ else if hasPrefix .MimeType "image/" }}
|
||||
<div class="enclosure-image">
|
||||
|
|
|
@ -1215,6 +1215,39 @@ audio, video {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.media-controls{
|
||||
font-size: .9em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.media-controls .media-control-label{
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.media-controls>div{
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content:center;
|
||||
min-width: 50%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-controls>div>*{
|
||||
padding-left:12px;
|
||||
}
|
||||
|
||||
.media-controls>div>*:first-child{
|
||||
padding-left:0;
|
||||
}
|
||||
|
||||
.media-controls span.speed-indicator{
|
||||
/*monospace to ensure constant width even when value change. JS ensure the value is always on 4 characters (in most cases)
|
||||
This reduce ui flickering due to element moving around a bit
|
||||
*/
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.integration-form summary {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
|
@ -446,8 +446,8 @@ function goToPage(page, fallbackSelf) {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {(number|event)} offset - many items to jump for focus.
|
||||
*
|
||||
* @param {(number|event)} offset - many items to jump for focus.
|
||||
*/
|
||||
function goToPrevious(offset) {
|
||||
if (offset instanceof KeyboardEvent) {
|
||||
|
@ -461,8 +461,8 @@ function goToPrevious(offset) {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {(number|event)} offset - How many items to jump for focus.
|
||||
*
|
||||
* @param {(number|event)} offset - How many items to jump for focus.
|
||||
*/
|
||||
function goToNext(offset) {
|
||||
if (offset instanceof KeyboardEvent) {
|
||||
|
@ -521,7 +521,7 @@ function goToListItem(offset) {
|
|||
items[i].classList.remove("current-item");
|
||||
|
||||
// By default adjust selection by offset
|
||||
let itemOffset = (i + offset + items.length) % items.length;
|
||||
let itemOffset = (i + offset + items.length) % items.length;
|
||||
// Allow jumping to top or bottom
|
||||
if (offset == TOP) {
|
||||
itemOffset = 0;
|
||||
|
@ -742,3 +742,43 @@ function getCsrfToken() {
|
|||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle all clicks on media player controls button on enclosures.
|
||||
* Will change the current speed and position of the player accordingly.
|
||||
* Will not save anything, all is done client-side, however, changing the position
|
||||
* will trigger the handlePlayerProgressionSave and save the new position backends side.
|
||||
* @param {Element} button
|
||||
*/
|
||||
function handleMediaControl(button) {
|
||||
const action = button.dataset.enclosureAction;
|
||||
const value = parseFloat(button.dataset.actionValue);
|
||||
const targetEnclosureId = button.dataset.enclosureId;
|
||||
const enclosures = document.querySelectorAll(`audio[data-enclosure-id="${targetEnclosureId}"],video[data-enclosure-id="${targetEnclosureId}"]`);
|
||||
const speedIndicator = document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${targetEnclosureId}"]`);
|
||||
enclosures.forEach((enclosure) => {
|
||||
switch (action) {
|
||||
case "seek":
|
||||
enclosure.currentTime = enclosure.currentTime + value > 0 ? enclosure.currentTime + value : 0;
|
||||
break;
|
||||
case "speed":
|
||||
// I set a floor speed of 0.25 to avoid too slow speed where it gives the impression it stopped.
|
||||
// 0.25 was chosen because it will allow to get back to 1x in two "faster" click, and lower value with same property would be 0.
|
||||
enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value);
|
||||
speedIndicator.forEach((speedI) => {
|
||||
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
|
||||
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
|
||||
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
|
||||
});
|
||||
break;
|
||||
case "speed-reset":
|
||||
enclosure.playbackRate = value ;
|
||||
speedIndicator.forEach((speedI) => {
|
||||
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
|
||||
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
|
||||
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -167,6 +167,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
playbackRateElements.forEach((element) => {
|
||||
if (element.dataset.playbackRate) {
|
||||
element.playbackRate = element.dataset.playbackRate;
|
||||
if (element.dataset.enclosureId){
|
||||
// In order to display properly the speed we need to do it on bootstrap.
|
||||
// Could not do it backend side because I didn't know how to do it because of the template inclusion and
|
||||
// the way the initial playback speed is handled. See enclosure_media_controls.html if you want to try to fix this
|
||||
document.querySelectorAll(`span.speed-indicator[data-enclosure-id="${element.dataset.enclosureId}"]`).forEach((speedI)=>{
|
||||
speedI.innerText = `${element.dataset.playbackRate}x`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set enclosure media controls handlers
|
||||
const mediaControlsElements = document.querySelectorAll("button[data-enclosure-action]");
|
||||
mediaControlsElements.forEach((element) => {
|
||||
element.addEventListener("click", () => handleMediaControl(element));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,5 +46,6 @@ func New(tpl *template.Engine, r *http.Request, sess *session.Session) *View {
|
|||
"sw_js_checksum": static.JavascriptBundleChecksums["service-worker"],
|
||||
"webauthn_js_checksum": static.JavascriptBundleChecksums["webauthn"],
|
||||
"webAuthnEnabled": config.Opts.WebAuthn(),
|
||||
"contentSecurityPolicy": config.Opts.ContentSecurityPolicy(),
|
||||
}}
|
||||
}
|
||||
|
|
|
@ -534,6 +534,15 @@ Default is 16 workers\&.
|
|||
YouTube URL which will be used for embeds\&.
|
||||
.br
|
||||
Default is https://www.youtube-nocookie.com/embed/\&.
|
||||
.TP
|
||||
.B CONTENT_SECURITY_POLICY
|
||||
Set custom value for Content-Security-Policy meta tag. Used when custom CSS is applied.
|
||||
.br
|
||||
It may contain "nonce-%s", where nonce will be placed\&.
|
||||
.br
|
||||
Default is "default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self' 'nonce-%s'; require-trusted-types-for 'script'; trusted-types ttpolicy;"\&.
|
||||
.TP
|
||||
|
||||
.SH AUTHORS
|
||||
.P
|
||||
Miniflux is written and maintained by Fr\['e]d\['e]ric Guillot\&.
|
||||
|
|
Loading…
Reference in New Issue