diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt index e60c2c4a6..1b666e094 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt @@ -158,7 +158,8 @@ data class PlayerSettings ( val acceptedOrientations: List, val fillScreen: Boolean, val videoFit: VideoPlayerFit, - val screensaver: Screensaver + val screensaver: Screensaver, + val playbackRate: Double ) { companion object { @@ -173,7 +174,8 @@ data class PlayerSettings ( val fillScreen = pigeonVar_list[7] as Boolean val videoFit = pigeonVar_list[8] as VideoPlayerFit val screensaver = pigeonVar_list[9] as Screensaver - return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit, screensaver) + val playbackRate = pigeonVar_list[10] as Double + return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit, screensaver, playbackRate) } } fun toList(): List { @@ -188,6 +190,7 @@ data class PlayerSettings ( fillScreen, videoFit, screensaver, + playbackRate, ) } override fun equals(other: Any?): Boolean { diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt index f425790e4..049011da7 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt @@ -395,6 +395,12 @@ fun CustomVideoControls( if (showSpeedDialog) { PlaybackSpeedPicker( player = exoPlayer, + onSpeedSelected = { speed -> + VideoPlayerObject.videoPlayerListener?.onPlaybackRateChanged( + speed.toDouble(), + callback = {} + ) + }, onDismissRequest = { showSpeedDialog = false } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/PlaybackSpeedPicker.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/PlaybackSpeedPicker.kt index a2dc07316..4d5e8e1f7 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/PlaybackSpeedPicker.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/PlaybackSpeedPicker.kt @@ -16,14 +16,19 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import androidx.media3.exoplayer.ExoPlayer -private val playbackSpeeds = listOf(0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f) +private val presetPlaybackSpeeds = + listOf(0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.25f, 2.5f, 2.75f, 3.0f) @Composable fun PlaybackSpeedPicker( player: ExoPlayer, + onSpeedSelected: (Float) -> Unit, onDismissRequest: () -> Unit ) { - var currentSpeed = player.playbackParameters.speed + val currentSpeed = player.playbackParameters.speed + val playbackSpeeds = remember(currentSpeed) { + (presetPlaybackSpeeds + currentSpeed).distinct().sorted() + } val selectedIndex = playbackSpeeds.indexOf(currentSpeed).coerceAtLeast(0) val listState = rememberLazyListState() @@ -56,6 +61,7 @@ fun PlaybackSpeedPicker( .focusRequester(focusRequesters[index]), onClick = { player.setPlaybackSpeed(speed) + onSpeedSelected(speed) onDismissRequest() }, selected = selected, @@ -65,4 +71,4 @@ fun PlaybackSpeedPicker( } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt index 44bb2a8d5..9ea793d6d 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt @@ -120,6 +120,11 @@ internal fun ExoPlayer( extractorsFactory = extractorsFactory, renderType = AssRenderType.LEGACY ) + .also { + it.setPlaybackSpeed( + (PlayerSettingsObject.settings.value?.playbackRate ?: 1.0).toFloat() + ) + } } fun updatePlaybackState() { diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 4ec248aa5..4b042ca47 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -2080,5 +2080,9 @@ } } }, - "setAs": "تعيين كـ" + "setAs": "تعيين كـ", + "rememberPlaybackRateTitle": "تذكر معدل التشغيل", + "rememberPlaybackRateDesc": "إعادة استخدام آخر معدل تشغيل محدد بعد إغلاق المشغل", + "defaultPlaybackRateTitle": "معدل التشغيل الافتراضي", + "defaultPlaybackRateDesc": "يُستخدم حتى تختار معدل تشغيل مختلف" } diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 640ad007f..b294da87f 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -2,5 +2,10 @@ "nativeName": "ইংরাজী", "accept": "মেনে নিচ্ছি", "active": "স্বক্রিয়", - "addAsFavorite": "পছন্দের তালিকাতে যোগ করুন" + "addAsFavorite": "পছন্দের তালিকাতে যোগ করুন", + "playbackRate": "প্লেব্যাক হার", + "rememberPlaybackRateTitle": "প্লেব্যাক হার মনে রাখুন", + "rememberPlaybackRateDesc": "প্লেয়ার বন্ধ করার পর আপনার শেষ নির্বাচিত প্লেব্যাক হার পুনরায় ব্যবহার করুন", + "defaultPlaybackRateTitle": "ডিফল্ট প্লেব্যাক হার", + "defaultPlaybackRateDesc": "আপনি ভিন্ন প্লেব্যাক হার বেছে না নেওয়া পর্যন্ত ব্যবহার করা হবে" } diff --git a/lib/l10n/app_br.arb b/lib/l10n/app_br.arb index b66c84462..82603f0fc 100644 --- a/lib/l10n/app_br.arb +++ b/lib/l10n/app_br.arb @@ -328,5 +328,10 @@ "seerrDetails": "Munudoù Seerr", "musicDashboard": "Taolenn-vourzh sonerezh", "quiet": "Sioul", - "loud": "Kreñv" + "loud": "Kreñv", + "playbackRate": "Tizhoù playback", + "rememberPlaybackRateTitle": "Derc’hel soñj eus ar c’hadenn playback", + "rememberPlaybackRateDesc": "Adimplijout ho kadenn playback diwezhañ diuzet goude bezañ serrad ar c’hwereer", + "defaultPlaybackRateTitle": "Kadenn playback dre ziouer", + "defaultPlaybackRateDesc": "Implijet ken ar choazit ur c’hadenn playback all" } diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index db04bf6bb..f681a8a98 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -1094,5 +1094,9 @@ "copyLoginLink": "Copiar enllaç", "includePassword": "Incloure contrasenya", "invalidAuthLink": "Enllaç d'inici de sessió no valid", - "homeBannerDetailed": "Detallat" + "homeBannerDetailed": "Detallat", + "rememberPlaybackRateTitle": "Recordar la velocitat de reproducció", + "rememberPlaybackRateDesc": "Reutilitza la darrera velocitat de reproducció seleccionada en tancar el reproductor", + "defaultPlaybackRateTitle": "Velocitat de reproducció per defecte", + "defaultPlaybackRateDesc": "S’utilitza fins que seleccioneu una velocitat diferent" } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index a2e096b51..666864bcd 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -1077,5 +1077,9 @@ "showMore": "Zobrazit více", "itemColorsTitle": "Barva položky", "clientSettingsUseSystemIMETitle": "Použití systémové klávesnice", - "homeBannerTV": "TV" + "homeBannerTV": "TV", + "rememberPlaybackRateTitle": "Zapamatovat rychlost přehrávání", + "rememberPlaybackRateDesc": "Znovu použít poslední vybranou rychlost přehrávání po zavření přehrávače", + "defaultPlaybackRateTitle": "Výchozí rychlost přehrávání", + "defaultPlaybackRateDesc": "Použije se, dokud nezvolíte jinou rychlost přehrávání" } diff --git a/lib/l10n/app_da.arb b/lib/l10n/app_da.arb index 052a6d554..685ac7251 100644 --- a/lib/l10n/app_da.arb +++ b/lib/l10n/app_da.arb @@ -2048,5 +2048,9 @@ "fallbackMaxBitrate": "Maksimal Fallback Bitrate (Mbps)", "concurrentStreams": "Maks Samtidige Streams", "concurrentStreamsHint": "0 = Ubegrænset", - "allowFmp4Container": "Tillad fMP4 transcoding container" + "allowFmp4Container": "Tillad fMP4 transcoding container", + "rememberPlaybackRateTitle": "Husk afspilningshastighed", + "rememberPlaybackRateDesc": "Genbrug den sidste valgte afspilningshastighed efter lukning af afspiller", + "defaultPlaybackRateTitle": "Standard afspilningshastighed", + "defaultPlaybackRateDesc": "Bruges indtil du vælger en anden afspilningshastighed" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f4d7b88a0..0ad8d44be 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2084,5 +2084,9 @@ } } }, - "settingsPlayerCrossfadeDesc": "Blende zwischen Titel über, die im selben Album nicht aufeinander folgen" + "settingsPlayerCrossfadeDesc": "Blende zwischen Titel über, die im selben Album nicht aufeinander folgen", + "rememberPlaybackRateTitle": "Wiedergaberate merken", + "rememberPlaybackRateDesc": "Verwendet die zuletzt ausgewählte Wiedergaberate nach dem Schließen des Players", + "defaultPlaybackRateTitle": "Standard-Wiedergaberate", + "defaultPlaybackRateDesc": "Wird verwendet, bis du eine andere Wiedergaberate auswählst" } diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb index 543b7af30..1debf7047 100644 --- a/lib/l10n/app_el.arb +++ b/lib/l10n/app_el.arb @@ -829,5 +829,10 @@ } }, "settingSecurityApplockTitle": "Κλείδωμα Εφαρμογής", - "@settingSecurityApplockTitle": {} + "@settingSecurityApplockTitle": {}, + "playbackRate": "Ρυθμός αναπαραγωγής", + "rememberPlaybackRateTitle": "Απομνημόνευση ρυθμού αναπαραγωγής", + "rememberPlaybackRateDesc": "Επαναχρησιμοποίηση του τελευταίου επιλεγμένου ρυθμού αναπαραγωγής μετά το κλείσιμο του player", + "defaultPlaybackRateTitle": "Προεπιλεγμένος ρυθμός αναπαραγωγής", + "defaultPlaybackRateDesc": "Χρησιμοποιείται μέχρι να επιλέξετε διαφορετικό ρυθμό αναπαραγωγής" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bc940880d..b355d8c13 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1393,6 +1393,14 @@ "autoNextOffStaticTitle": "Static", "autoNextOffStaticDesc": "Show the next-up screen when 30 seconds of playtime remain", "playbackRate": "Playback rate", + "rememberPlaybackRateTitle": "Remember playback rate", + "@rememberPlaybackRateTitle": {}, + "rememberPlaybackRateDesc": "Reuse your last selected playback rate after closing the player", + "@rememberPlaybackRateDesc": {}, + "defaultPlaybackRateTitle": "Default playback rate", + "@defaultPlaybackRateTitle": {}, + "defaultPlaybackRateDesc": "Used until you select a different playback rate", + "@defaultPlaybackRateDesc": {}, "speed": "Speed", "unableToPlayMedia": "There was an error finding a compatible media type", "errorOpeningMedia": "Something went wrong trying to play this media", @@ -2855,4 +2863,4 @@ "leanBackModeTitle": "Lean-back Mode", "leanBackModeDesc": "Enable lean-back mode on Android devices, even if the device reports back as false.", "leanBackModeInfo": "Lean-back mode is re-enabled by default on AndroidTV devices. Toggling this option on non-AndroidTV devices may cause unexpected behavior." -} \ No newline at end of file +} diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 03b49a09a..466049ece 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2054,5 +2054,9 @@ "pictureInPictureNotSupported": "Imagen dentro de imagen no está disponible en este dispositivo", "@pictureInPictureNotSupported": { "description": "Snackbar shown when the user taps the PiP button and the OS reports it unsupported" - } + }, + "rememberPlaybackRateTitle": "Recordar velocidad de reproducción", + "rememberPlaybackRateDesc": "Reutiliza la última velocidad de reproducción seleccionada al cerrar el reproductor", + "defaultPlaybackRateTitle": "Velocidad de reproducción predeterminada", + "defaultPlaybackRateDesc": "Se usa hasta que selecciones una velocidad diferente" } diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 1343f21a7..cd26b4208 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -2054,5 +2054,9 @@ "newsCategoriesSubLabel": "دسته بندی ها برای اختصاص به کانال ها مطابق با پیشوند اخبار", "sportsCategoriesSubLabel": "دسته بندی برای اختصاص به کانال های تطبیق پیشوند ورزشی", "kidsCategoriesSubLabel": "دسته بندی برای اختصاص دادن به کانال های تطبیق پیشوند کودکان", - "enableNewTVLayoutDesc": "بنر های سبک تلویزیون بزرگتر را در داشبورد نشان دهید. این تبدیل به طرح پیش فرض در یک انتشار آینده خواهد شد." + "enableNewTVLayoutDesc": "بنر های سبک تلویزیون بزرگتر را در داشبورد نشان دهید. این تبدیل به طرح پیش فرض در یک انتشار آینده خواهد شد.", + "rememberPlaybackRateTitle": "به خاطر سپردن نرخ پخش", + "rememberPlaybackRateDesc": "استفاده مجدد از آخرین نرخ پخش انتخاب‌شده پس از بستن پخش‌کننده", + "defaultPlaybackRateTitle": "نرخ پخش پیش‌فرض", + "defaultPlaybackRateDesc": "تا زمانی که نرخ پخش دیگری را انتخاب کنید استفاده می‌شود" } diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index acbd32bb9..5267b231d 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -2187,5 +2187,9 @@ "quiet": "Hiljainen", "loud": "Voimakas", "share": "Jaa", - "setAs": "Aseta" + "setAs": "Aseta", + "rememberPlaybackRateTitle": "Muista toistonopeus", + "rememberPlaybackRateDesc": "Käytä viimeksi valittua toistonopeutta toiston sulkemisen jälkeen", + "defaultPlaybackRateTitle": "Oletustoistonopeus", + "defaultPlaybackRateDesc": "Käytetään kunnes valitset toisen toistonopeuden" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 41c4f91d9..43ebf0c97 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2019,5 +2019,9 @@ "musicDashboard": "Tableau de bord musical", "quiet": "Faible", "loud": "Fort", - "toggleSubtitles": "Afficher/Masquer les sous-titres" + "toggleSubtitles": "Afficher/Masquer les sous-titres", + "rememberPlaybackRateTitle": "Mémoriser la vitesse de lecture", + "rememberPlaybackRateDesc": "Réutiliser la dernière vitesse de lecture sélectionnée après la fermeture du lecteur", + "defaultPlaybackRateTitle": "Vitesse de lecture par défaut", + "defaultPlaybackRateDesc": "Utilisée jusqu’à ce que vous choisissiez une autre vitesse de lecture" } diff --git a/lib/l10n/app_he.arb b/lib/l10n/app_he.arb index 177005601..8b651e52c 100644 --- a/lib/l10n/app_he.arb +++ b/lib/l10n/app_he.arb @@ -613,5 +613,10 @@ "subtitles": "כתוביות", "switchUser": "החלף משתמש", "sync": "סנכרן", - "hideEmpty": "הסתר ריק" + "hideEmpty": "הסתר ריק", + "playbackRate": "קצב השמעה", + "rememberPlaybackRateTitle": "זכור קצב השמעה", + "rememberPlaybackRateDesc": "השתמש שוב בקצב ההשמעה האחרון שנבחר לאחר סגירת הנגן", + "defaultPlaybackRateTitle": "קצב השמעה ברירת מחדל", + "defaultPlaybackRateDesc": "בשימוש עד שתבחר קצב השמעה אחר" } diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index 965d5a9ff..354120bd3 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -783,5 +783,10 @@ "type": "String" } } - } + }, + "playbackRate": "Brzina reprodukcije", + "rememberPlaybackRateTitle": "Zapamti brzinu reprodukcije", + "rememberPlaybackRateDesc": "Ponovno koristi zadnju odabranu brzinu reprodukcije nakon zatvaranja playera", + "defaultPlaybackRateTitle": "Zadana brzina reprodukcije", + "defaultPlaybackRateDesc": "Koristi se dok ne odaberete drugu brzinu reprodukcije" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index a61443914..9fbca94cf 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -798,5 +798,10 @@ } } }, - "syncOverlaySyncing": "Adatok szinkronizálása" + "syncOverlaySyncing": "Adatok szinkronizálása", + "playbackRate": "Lejátszási sebesség", + "rememberPlaybackRateTitle": "Lejátszási sebesség megjegyzése", + "rememberPlaybackRateDesc": "A lejátszó bezárása után újra használja az utoljára kiválasztott lejátszási sebességet", + "defaultPlaybackRateTitle": "Alapértelmezett lejátszási sebesség", + "defaultPlaybackRateDesc": "Addig használatos, amíg másik lejátszási sebességet nem választasz" } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index ff8d45617..5b7e6d15c 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -2155,5 +2155,9 @@ "removeFromQueue": "Hapus dari antrean", "musicDashboard": "Dasbor Musik", "quiet": "Pelan", - "loud": "Keras" + "loud": "Keras", + "rememberPlaybackRateTitle": "Ingat kecepatan pemutaran", + "rememberPlaybackRateDesc": "Gunakan kembali kecepatan pemutaran terakhir yang dipilih setelah menutup pemutar", + "defaultPlaybackRateTitle": "Kecepatan pemutaran default", + "defaultPlaybackRateDesc": "Digunakan sampai Anda memilih kecepatan yang berbeda" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 78d105394..01fcab48f 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1965,5 +1965,9 @@ "audioPlayerRepeatAll": "Ripeti tutto", "audioPlayerRepeatOff": "Ripetizione off", "audioPlayerRepeatOne": "Ripeti una volta", - "audioPlayerShuffle": "Shuffle" + "audioPlayerShuffle": "Shuffle", + "rememberPlaybackRateTitle": "Ricorda velocità di riproduzione", + "rememberPlaybackRateDesc": "Riutilizza l’ultima velocità di riproduzione selezionata dopo aver chiuso il lettore", + "defaultPlaybackRateTitle": "Velocità di riproduzione predefinita", + "defaultPlaybackRateDesc": "Viene usata finché non selezioni una velocità diversa" } diff --git a/lib/l10n/app_jp.arb b/lib/l10n/app_jp.arb index a55c54bef..c3bd7c54c 100644 --- a/lib/l10n/app_jp.arb +++ b/lib/l10n/app_jp.arb @@ -683,5 +683,10 @@ "shareQRCode": "QRコードをスキャン", "copyLoginLink": "リンクをコピー", "invalidAuthLink": "ログインリンクが無効です", - "homeBannerSlideshow": "スライドショー" + "homeBannerSlideshow": "スライドショー", + "playbackRate": "再生速度", + "rememberPlaybackRateTitle": "再生速度を記憶", + "rememberPlaybackRateDesc": "プレーヤーを閉じた後に最後に選択した再生速度を再利用します", + "defaultPlaybackRateTitle": "デフォルトの再生速度", + "defaultPlaybackRateDesc": "別の再生速度を選択するまで使用されます" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 9dd67579a..b38736423 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -784,5 +784,10 @@ "example": "2" } } - } + }, + "playbackRate": "재생 속도", + "rememberPlaybackRateTitle": "재생 속도 기억", + "rememberPlaybackRateDesc": "플레이어를 닫은 뒤 마지막으로 선택한 재생 속도를 다시 사용합니다", + "defaultPlaybackRateTitle": "기본 재생 속도", + "defaultPlaybackRateDesc": "다른 재생 속도를 선택할 때까지 사용됩니다" } diff --git a/lib/l10n/app_mr.arb b/lib/l10n/app_mr.arb index ca7c48e63..7d03c24a1 100644 --- a/lib/l10n/app_mr.arb +++ b/lib/l10n/app_mr.arb @@ -1272,5 +1272,9 @@ } } }, - "today": "आज" + "today": "आज", + "rememberPlaybackRateTitle": "प्लेबॅक दर लक्षात ठेवा", + "rememberPlaybackRateDesc": "प्लेअर बंद केल्यानंतर शेवटची निवडलेली प्लेबॅक दर पुन्हा वापरा", + "defaultPlaybackRateTitle": "डीफॉल्ट प्लेबॅक दर", + "defaultPlaybackRateDesc": "आपण वेगवेगळा प्लेबॅक दर निवडत नाही तोपर्यंत वापरला जातो" } diff --git a/lib/l10n/app_mt.arb b/lib/l10n/app_mt.arb index bb50309e0..66df7ae5f 100644 --- a/lib/l10n/app_mt.arb +++ b/lib/l10n/app_mt.arb @@ -1293,5 +1293,9 @@ "videoScalingFitHeight": "Għoli it-Tul", "@videoScalingFitHeight": {}, "videoScalingFitWidth": "Għoli il-wisa'", - "@videoScalingFitWidth": {} + "@videoScalingFitWidth": {}, + "rememberPlaybackRateTitle": "Ftakar ir-rata tal-playback", + "rememberPlaybackRateDesc": "Erġa’ uża l-aħħar rata tal-playback magħżula wara li tagħlaq il-plejer", + "defaultPlaybackRateTitle": "Rata tal-playback default", + "defaultPlaybackRateDesc": "Tintuża sakemm tagħżel rata oħra tal-playback" } diff --git a/lib/l10n/app_nb.arb b/lib/l10n/app_nb.arb index 22bc83523..354f86a47 100644 --- a/lib/l10n/app_nb.arb +++ b/lib/l10n/app_nb.arb @@ -940,5 +940,10 @@ "settingsPlayerMobileWarning": "Å skru på maskinvareaskelerasjon og systemspesifikke libass-undertekster på Android kan føre til at noen undertekster ikke vises.", "@settingsPlayerMobileWarning": {}, "nativeName": "Norsk bokmål", - "@nativeName": {} + "@nativeName": {}, + "playbackRate": "Avspillingshastighet", + "rememberPlaybackRateTitle": "Husk avspillingshastighet", + "rememberPlaybackRateDesc": "Bruk igjen den sist valgte avspillingshastigheten etter at spilleren lukkes", + "defaultPlaybackRateTitle": "Standard avspillingshastighet", + "defaultPlaybackRateDesc": "Brukes til du velger en annen avspillingshastighet" } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 3f67a4946..7f7c9a6eb 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1941,5 +1941,9 @@ } } }, - "anyLanguage": "Een taal" + "anyLanguage": "Een taal", + "rememberPlaybackRateTitle": "Onthoud afspeelsnelheid", + "rememberPlaybackRateDesc": "Hergebruik de laatst gekozen afspeelsnelheid na het sluiten van de speler", + "defaultPlaybackRateTitle": "Standaard afspeelsnelheid", + "defaultPlaybackRateDesc": "Wordt gebruikt tot je een andere afspeelsnelheid kiest" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index b8ee4074d..4462c796a 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2058,5 +2058,9 @@ } } }, - "settingsPlayerPlayPauseFadeTitle": "Zanikanie głośności przy pauzie" + "settingsPlayerPlayPauseFadeTitle": "Zanikanie głośności przy pauzie", + "rememberPlaybackRateTitle": "Zapamiętaj szybkość odtwarzania", + "rememberPlaybackRateDesc": "Ponownie użyj ostatnio wybranej szybkości odtwarzania po zamknięciu odtwarzacza", + "defaultPlaybackRateTitle": "Domyślna szybkość odtwarzania", + "defaultPlaybackRateDesc": "Używane do momentu wybrania innej szybkości odtwarzania" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fab51e528..c6274498e 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1919,5 +1919,9 @@ "seerrSeries": "Descobrir Séries", "today": "Hoje", "anyLanguage": "Qualquer idioma", - "enableEdgeGesturesTitle": "Ativar gestos de borda" + "enableEdgeGesturesTitle": "Ativar gestos de borda", + "rememberPlaybackRateTitle": "Lembrar velocidade de reprodução", + "rememberPlaybackRateDesc": "Reutilizar a última velocidade de reprodução selecionada após fechar o reprodutor", + "defaultPlaybackRateTitle": "Velocidade de reprodução predefinida", + "defaultPlaybackRateDesc": "Usada até selecionar uma velocidade diferente" } diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index 5c3ef88e3..621b6c684 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -1921,5 +1921,9 @@ "example": "1" } } - } + }, + "rememberPlaybackRateTitle": "Lembrar velocidade de reprodução", + "rememberPlaybackRateDesc": "Reutilize a última velocidade de reprodução selecionada após fechar o player", + "defaultPlaybackRateTitle": "Velocidade de reprodução padrão", + "defaultPlaybackRateDesc": "Usada até você selecionar uma velocidade diferente" } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index 6ffd3dc90..15d780e26 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -1231,5 +1231,9 @@ "television": "Televizor", "@television": {}, "layoutModeDual": "Dual", - "@layoutModeDual": {} + "@layoutModeDual": {}, + "rememberPlaybackRateTitle": "Ține minte rata de redare", + "rememberPlaybackRateDesc": "Reutilizează ultima rată de redare selectată după închiderea playerului", + "defaultPlaybackRateTitle": "Rată de redare implicită", + "defaultPlaybackRateDesc": "Se folosește până când selectezi o altă rată de redare" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 703276aac..944720427 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -2019,5 +2019,9 @@ "audioPlayerRepeatAll": "Повторить все", "syncPlaylistDeleteTitle": "Удалить синхронизированный плейлист", "playerSettingsReplayGainLevelTitle": "Уровень ReplayGain", - "removeFromQueue": "Удалить из очереди" + "removeFromQueue": "Удалить из очереди", + "rememberPlaybackRateTitle": "Запомнить скорость воспроизведения", + "rememberPlaybackRateDesc": "Повторно использовать последнюю выбранную скорость воспроизведения после закрытия плеера", + "defaultPlaybackRateTitle": "Скорость воспроизведения по умолчанию", + "defaultPlaybackRateDesc": "Используется до тех пор, пока вы не выберете другую скорость воспроизведения" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d16cae425..df07e444d 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -698,5 +698,10 @@ "copyLoginLink": "Kopírovať link", "includePassword": "Zahrnúť heslo", "invalidAuthLink": "Nesprávna adresa prihlásenia", - "gestures": "Gestá" + "gestures": "Gestá", + "playbackRate": "Rýchlosť prehrávania", + "rememberPlaybackRateTitle": "Zapamätať rýchlosť prehrávania", + "rememberPlaybackRateDesc": "Po zatvorení prehrávača znovu použiť poslednú vybranú rýchlosť prehrávania", + "defaultPlaybackRateTitle": "Predvolená rýchlosť prehrávania", + "defaultPlaybackRateDesc": "Použije sa, kým nevyberiete inú rýchlosť prehrávania" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index a77f56d6f..696e6666a 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -572,5 +572,10 @@ "contentRating": "Innehållsbetyg", "settingsBlurredPlaceholderDesc": "Visa suddig bakgrund när du laddar affischer", "settingsContinue": "Fortsätt", - "settingsHomeBannerDescription": "Visa som en bildspel, karusell eller dölja bannern" + "settingsHomeBannerDescription": "Visa som en bildspel, karusell eller dölja bannern", + "playbackRate": "Uppspelningshastighet", + "rememberPlaybackRateTitle": "Kom ihåg uppspelningshastighet", + "rememberPlaybackRateDesc": "Återanvänd den senast valda uppspelningshastigheten efter att spelaren stängs", + "defaultPlaybackRateTitle": "Standarduppspelningshastighet", + "defaultPlaybackRateDesc": "Används tills du väljer en annan uppspelningshastighet" } diff --git a/lib/l10n/app_ta.arb b/lib/l10n/app_ta.arb index 7c288bc73..ba9c944ea 100644 --- a/lib/l10n/app_ta.arb +++ b/lib/l10n/app_ta.arb @@ -1961,5 +1961,9 @@ "quiet": "அமைதியான", "loud": "சத்தமாக", "share": "பங்கு", - "setAs": "என அமைக்கவும்" + "setAs": "என அமைக்கவும்", + "rememberPlaybackRateTitle": "பின்னணி வீதத்தை நினைவில் கொள்", + "rememberPlaybackRateDesc": "பிளேயரை மூடிய பிறகு கடைசியாகத் தேர்ந்தெடுத்த பின்னணி வீதத்தை மீண்டும் பயன்படுத்து", + "defaultPlaybackRateTitle": "இயல்புநிலை பின்னணி வீதம்", + "defaultPlaybackRateDesc": "நீங்கள் வேறொரு பின்னணி வீதத்தைத் தேர்ந்தெடுக்கும் வரை இது பயன்படுத்தப்படும்" } diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 0967ef424..0218785ee 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -1 +1,7 @@ -{} +{ + "playbackRate": "อัตราการเล่น", + "rememberPlaybackRateTitle": "จำอัตราการเล่น", + "rememberPlaybackRateDesc": "ใช้อัตราการเล่นที่เลือกล่าสุดอีกครั้งหลังจากปิดตัวเล่น", + "defaultPlaybackRateTitle": "อัตราการเล่นเริ่มต้น", + "defaultPlaybackRateDesc": "ใช้จนกว่าคุณจะเลือกอัตราการเล่นอื่น" +} diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 0e9da4a0a..c8893eb70 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -1739,5 +1739,9 @@ "removeFromRadarr": "Radarr'dan kaldır", "markAllSeasonsAsAvailable": "Tüm sezonları mevcut olarak işaretle", "markAsAvailable": "Mevcut olarak işaretle", - "deleteData": "Verileri sil" + "deleteData": "Verileri sil", + "rememberPlaybackRateTitle": "Oynatma hızını hatırla", + "rememberPlaybackRateDesc": "Oynatıcı kapatıldıktan sonra son seçilen oynatma hızını tekrar kullan", + "defaultPlaybackRateTitle": "Varsayılan oynatma hızı", + "defaultPlaybackRateDesc": "Başka bir oynatma hızı seçene kadar kullanılır" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ffbbca887..da24f968b 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1814,5 +1814,9 @@ "type": "int" } } - } + }, + "rememberPlaybackRateTitle": "Запам’ятати швидкість відтворення", + "rememberPlaybackRateDesc": "Повторно використовувати останню вибрану швидкість відтворення після закриття плеєра", + "defaultPlaybackRateTitle": "Швидкість відтворення за замовчуванням", + "defaultPlaybackRateDesc": "Використовується, доки ви не оберете іншу швидкість відтворення" } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index b7ec20c4b..938ce2bdc 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -1551,5 +1551,9 @@ "settingsLocalUrlSetTitle": "Cấu hình URL cục bộ", "@settingsLocalUrlSetTitle": {}, "settingsLocalUrlSetDesc": "Chỉ định địa chỉ máy chủ cục bộ. Fladder sẽ tự dùng URL này khi thiết bị của ông ở trong cùng mạng.", - "@settingsLocalUrlSetDesc": {} + "@settingsLocalUrlSetDesc": {}, + "rememberPlaybackRateTitle": "Nhớ tốc độ phát", + "rememberPlaybackRateDesc": "Tái sử dụng tốc độ phát cuối cùng đã chọn sau khi đóng trình phát", + "defaultPlaybackRateTitle": "Tốc độ phát mặc định", + "defaultPlaybackRateDesc": "Được sử dụng cho đến khi bạn chọn một tốc độ phát khác" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 32da3ee7f..e3828c011 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2091,5 +2091,9 @@ } }, "playItems": "播放项目", - "shuffleItems": "随机排列项目" + "shuffleItems": "随机排列项目", + "rememberPlaybackRateTitle": "记住播放速度", + "rememberPlaybackRateDesc": "关闭播放器后重新使用上次选择的播放速度", + "defaultPlaybackRateTitle": "默认播放速度", + "defaultPlaybackRateDesc": "在你选择其他播放速度之前会一直使用" } diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 889ec18b7..bff67397c 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -1856,5 +1856,9 @@ "hidePlayedInLatestTitle": "隱藏已觀看內容", "hidePlayedInLatestDescription": "隱藏「最近新增的影片」中已觀看的內容", "groupedFoldersTitle": "分組媒體庫", - "groupedFoldersDescription": "自動將以下媒體庫的內容分組為「電影」、「音樂」和「電視節目」等檢視項目。" + "groupedFoldersDescription": "自動將以下媒體庫的內容分組為「電影」、「音樂」和「電視節目」等檢視項目。", + "rememberPlaybackRateTitle": "記住播放速度", + "rememberPlaybackRateDesc": "關閉播放器後重新使用上次選擇的播放速度", + "defaultPlaybackRateTitle": "預設播放速度", + "defaultPlaybackRateDesc": "在您選擇其他播放速度之前會一直使用" } diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index 466ab647c..89f8ec938 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -100,6 +100,9 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(Screensaver.logo) Screensaver screensaver, @Default(false) bool enableSpeedBoost, @Default(2.0) double speedBoostRate, + @Default(false) bool rememberPlaybackRate, + @Default(1.0) double defaultPlaybackRate, + double? lastPlaybackRate, @Default(true) bool enableDoubleTapSeek, @Default(false) bool enableAdvancedVideoOptions, @Default(true) bool enableEdgeGestures, @@ -115,6 +118,30 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { double get volume => internalVolume; + double get effectivePlaybackRate { + if (!rememberPlaybackRate) return 1.0; + return clampPlaybackRate(lastPlaybackRate ?? defaultPlaybackRate); + } + + VideoPlayerSettingsModel withPlaybackRatePersistence(bool remember) { + return copyWith( + rememberPlaybackRate: remember, + lastPlaybackRate: null, + ); + } + + VideoPlayerSettingsModel withDefaultPlaybackRate(double rate) { + return copyWith( + defaultPlaybackRate: clampPlaybackRate(rate), + lastPlaybackRate: null, + ); + } + + VideoPlayerSettingsModel withLastPlaybackRate(double rate) { + if (!rememberPlaybackRate) return this; + return copyWith(lastPlaybackRate: clampPlaybackRate(rate)); + } + factory VideoPlayerSettingsModel.fromJson(Map json) => _$VideoPlayerSettingsModelFromJson(json); PlayerOptions get wantedPlayer => @@ -166,6 +193,8 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { } } +double clampPlaybackRate(double rate) => rate.clamp(0.25, 3.0).toDouble(); + enum PlayerOptions { libMDK, libMPV, diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index 61568659c..3e79111cd 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -33,6 +33,9 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { Screensaver get screensaver; bool get enableSpeedBoost; double get speedBoostRate; + bool get rememberPlaybackRate; + double get defaultPlaybackRate; + double? get lastPlaybackRate; bool get enableDoubleTapSeek; bool get enableAdvancedVideoOptions; bool get enableEdgeGestures; @@ -78,6 +81,9 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('screensaver', screensaver)) ..add(DiagnosticsProperty('enableSpeedBoost', enableSpeedBoost)) ..add(DiagnosticsProperty('speedBoostRate', speedBoostRate)) + ..add(DiagnosticsProperty('rememberPlaybackRate', rememberPlaybackRate)) + ..add(DiagnosticsProperty('defaultPlaybackRate', defaultPlaybackRate)) + ..add(DiagnosticsProperty('lastPlaybackRate', lastPlaybackRate)) ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty('enableAdvancedVideoOptions', enableAdvancedVideoOptions)) ..add(DiagnosticsProperty('enableEdgeGestures', enableEdgeGestures)) @@ -93,7 +99,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs, ambientBlur: $ambientBlur)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, rememberPlaybackRate: $rememberPlaybackRate, defaultPlaybackRate: $defaultPlaybackRate, lastPlaybackRate: $lastPlaybackRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs, ambientBlur: $ambientBlur)'; } } @@ -123,6 +129,9 @@ abstract mixin class $VideoPlayerSettingsModelCopyWith<$Res> { Screensaver screensaver, bool enableSpeedBoost, double speedBoostRate, + bool rememberPlaybackRate, + double defaultPlaybackRate, + double? lastPlaybackRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, bool enableEdgeGestures, @@ -167,6 +176,9 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> implements $VideoPlayerSettin Object? screensaver = null, Object? enableSpeedBoost = null, Object? speedBoostRate = null, + Object? rememberPlaybackRate = null, + Object? defaultPlaybackRate = null, + Object? lastPlaybackRate = freezed, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, Object? enableEdgeGestures = null, @@ -256,6 +268,18 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> implements $VideoPlayerSettin ? _self.speedBoostRate : speedBoostRate // ignore: cast_nullable_to_non_nullable as double, + rememberPlaybackRate: null == rememberPlaybackRate + ? _self.rememberPlaybackRate + : rememberPlaybackRate // ignore: cast_nullable_to_non_nullable + as bool, + defaultPlaybackRate: null == defaultPlaybackRate + ? _self.defaultPlaybackRate + : defaultPlaybackRate // ignore: cast_nullable_to_non_nullable + as double, + lastPlaybackRate: freezed == lastPlaybackRate + ? _self.lastPlaybackRate + : lastPlaybackRate // ignore: cast_nullable_to_non_nullable + as double?, enableDoubleTapSeek: null == enableDoubleTapSeek ? _self.enableDoubleTapSeek : enableDoubleTapSeek // ignore: cast_nullable_to_non_nullable @@ -417,6 +441,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { Screensaver screensaver, bool enableSpeedBoost, double speedBoostRate, + bool rememberPlaybackRate, + double defaultPlaybackRate, + double? lastPlaybackRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, bool enableEdgeGestures, @@ -454,6 +481,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.screensaver, _that.enableSpeedBoost, _that.speedBoostRate, + _that.rememberPlaybackRate, + _that.defaultPlaybackRate, + _that.lastPlaybackRate, _that.enableDoubleTapSeek, _that.enableAdvancedVideoOptions, _that.enableEdgeGestures, @@ -505,6 +535,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { Screensaver screensaver, bool enableSpeedBoost, double speedBoostRate, + bool rememberPlaybackRate, + double defaultPlaybackRate, + double? lastPlaybackRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, bool enableEdgeGestures, @@ -541,6 +574,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.screensaver, _that.enableSpeedBoost, _that.speedBoostRate, + _that.rememberPlaybackRate, + _that.defaultPlaybackRate, + _that.lastPlaybackRate, _that.enableDoubleTapSeek, _that.enableAdvancedVideoOptions, _that.enableEdgeGestures, @@ -591,6 +627,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { Screensaver screensaver, bool enableSpeedBoost, double speedBoostRate, + bool rememberPlaybackRate, + double defaultPlaybackRate, + double? lastPlaybackRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, bool enableEdgeGestures, @@ -627,6 +666,9 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.screensaver, _that.enableSpeedBoost, _that.speedBoostRate, + _that.rememberPlaybackRate, + _that.defaultPlaybackRate, + _that.lastPlaybackRate, _that.enableDoubleTapSeek, _that.enableAdvancedVideoOptions, _that.enableEdgeGestures, @@ -667,6 +709,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel with Diagnostic this.screensaver = Screensaver.logo, this.enableSpeedBoost = false, this.speedBoostRate = 2.0, + this.rememberPlaybackRate = false, + this.defaultPlaybackRate = 1.0, + this.lastPlaybackRate, this.enableDoubleTapSeek = true, this.enableAdvancedVideoOptions = false, this.enableEdgeGestures = true, @@ -759,6 +804,14 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel with Diagnostic final double speedBoostRate; @override @JsonKey() + final bool rememberPlaybackRate; + @override + @JsonKey() + final double defaultPlaybackRate; + @override + final double? lastPlaybackRate; + @override + @JsonKey() final bool enableDoubleTapSeek; @override @JsonKey() @@ -829,6 +882,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel with Diagnostic ..add(DiagnosticsProperty('screensaver', screensaver)) ..add(DiagnosticsProperty('enableSpeedBoost', enableSpeedBoost)) ..add(DiagnosticsProperty('speedBoostRate', speedBoostRate)) + ..add(DiagnosticsProperty('rememberPlaybackRate', rememberPlaybackRate)) + ..add(DiagnosticsProperty('defaultPlaybackRate', defaultPlaybackRate)) + ..add(DiagnosticsProperty('lastPlaybackRate', lastPlaybackRate)) ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty('enableAdvancedVideoOptions', enableAdvancedVideoOptions)) ..add(DiagnosticsProperty('enableEdgeGestures', enableEdgeGestures)) @@ -844,7 +900,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel with Diagnostic @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs, ambientBlur: $ambientBlur)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, rememberPlaybackRate: $rememberPlaybackRate, defaultPlaybackRate: $defaultPlaybackRate, lastPlaybackRate: $lastPlaybackRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs, ambientBlur: $ambientBlur)'; } } @@ -875,6 +931,9 @@ abstract mixin class _$VideoPlayerSettingsModelCopyWith<$Res> implements $VideoP Screensaver screensaver, bool enableSpeedBoost, double speedBoostRate, + bool rememberPlaybackRate, + double defaultPlaybackRate, + double? lastPlaybackRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, bool enableEdgeGestures, @@ -919,6 +978,9 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> implements _$VideoPlayerSett Object? screensaver = null, Object? enableSpeedBoost = null, Object? speedBoostRate = null, + Object? rememberPlaybackRate = null, + Object? defaultPlaybackRate = null, + Object? lastPlaybackRate = freezed, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, Object? enableEdgeGestures = null, @@ -1008,6 +1070,18 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> implements _$VideoPlayerSett ? _self.speedBoostRate : speedBoostRate // ignore: cast_nullable_to_non_nullable as double, + rememberPlaybackRate: null == rememberPlaybackRate + ? _self.rememberPlaybackRate + : rememberPlaybackRate // ignore: cast_nullable_to_non_nullable + as bool, + defaultPlaybackRate: null == defaultPlaybackRate + ? _self.defaultPlaybackRate + : defaultPlaybackRate // ignore: cast_nullable_to_non_nullable + as double, + lastPlaybackRate: freezed == lastPlaybackRate + ? _self.lastPlaybackRate + : lastPlaybackRate // ignore: cast_nullable_to_non_nullable + as double?, enableDoubleTapSeek: null == enableDoubleTapSeek ? _self.enableDoubleTapSeek : enableDoubleTapSeek // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index ea3c322c1..d8b7c6ece 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -35,6 +35,9 @@ _VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson(Map _$VideoPlayerSettingsModelToJson(_VideoPlayerSettingsModel 'screensaver': _$ScreensaverEnumMap[instance.screensaver]!, 'enableSpeedBoost': instance.enableSpeedBoost, 'speedBoostRate': instance.speedBoostRate, + 'rememberPlaybackRate': instance.rememberPlaybackRate, + 'defaultPlaybackRate': instance.defaultPlaybackRate, + 'lastPlaybackRate': instance.lastPlaybackRate, 'enableDoubleTapSeek': instance.enableDoubleTapSeek, 'enableAdvancedVideoOptions': instance.enableAdvancedVideoOptions, 'enableEdgeGestures': instance.enableEdgeGestures, diff --git a/lib/providers/settings/pigeon_player_settings_provider.dart b/lib/providers/settings/pigeon_player_settings_provider.dart index 0a896acd0..255155d56 100644 --- a/lib/providers/settings/pigeon_player_settings_provider.dart +++ b/lib/providers/settings/pigeon_player_settings_provider.dart @@ -62,6 +62,7 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds, skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds, fillScreen: value.fillScreen, + playbackRate: value.effectivePlaybackRate, videoFit: switch (value.videoFit) { BoxFit.fill => pigeon.VideoPlayerFit.fill, BoxFit.contain => pigeon.VideoPlayerFit.contain, diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index 529f5c5ba..b3fe16f8a 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -119,8 +120,32 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier setPlaybackRate( + double value, { + bool persistLastUsed = true, + bool applyToPlayer = true, + }) async { + final clampedValue = clampPlaybackRate(value); + ref.read(playbackRateProvider.notifier).state = clampedValue; + + if (persistLastUsed && state.rememberPlaybackRate) { + state = state.withLastPlaybackRate(clampedValue); + } + + if (applyToPlayer) { + await ref.read(videoPlayerProvider).applyPlaybackRate(clampedValue); + } + } + + void setRememberPlaybackRate(bool value) { + state = state.withPlaybackRatePersistence(value); + } + + void setDefaultPlaybackRate(double value) { + state = state.withDefaultPlaybackRate(value); } void toggleOrientation(Set? orientation) => diff --git a/lib/providers/video_player_provider.dart b/lib/providers/video_player_provider.dart index 904bc6484..e1e3d6db6 100644 --- a/lib/providers/video_player_provider.dart +++ b/lib/providers/video_player_provider.dart @@ -118,7 +118,9 @@ class VideoPlayerNotifier extends StateNotifier { Future loadPlaybackItem(PlaybackModel model, Duration startPosition) async { ref.read(playBackModel)?.dispose(); await state.stop(); - ref.read(playbackRateProvider.notifier).state = 1.0; + final playbackSettings = ref.read(videoPlayerSettingsProvider); + final initialPlaybackRate = playbackSettings.effectivePlaybackRate; + ref.read(playbackRateProvider.notifier).state = initialPlaybackRate; final useMinimizedPlayer = model.item.type == FladderItemType.audio || model.mediaStreams?.videoStreams.isEmpty == true; @@ -139,6 +141,9 @@ class VideoPlayerNotifier extends StateNotifier { ref.read(playBackModel.notifier).update((state) => newPlaybackModel); await state.loadVideo(model, effectiveStartPosition, true); await state.setVolume(ref.read(videoPlayerSettingsProvider).volume); + if (playbackSettings.rememberPlaybackRate) { + await state.applyPlaybackRate(initialPlaybackRate); + } await state.setAudioTrack(null, model); await state.setSubtitleTrack(null, model); @@ -178,7 +183,9 @@ class VideoPlayerNotifier extends StateNotifier { final effectiveStartPosition = await queuedModel.resolvedStartPosition(startPosition); ref.read(playBackModel.notifier).update((state) => queuedModel); - ref.read(playbackRateProvider.notifier).state = 1.0; + final videoSettings = ref.read(videoPlayerSettingsProvider); + final initialPlaybackRate = videoSettings.effectivePlaybackRate; + ref.read(playbackRateProvider.notifier).state = initialPlaybackRate; mediaState.update((state) => state.copyWith( state: keepFullScreenLayout ? VideoPlayerState.fullScreen : VideoPlayerState.minimized, @@ -191,6 +198,9 @@ class VideoPlayerNotifier extends StateNotifier { await state.loadAudioQueue(queue, currentIndex, effectiveStartPosition, true); await state.setVolume(ref.read(videoPlayerSettingsProvider).volume); + if (videoSettings.rememberPlaybackRate) { + await state.applyPlaybackRate(initialPlaybackRate); + } mediaState.update((state) => state.copyWith( buffering: false, diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 1cf040276..b8222beae 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -47,7 +47,7 @@ class _PlayerSettingsPageState extends ConsumerState { final connectionState = ref.watch(connectivityStatusProvider); final userSettings = ref.watch(userProvider.select((value) => value?.userSettings)); - + final playbackRateValue = videoSettings.lastPlaybackRate ?? videoSettings.defaultPlaybackRate; final currentPlayer = videoSettings.wantedPlayer; final crossfadeSupported = videoSettings.canUseCrossfade; @@ -143,6 +143,60 @@ class _PlayerSettingsPageState extends ConsumerState { ], ), const SizedBox(height: 12), + ...settingsListGroup( + context, + SettingsLabelDivider(label: context.localized.playbackRate), + [ + SettingsListTile( + label: Text(context.localized.rememberPlaybackRateTitle), + subLabel: Text(context.localized.rememberPlaybackRateDesc), + onTap: () => provider.setRememberPlaybackRate(!videoSettings.rememberPlaybackRate), + trailing: Switch( + value: videoSettings.rememberPlaybackRate, + onChanged: provider.setRememberPlaybackRate, + ), + ), + if (videoSettings.rememberPlaybackRate) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.localized.defaultPlaybackRateTitle, + style: Theme.of(context).textTheme.titleLarge, + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + context.localized.defaultPlaybackRateDesc, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Row( + children: [ + Expanded( + child: FladderSlider( + min: 0.25, + max: 3.0, + value: playbackRateValue, + divisions: 55, + onChanged: provider.setDefaultPlaybackRate, + ), + ), + const SizedBox(width: 12), + Text( + '${playbackRateValue.toStringAsFixed(2)}x', + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( (entry) => SettingsListTileEnum( diff --git a/lib/screens/video_player/components/video_player_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index 12d9658d2..de30a5bc6 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -492,7 +492,6 @@ Future showPlaybackSpeed(BuildContext context) { return StatefulBuilder(builder: (context, setState) { return Consumer( builder: (context, ref, child) { - final player = ref.watch(videoPlayerProvider); final lastSpeed = ref.watch(playbackRateProvider); return SimpleDialog( contentPadding: const EdgeInsets.only(top: 8, bottom: 24), @@ -516,8 +515,7 @@ Future showPlaybackSpeed(BuildContext context) { value: lastSpeed, divisions: 55, onChanged: (value) { - ref.read(playbackRateProvider.notifier).state = value; - player.setSpeed(value); + ref.read(videoPlayerSettingsProvider.notifier).setPlaybackRate(value); }, ), ), diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index a119adb7a..3c23c3eb8 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -820,8 +820,9 @@ class _DesktopControlsState extends ConsumerState { _originalSpeed = ref.read(playbackRateProvider); _speedBoostActive = true; - ref.read(videoPlayerProvider).setSpeed(settings.speedBoostRate); - ref.read(playbackRateProvider.notifier).state = settings.speedBoostRate; + unawaited( + ref.read(videoPlayerSettingsProvider.notifier).setPlaybackRate(settings.speedBoostRate, persistLastUsed: false), + ); } void _deactivateSpeedBoost() { @@ -829,8 +830,9 @@ class _DesktopControlsState extends ConsumerState { _speedBoostActive = false; if (_originalSpeed != null) { - ref.read(videoPlayerProvider).setSpeed(_originalSpeed!); - ref.read(playbackRateProvider.notifier).state = _originalSpeed!; + unawaited( + ref.read(videoPlayerSettingsProvider.notifier).setPlaybackRate(_originalSpeed!, persistLastUsed: false), + ); _originalSpeed = null; } } diff --git a/lib/src/player_settings_helper.g.dart b/lib/src/player_settings_helper.g.dart index 2b44241d0..8dc72d2ba 100644 --- a/lib/src/player_settings_helper.g.dart +++ b/lib/src/player_settings_helper.g.dart @@ -14,21 +14,19 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } + bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { - return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key])); } return a == b; } - enum Screensaver { disabled, dvd, @@ -87,6 +85,7 @@ class PlayerSettings { required this.fillScreen, required this.videoFit, required this.screensaver, + required this.playbackRate, }); bool enableTunneling; @@ -109,6 +108,8 @@ class PlayerSettings { Screensaver screensaver; + double playbackRate; + List _toList() { return [ enableTunneling, @@ -121,11 +122,13 @@ class PlayerSettings { fillScreen, videoFit, screensaver, + playbackRate, ]; } Object encode() { - return _toList(); } + return _toList(); + } static PlayerSettings decode(Object result) { result as List; @@ -140,6 +143,7 @@ class PlayerSettings { fillScreen: result[7]! as bool, videoFit: result[8]! as VideoPlayerFit, screensaver: result[9]! as Screensaver, + playbackRate: result[10]! as double, ); } @@ -157,11 +161,9 @@ class PlayerSettings { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -169,25 +171,25 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is Screensaver) { + } else if (value is Screensaver) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is VideoPlayerFit) { + } else if (value is VideoPlayerFit) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is PlayerOrientations) { + } else if (value is PlayerOrientations) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is AutoNextType) { + } else if (value is AutoNextType) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is SegmentType) { + } else if (value is SegmentType) { buffer.putUint8(133); writeValue(buffer, value.index); - } else if (value is SegmentSkip) { + } else if (value is SegmentSkip) { buffer.putUint8(134); writeValue(buffer, value.index); - } else if (value is PlayerSettings) { + } else if (value is PlayerSettings) { buffer.putUint8(135); writeValue(buffer, value.encode()); } else { @@ -198,25 +200,25 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final int? value = readValue(buffer) as int?; return value == null ? null : Screensaver.values[value]; - case 130: + case 130: final int? value = readValue(buffer) as int?; return value == null ? null : VideoPlayerFit.values[value]; - case 131: + case 131: final int? value = readValue(buffer) as int?; return value == null ? null : PlayerOrientations.values[value]; - case 132: + case 132: final int? value = readValue(buffer) as int?; return value == null ? null : AutoNextType.values[value]; - case 133: + case 133: final int? value = readValue(buffer) as int?; return value == null ? null : SegmentType.values[value]; - case 134: + case 134: final int? value = readValue(buffer) as int?; return value == null ? null : SegmentSkip.values[value]; - case 135: + case 135: return PlayerSettings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -238,15 +240,15 @@ class PlayerSettingsPigeon { final String pigeonVar_messageChannelSuffix; Future sendPlayerSettings(PlayerSettings playerSettings) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerSettings]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index 2300e3afb..2c9ca3368 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -110,7 +110,13 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro final player = switch (ref.read(videoPlayerSettingsProvider).wantedPlayer) { PlayerOptions.libMDK => LibMDK(), PlayerOptions.libMPV => LibMPV(), - PlayerOptions.nativePlayer => NativePlayer(), + PlayerOptions.nativePlayer => NativePlayer( + onPlaybackRateSelected: (speed) { + unawaited( + ref.read(videoPlayerSettingsProvider.notifier).setPlaybackRate(speed, applyToPlayer: false), + ); + }, + ), }; setup(player); @@ -564,9 +570,13 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro return super.seek(position); } + Future applyPlaybackRate(double speed) async { + await _player?.setSpeed(speed); + } + @override - Future setSpeed(double speed) { - _player?.setSpeed(speed); + Future setSpeed(double speed) async { + await ref.read(videoPlayerSettingsProvider.notifier).setPlaybackRate(speed); return super.setSpeed(speed); } diff --git a/lib/wrappers/players/native_player.dart b/lib/wrappers/players/native_player.dart index ffa249336..2bd0caa10 100644 --- a/lib/wrappers/players/native_player.dart +++ b/lib/wrappers/players/native_player.dart @@ -17,6 +17,9 @@ import 'package:fladder/wrappers/players/player_states.dart'; bool nativeActivityStarted = false; class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback { + NativePlayer({required this.onPlaybackRateSelected}); + + final void Function(double speed) onPlaybackRateSelected; final player = VideoPlayerApi(); final activity = NativeVideoActivity(); @@ -78,7 +81,7 @@ class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback { } @override - Future setSpeed(double speed) async {} + Future setSpeed(double speed) => player.setPlaybackSpeed(speed); @override Future setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async { @@ -118,6 +121,10 @@ class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback { _stateController.add(lastState); } + void onPlaybackRateChanged(double speed) { + onPlaybackRateSelected(speed); + } + final StreamController _stateController = StreamController.broadcast(); @override diff --git a/pigeons/player_settings_pigeon.dart b/pigeons/player_settings_pigeon.dart index 1953a0d6c..4bb3f582f 100644 --- a/pigeons/player_settings_pigeon.dart +++ b/pigeons/player_settings_pigeon.dart @@ -23,6 +23,7 @@ class PlayerSettings { final bool fillScreen; final VideoPlayerFit videoFit; final Screensaver screensaver; + final double playbackRate; const PlayerSettings({ required this.enableTunneling, @@ -35,6 +36,7 @@ class PlayerSettings { required this.fillScreen, required this.videoFit, required this.screensaver, + required this.playbackRate, }); } diff --git a/pigeons/video_player.dart b/pigeons/video_player.dart index 693228e86..c3304c8e7 100644 --- a/pigeons/video_player.dart +++ b/pigeons/video_player.dart @@ -315,6 +315,7 @@ class GuideProgram { @FlutterApi() abstract class VideoPlayerListenerCallback { void onPlaybackStateChanged(PlaybackState state); + void onPlaybackRateChanged(double speed); } @FlutterApi() diff --git a/test/playback_rate_settings_test.dart b/test/playback_rate_settings_test.dart new file mode 100644 index 000000000..d76e33fb9 --- /dev/null +++ b/test/playback_rate_settings_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:fladder/models/settings/video_player_settings.dart'; +import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/providers/shared_provider.dart'; + +void main() { + group('playback-rate settings model', () { + test('older persisted settings retain the existing behavior', () { + final settings = VideoPlayerSettingsModel.fromJson(const {}); + + expect(settings.rememberPlaybackRate, isFalse); + expect(settings.defaultPlaybackRate, 1.0); + expect(settings.lastPlaybackRate, isNull); + expect(settings.effectivePlaybackRate, 1.0); + }); + + test('effective rate uses the default and then the last selected rate', () { + final settings = VideoPlayerSettingsModel( + rememberPlaybackRate: true, + defaultPlaybackRate: 1.25, + ); + + expect(settings.effectivePlaybackRate, 1.25); + expect(settings.copyWith(lastPlaybackRate: 1.75).effectivePlaybackRate, 1.75); + }); + + test('disabled persistence always resolves to the existing 1.0x default', () { + final settings = VideoPlayerSettingsModel( + defaultPlaybackRate: 1.5, + lastPlaybackRate: 2.0, + ); + + expect(settings.effectivePlaybackRate, 1.0); + }); + + test('playback rates are clamped to the supported range', () { + expect(clampPlaybackRate(0.1), 0.25); + expect(clampPlaybackRate(1.5), 1.5); + expect(clampPlaybackRate(4.0), 3.0); + }); + }); + + group('playback-rate settings notifier', () { + late ProviderContainer container; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + final preferences = await SharedPreferences.getInstance(); + container = ProviderContainer( + overrides: [ + sharedPreferencesProvider.overrideWithValue(preferences), + ], + ); + }); + + tearDown(() => container.dispose()); + + test('default changes and disabling clear the remembered rate', () async { + final notifier = container.read(videoPlayerSettingsProvider.notifier); + + notifier.setRememberPlaybackRate(true); + await notifier.setPlaybackRate(2.0, applyToPlayer: false); + expect(container.read(videoPlayerSettingsProvider).lastPlaybackRate, 2.0); + + notifier.setDefaultPlaybackRate(1.5); + final changedDefault = container.read(videoPlayerSettingsProvider); + expect(changedDefault.defaultPlaybackRate, 1.5); + expect(changedDefault.lastPlaybackRate, isNull); + + await notifier.setPlaybackRate(2.5, applyToPlayer: false); + notifier.setRememberPlaybackRate(false); + final disabled = container.read(videoPlayerSettingsProvider); + expect(disabled.rememberPlaybackRate, isFalse); + expect(disabled.lastPlaybackRate, isNull); + }); + + test('ordinary changes persist only while the feature is enabled', () async { + final notifier = container.read(videoPlayerSettingsProvider.notifier); + + await notifier.setPlaybackRate(2.0, applyToPlayer: false); + expect(container.read(playbackRateProvider), 2.0); + expect(container.read(videoPlayerSettingsProvider).lastPlaybackRate, isNull); + + notifier.setRememberPlaybackRate(true); + await notifier.setPlaybackRate(4.0, applyToPlayer: false); + expect(container.read(playbackRateProvider), 3.0); + expect(container.read(videoPlayerSettingsProvider).lastPlaybackRate, 3.0); + + await notifier.setPlaybackRate( + 2.5, + persistLastUsed: false, + applyToPlayer: false, + ); + expect(container.read(playbackRateProvider), 2.5); + expect(container.read(videoPlayerSettingsProvider).lastPlaybackRate, 3.0); + }); + }); +}