diff options
author | 2024-03-16 14:20:02 +0100 | |
---|---|---|
committer | 2024-03-17 11:53:30 -0700 | |
commit | 00dabc1d3c86af88ece7a292ecc968f5d825749e (patch) | |
tree | 9369f3d606abf6e52257547631859bd772163e88 | |
parent | b68ada396a342ef6ab447c2bb98d7c96aa643178 (diff) | |
download | v2-00dabc1d3c86af88ece7a292ecc968f5d825749e.tar.gz v2-00dabc1d3c86af88ece7a292ecc968f5d825749e.tar.zst v2-00dabc1d3c86af88ece7a292ecc968f5d825749e.zip |
feat: Media player: Conrol playback speed
fix #1845
31 files changed, 188 insertions, 74 deletions
diff --git a/client/model.go b/client/model.go index 05bc0c2f..c0d42eb8 100644 --- a/client/model.go +++ b/client/model.go @@ -41,6 +41,7 @@ type User struct { 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"` } func (u User) String() string { @@ -58,28 +59,29 @@ type UserCreationRequest struct { // UserModificationRequest represents the request to update a user. type UserModificationRequest struct { - Username *string `json:"username"` - Password *string `json:"password"` - 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"` - 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"` + Username *string `json:"username"` + Password *string `json:"password"` + 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"` + 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"` } // Users represents a list of users. diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 0721f218..cfc1159d 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -871,4 +871,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 media_playback_rate numeric default 1;` + _, err = tx.Exec(sql) + return err + }, } diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 58650850..55e82a94 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Dieses Abonnement kann nicht gelesen werden: %v.", "error.feed_not_found": "Dieses Abonnement existiert nicht oder gehört nicht zu diesem Benutzer.", "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." + "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" } diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 2351ee26..b2a71007 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο", + "error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους" } diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 50a43b9f..777b9818 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index e08c8438..981279e9 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index 9da193a3..e81973e9 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index ca6c264b..70fe9ebd 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Impossible d'analyser ce flux : %v.", "error.feed_not_found": "Impossible de trouver ce flux.", "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." + "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" } diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index 00c81287..bfb9a19d 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति", + "error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है" } diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 70aa938a..a1e0ee3c 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -507,5 +507,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index ac1ce3c5..b218bc08 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index 5363d89a..3e6b41f6 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -507,5 +507,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度", + "error.settings_media_playback_rate_range": "再生速度が範囲外" } diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index 5b5b57db..a8c29c86 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index 3a7ad74d..4e0b7b92 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -541,5 +541,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index 1e1c1b9b..3e0de941 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %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" } diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 2c55537e..940380a4 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -541,5 +541,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео", + "error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона" } diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index 3d8c8add..0d650c2a 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -524,5 +524,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "Ses/video oynatma hızı", + "error.settings_media_playback_rate_range": "Oynatma hızı aralık dışında" } diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index 6bf94b09..76982ea1 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -541,5 +541,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео", + "error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону" } diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 1d7d2e32..f12ea8da 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -507,5 +507,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "音频/视频的播放速度", + "error.settings_media_playback_rate_range": "播放速度超出范围" } diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index 8f748c01..5696491c 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -507,5 +507,7 @@ "error.unable_to_parse_feed": "Unable to parse this feed: %v.", "error.feed_not_found": "This feed does not exist or does not belong to this user.", "error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.", - "error.feed_format_not_detected": "Unable to detect feed format: %v." + "error.feed_format_not_detected": "Unable to detect feed format: %v.", + "form.prefs.label.media_playback_rate": "音訊/視訊的播放速度", + "error.settings_media_playback_rate_range": "播放速度超出範圍" } diff --git a/internal/model/model.go b/internal/model/model.go index 82d69b8a..aed0fe14 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -26,3 +26,11 @@ func OptionalInt64(value int64) *int64 { } return nil } + +// OptionalFloat populates an optional float64 field. +func OptionalFloat(value float64) *float64 { + if value > 0 { + return &value + } + return nil +} diff --git a/internal/model/user.go b/internal/model/user.go index 61ff1065..62aff600 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -35,6 +35,7 @@ type User struct { 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"` } // UserCreationRequest represents the request to create a user. @@ -48,28 +49,29 @@ 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"` + 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"` } // Patch updates the User object with the modification request. @@ -161,6 +163,10 @@ func (u *UserModificationRequest) Patch(user *User) { if u.MarkReadOnView != nil { user.MarkReadOnView = *u.MarkReadOnView } + + if u.MediaPlaybackRate != nil { + user.MediaPlaybackRate = *u.MediaPlaybackRate + } } // UseTimezone converts last login date to the given timezone. diff --git a/internal/storage/user.go b/internal/storage/user.go index b3d50a17..4f30ac0d 100644 --- a/internal/storage/user.go +++ b/internal/storage/user.go @@ -91,7 +91,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m cjk_reading_speed, default_home_page, categories_sorting_order, - mark_read_on_view + mark_read_on_view, + media_playback_rate ` tx, err := s.db.Begin() @@ -130,6 +131,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m &user.DefaultHomePage, &user.CategoriesSortingOrder, &user.MarkReadOnView, + &user.MediaPlaybackRate, ) if err != nil { tx.Rollback() @@ -186,9 +188,10 @@ func (s *Storage) UpdateUser(user *model.User) error { cjk_reading_speed=$19, default_home_page=$20, categories_sorting_order=$21, - mark_read_on_view=$22 + mark_read_on_view=$22, + media_playback_rate=$23 WHERE - id=$23 + id=$24 ` _, err = s.db.Exec( @@ -215,6 +218,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.DefaultHomePage, user.CategoriesSortingOrder, user.MarkReadOnView, + user.MediaPlaybackRate, user.ID, ) if err != nil { @@ -243,9 +247,10 @@ func (s *Storage) UpdateUser(user *model.User) error { cjk_reading_speed=$18, default_home_page=$19, categories_sorting_order=$20, - mark_read_on_view=$21 + mark_read_on_view=$21, + media_playback_rate=$22 WHERE - id=$22 + id=$23 ` _, err := s.db.Exec( @@ -271,6 +276,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.DefaultHomePage, user.CategoriesSortingOrder, user.MarkReadOnView, + user.MediaPlaybackRate, user.ID, ) @@ -318,7 +324,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) { cjk_reading_speed, default_home_page, categories_sorting_order, - mark_read_on_view + mark_read_on_view, + media_playback_rate FROM users WHERE @@ -353,7 +360,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) { cjk_reading_speed, default_home_page, categories_sorting_order, - mark_read_on_view + mark_read_on_view, + media_playback_rate FROM users WHERE @@ -388,7 +396,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) { cjk_reading_speed, default_home_page, categories_sorting_order, - mark_read_on_view + mark_read_on_view, + media_playback_rate FROM users WHERE @@ -430,7 +439,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) { u.cjk_reading_speed, u.default_home_page, u.categories_sorting_order, - u.mark_read_on_view + u.mark_read_on_view, + media_playback_rate FROM users u LEFT JOIN @@ -467,6 +477,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err &user.DefaultHomePage, &user.CategoriesSortingOrder, &user.MarkReadOnView, + &user.MediaPlaybackRate, ) if err == sql.ErrNoRows { @@ -574,7 +585,8 @@ func (s *Storage) Users() (model.Users, error) { cjk_reading_speed, default_home_page, categories_sorting_order, - mark_read_on_view + mark_read_on_view, + media_playback_rate FROM users ORDER BY username ASC @@ -612,6 +624,7 @@ func (s *Storage) Users() (model.Users, error) { &user.DefaultHomePage, &user.CategoriesSortingOrder, &user.MarkReadOnView, + &user.MediaPlaybackRate, ) if err != nil { diff --git a/internal/template/templates/views/entry.html b/internal/template/templates/views/entry.html index 6a4ed9c5..4284f097 100644 --- a/internal/template/templates/views/entry.html +++ b/internal/template/templates/views/entry.html @@ -172,6 +172,7 @@ <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 }}" > {{ if (and $.user (mustBeProxyfied "audio")) }} @@ -185,6 +186,7 @@ <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 }}" > {{ if (and $.user (mustBeProxyfied "video")) }} @@ -214,6 +216,7 @@ <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 }}" > {{ if (and $.user (mustBeProxyfied "audio")) }} @@ -227,6 +230,7 @@ <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 }}" > {{ if (and $.user (mustBeProxyfied "video")) }} diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html index 9846d893..0be77f62 100644 --- a/internal/template/templates/views/settings.html +++ b/internal/template/templates/views/settings.html @@ -108,6 +108,9 @@ <label for="form-default-reading-speed">{{ t "form.prefs.label.default_reading_speed" }}</label> <input type="number" name="default_reading_speed" id="form-default-reading-speed" value="{{ .form.DefaultReadingSpeed }}" min="1"> + <label for="form-media-playback-rate">{{ t "form.prefs.label.media_playback_rate" }}</label> + <input type="number" name="media_playback_rate" id="form-media-playback-rate" value="{{ .form.MediaPlaybackRate }}" min="0.25" max="4" step="any" /> + <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> diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go index d5442218..a46d9714 100644 --- a/internal/ui/form/settings.go +++ b/internal/ui/form/settings.go @@ -33,6 +33,7 @@ type SettingsForm struct { DefaultHomePage string CategoriesSortingOrder string MarkReadOnView bool + MediaPlaybackRate float64 } // Merge updates the fields of the given user. @@ -55,6 +56,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User { user.DefaultHomePage = s.DefaultHomePage user.CategoriesSortingOrder = s.CategoriesSortingOrder user.MarkReadOnView = s.MarkReadOnView + user.MediaPlaybackRate = s.MediaPlaybackRate if s.Password != "" { user.Password = s.Password @@ -84,6 +86,10 @@ func (s *SettingsForm) Validate() *locale.LocalizedError { } } + if s.MediaPlaybackRate < 0.25 || s.MediaPlaybackRate > 4 { + return locale.NewLocalizedError("error.settings_media_playback_rate_range") + } + return nil } @@ -101,6 +107,10 @@ func NewSettingsForm(r *http.Request) *SettingsForm { if err != nil { cjkReadingSpeed = 0 } + mediaPlaybackRate, err := strconv.ParseFloat(r.FormValue("media_playback_rate"), 64) + if err != nil { + mediaPlaybackRate = 1 + } return &SettingsForm{ Username: r.FormValue("username"), Password: r.FormValue("password"), @@ -122,5 +132,6 @@ 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", + MediaPlaybackRate: mediaPlaybackRate, } } diff --git a/internal/ui/form/settings_test.go b/internal/ui/form/settings_test.go index a8afdfb7..84bbd9b7 100644 --- a/internal/ui/form/settings_test.go +++ b/internal/ui/form/settings_test.go @@ -22,6 +22,7 @@ func TestValid(t *testing.T) { DefaultReadingSpeed: 35, CJKReadingSpeed: 25, DefaultHomePage: "unread", + MediaPlaybackRate: 1.25, } err := settings.Validate() @@ -45,6 +46,7 @@ func TestConfirmationEmpty(t *testing.T) { DefaultReadingSpeed: 35, CJKReadingSpeed: 25, DefaultHomePage: "unread", + MediaPlaybackRate: 1.25, } err := settings.Validate() @@ -72,6 +74,7 @@ func TestConfirmationIncorrect(t *testing.T) { DefaultReadingSpeed: 35, CJKReadingSpeed: 25, DefaultHomePage: "unread", + MediaPlaybackRate: 1.25, } err := settings.Validate() diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go index 23e6d401..3a96b29c 100644 --- a/internal/ui/settings_show.go +++ b/internal/ui/settings_show.go @@ -41,6 +41,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { DefaultHomePage: user.DefaultHomePage, CategoriesSortingOrder: user.CategoriesSortingOrder, MarkReadOnView: user.MarkReadOnView, + MediaPlaybackRate: user.MediaPlaybackRate, } timezones, err := h.store.Timezones() diff --git a/internal/ui/settings_update.go b/internal/ui/settings_update.go index 122ad441..e0e3c0af 100644 --- a/internal/ui/settings_update.go +++ b/internal/ui/settings_update.go @@ -62,6 +62,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) { DefaultReadingSpeed: model.OptionalInt(settingsForm.DefaultReadingSpeed), CJKReadingSpeed: model.OptionalInt(settingsForm.CJKReadingSpeed), DefaultHomePage: model.OptionalString(settingsForm.DefaultHomePage), + MediaPlaybackRate: model.OptionalFloat(settingsForm.MediaPlaybackRate), } if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil { diff --git a/internal/ui/static/js/bootstrap.js b/internal/ui/static/js/bootstrap.js index 0acef71c..0a9c2b78 100644 --- a/internal/ui/static/js/bootstrap.js +++ b/internal/ui/static/js/bootstrap.js @@ -152,11 +152,19 @@ document.addEventListener("DOMContentLoaded", () => { }); // Save and resume media position - const elements = document.querySelectorAll("audio[data-last-position],video[data-last-position]"); - elements.forEach((element) => { + const lastPositionElements = document.querySelectorAll("audio[data-last-position],video[data-last-position]"); + lastPositionElements.forEach((element) => { if (element.dataset.lastPosition) { element.currentTime = element.dataset.lastPosition; } element.ontimeupdate = () => handlePlayerProgressionSave(element); }); + + // Set media playback rate + const playbackRateElements = document.querySelectorAll("audio[data-playback-rate],video[data-playback-rate]"); + playbackRateElements.forEach((element) => { + if (element.dataset.playbackRate) { + element.playbackRate = element.dataset.playbackRate; + } + }); }); diff --git a/internal/validator/user.go b/internal/validator/user.go index 8c6cc9d2..a397167e 100644 --- a/internal/validator/user.go +++ b/internal/validator/user.go @@ -102,6 +102,12 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod } } + if changes.MediaPlaybackRate != nil { + if err := validateMediaPlaybackRate(*changes.MediaPlaybackRate); err != nil { + return err + } + } + return nil } @@ -182,3 +188,10 @@ func validateDefaultHomePage(defaultHomePage string) *locale.LocalizedError { } return nil } + +func validateMediaPlaybackRate(mediaPlaybackRate float64) *locale.LocalizedError { + if mediaPlaybackRate < 0.25 || mediaPlaybackRate > 4 { + return locale.NewLocalizedError("error.settings_media_playback_rate_range") + } + return nil +} |