diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9689bca070..8ee16d12f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -308,7 +308,7 @@ dependencies { // AY --> // mpv-android - implementation(aniyomilibs.aniyomi.mpv) + implementation(aniyomilibs.mpv.lib) // FFmpeg-kit implementation(aniyomilibs.ffmpeg.kit) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 221c03a115..275cd2ca30 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -17,7 +17,7 @@ -keep,allowoptimization class app.cash.quickjs.** { public protected *; } -keep,allowoptimization class uy.kohesive.injekt.** { public protected *; } # AY --> --keep,allowoptimization class is.xyz.mpv.** { public protected *; } +-keep,allowoptimization class is.xyz.mpv.** { *; } -keep,allowoptimization class com.arthenica.** { public protected *; } # <-- AY diff --git a/app/src/main/assets/aniyomi.lua b/app/src/main/assets/aniyomi.lua index 4654e2eac1..84136eeaec 100644 --- a/app/src/main/assets/aniyomi.lua +++ b/app/src/main/assets/aniyomi.lua @@ -80,6 +80,9 @@ end function aniyomi.int_picker(title, name_format, start, stop, step, property) mp.set_property("user-data/aniyomi/launch_int_picker", title .. "|" .. name_format .. "|" .. start .. "|" .. stop .. "|" .. step .. "|" .. property) end +function aniyomi.show_seek_text(value, text) + mp.set_property("user-data/aniyomi/show_seek_text", tostring(value) .. "|" .. text) +end -- Legacy function aniyomi.left_seek_by(value) aniyomi.seek_by(-value) diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 286d2d2c38..b33ab716c3 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -27,7 +27,7 @@ import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.SyncEpisodeProgressWithTrack import eu.kanade.domain.track.interactor.TrackEpisode -import eu.kanade.tachiyomi.ui.player.utils.TrackSelect +import eu.kanade.tachiyomi.ui.player.domain.TrackSelect import mihon.data.repository.ExtensionRepoRepositoryImpl import mihon.domain.episode.interactor.FilterEpisodesForDownload import mihon.domain.extensionrepo.interactor.CreateExtensionRepo diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt index 3583c19863..d46c7c6d0a 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt @@ -25,7 +25,7 @@ object PlayerSettingsDecoderScreen : SearchableSettings { val tryHw = decoderPreferences.tryHWDecoding() val useGpuNext = decoderPreferences.gpuNext() - val debanding = decoderPreferences.videoDebanding() + val debanding = decoderPreferences.debanding() val yuv420p = decoderPreferences.useYUV420P() return listOf( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt index 9cf6f19b75..ed67314fae 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt @@ -26,6 +26,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentMap import tachiyomi.i18n.MR +import tachiyomi.i18n.animiru.AMMR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState @@ -63,6 +64,10 @@ object PlayerSettingsPlayerScreen : SearchableSettings { preference = playerPreferences.preserveWatchingPosition(), title = stringResource(AYMR.strings.pref_preserve_watching_position), ), + Preference.PreferenceItem.SwitchPreference( + preference = playerPreferences.switchOnFailure(), + title = stringResource(AMMR.strings.player_pref_switch_on_failure), + ), Preference.PreferenceItem.ListPreference( preference = playerPreferences.defaultPlayerOrientationType(), entries = PlayerOrientation.entries.associateWith { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/custombutton/PlayerSettingsCustomButtonScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/custombutton/PlayerSettingsCustomButtonScreenModel.kt index f1bca08ff4..7ef83ddba0 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/custombutton/PlayerSettingsCustomButtonScreenModel.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/custombutton/PlayerSettingsCustomButtonScreenModel.kt @@ -146,21 +146,3 @@ sealed interface CustomButtonScreenState { get() = customButtons.isEmpty() } } - -sealed interface CustomButtonFetchState { - @Immutable - data object Loading : CustomButtonFetchState - - @Immutable - data class Success(val customButtons: ImmutableList) : CustomButtonFetchState - - @Immutable - data class Error(val errorMessage: String) : CustomButtonFetchState -} - -fun CustomButtonFetchState.getButtons(): ImmutableList { - return when (this) { - is CustomButtonFetchState.Success -> this.customButtons - else -> emptyList().toImmutableList() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index e994361601..b682630993 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -23,6 +23,8 @@ import eu.kanade.tachiyomi.network.JavaScriptEngine import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.AndroidSourceManager import eu.kanade.tachiyomi.ui.player.ExternalIntents +import eu.kanade.tachiyomi.ui.player.domain.AudioManager +import eu.kanade.tachiyomi.ui.player.domain.BrightnessManager import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf @@ -164,6 +166,11 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { GoogleDriveService(app) } // <-- AM (SYNC_DRIVE) + // AM --> + addSingletonFactory { AudioManager(app) } + addSingletonFactory { BrightnessManager(app) } + // <-- AM + // Asynchronously init expensive components for a faster cold start ContextCompat.getMainExecutor(app).execute { get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/AniyomiMPVView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/AniyomiMPVView.kt index 2556bb941f..4e8d30062f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/AniyomiMPVView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/AniyomiMPVView.kt @@ -32,13 +32,12 @@ import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences import `is`.xyz.mpv.BaseMPVView import `is`.xyz.mpv.KeyMapping -import `is`.xyz.mpv.MPVLib +import `is`.xyz.mpv.MPV import logcat.LogPriority import logcat.logcat import uy.kohesive.injekt.injectLazy -import kotlin.reflect.KProperty -class AniyomiMPVView(context: Context, attributes: AttributeSet) : BaseMPVView(context, attributes) { +class AniyomiMPVView(context: Context, attributes: AttributeSet?) : BaseMPVView(context, attributes) { private val playerPreferences: PlayerPreferences by injectLazy() private val decoderPreferences: DecoderPreferences by injectLazy() @@ -49,121 +48,77 @@ class AniyomiMPVView(context: Context, attributes: AttributeSet) : BaseMPVView(c var isExiting = false - private fun getPropertyInt(property: String): Int? { - return MPVLib.getPropertyInt(property) as Int? - } - - private fun getPropertyBoolean(property: String): Boolean? { - return MPVLib.getPropertyBoolean(property) as Boolean? - } - - private fun getPropertyDouble(property: String): Double? { - return MPVLib.getPropertyDouble(property) as Double? - } - - private fun getPropertyString(property: String): String? { - return MPVLib.getPropertyString(property) as String? - } - - val duration: Int? - get() = getPropertyInt("duration") - - var timePos: Int? - get() = getPropertyInt("time-pos") - set(position) = MPVLib.setPropertyInt("time-pos", position!!) - - var paused: Boolean? - get() = getPropertyBoolean("pause") - set(paused) = MPVLib.setPropertyBoolean("pause", paused!!) - - val hwdecActive: String - get() = getPropertyString("hwdec-current") ?: "no" - - val videoH: Int? - get() = getPropertyInt("video-params/h") - /** * Returns the video aspect ratio. Rotation is taken into account. */ fun getVideoOutAspect(): Double? { - return getPropertyDouble("video-params/aspect")?.let { + return mpv?.getPropertyDouble("video-params/aspect")?.let { if (it < 0.001) return 0.0 - if ((getPropertyInt("video-params/rotate") ?: 0) % 180 == 90) 1.0 / it else it + if ((mpv?.getPropertyInt("video-params/rotate") ?: 0) % 180 == 90) 1.0 / it else it } } - inner class TrackDelegate(private val name: String) { - operator fun getValue(thisRef: Any?, property: KProperty<*>): Int { - val v = getPropertyString(name) - // we can get null here for "no" or other invalid value - return v?.toIntOrNull() ?: -1 - } - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { - if (value == -1) { - MPVLib.setPropertyString(name, "no") - } else { - MPVLib.setPropertyInt(name, value) - } - } - } - - var sid: Int by TrackDelegate("sid") - var secondarySid: Int by TrackDelegate("secondary-sid") - var aid: Int by TrackDelegate("aid") - - override fun initOptions(vo: String) { + fun init(mpvInst: MPV) { + this.mpv = mpvInst setVo(if (decoderPreferences.gpuNext().get()) "gpu-next" else "gpu") - MPVLib.setPropertyBoolean("pause", true) - MPVLib.setOptionString("profile", "fast") - MPVLib.setOptionString("hwdec", if (decoderPreferences.tryHWDecoding().get()) "auto" else "no") - when (decoderPreferences.videoDebanding().get()) { - Debanding.None -> {} - Debanding.CPU -> MPVLib.setOptionString("vf", "gradfun=radius=12") - Debanding.GPU -> MPVLib.setOptionString("deband", "yes") - } + mpv?.setPropertyBoolean("pause", true) + mpv?.setOptionString("profile", "fast") + mpv?.setOptionString("hwdec", if (decoderPreferences.tryHWDecoding().get()) "auto" else "no") if (decoderPreferences.useYUV420P().get()) { - MPVLib.setOptionString("vf", "format=yuv420p") + mpv?.setOptionString("vf", "format=yuv420p") } - MPVLib.setOptionString("msg-level", "all=" + if (networkPreferences.verboseLogging().get()) "v" else "warn") + mpv?.setOptionString("msg-level", "all=" + if (networkPreferences.verboseLogging().get()) "v" else "warn") - MPVLib.setPropertyBoolean("keep-open", true) - MPVLib.setPropertyBoolean("input-default-bindings", true) + mpv?.setPropertyBoolean("input-default-bindings", true) - MPVLib.setOptionString("ytdl", "no") - MPVLib.setOptionString("tls-verify", "yes") - MPVLib.setOptionString("tls-ca-file", "${context.filesDir.path}/${PlayerActivity.MPV_DIR}/cacert.pem") + mpv?.setOptionString("idle", "yes") + mpv?.setOptionString("ytdl", "no") + mpv?.setOptionString("tls-verify", "yes") + mpv?.setOptionString("tls-ca-file", "${context.filesDir.path}/${PlayerActivity.MPV_DIR}/cacert.pem") + + // We handle selecting this in the viewmodel + mpv?.setOptionString("sid", "no") + mpv?.setOptionString("aid", "no") // Limit demuxer cache since the defaults are too high for mobile devices val cacheMegs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) 64 else 32 - MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") - MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") - // + mpv?.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") + mpv?.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") + val screenshotDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) screenshotDir.mkdirs() - MPVLib.setOptionString("screenshot-directory", screenshotDir.path) + mpv?.setOptionString("screenshot-directory", screenshotDir.path) VideoFilters.entries.forEach { - MPVLib.setOptionString(it.mpvProperty, it.preference(decoderPreferences).get().toString()) + mpv?.setOptionString(it.mpvProperty, it.preference(decoderPreferences).get().toString()) } - MPVLib.setOptionString("speed", playerPreferences.playerSpeed().get().toString()) + mpv?.setOptionString("speed", playerPreferences.playerSpeed().get().toString()) // workaround for - MPVLib.setOptionString("vd-lavc-film-grain", "cpu") + mpv?.setOptionString("vd-lavc-film-grain", "cpu") + postInitOptions() setupSubtitlesOptions() setupAudioOptions() + observeProperties() } - override fun observeProperties() { - for ((name, format) in observedProps) MPVLib.observeProperty(name, format) + fun observeProperties() { + for ((name, format) in observedProps) mpv?.observeProperty(name, format) } - override fun postInitOptions() { + fun postInitOptions() { + when (decoderPreferences.debanding().get()) { + Debanding.None -> {} + Debanding.CPU -> mpv?.setOptionString("vf", "gradfun=radius=12") + Debanding.GPU -> mpv?.setOptionString("deband", "yes") + } + advancedPreferences.playerStatisticsPage().get().let { if (it != 0) { - MPVLib.command(arrayOf("script-binding", "stats/display-stats-toggle")) - MPVLib.command(arrayOf("script-binding", "stats/display-page-$it")) + mpv?.command("script-binding", "stats/display-stats-toggle") + mpv?.command("script-binding", "stats/display-page-$it") } } } @@ -173,7 +128,7 @@ class AniyomiMPVView(context: Context, attributes: AttributeSet) : BaseMPVView(c return false } - var mapped = KeyMapping.map.get(event.keyCode) + var mapped = KeyMapping[event.keyCode] if (mapped == null) { // Fallback to produced glyph if (!event.isPrintingKey) { @@ -202,89 +157,67 @@ class AniyomiMPVView(context: Context, attributes: AttributeSet) : BaseMPVView(c val action = if (event.action == KeyEvent.ACTION_DOWN) "keydown" else "keyup" mod.add(mapped) - MPVLib.command(arrayOf(action, mod.joinToString("+"))) + mpv?.command(action, mod.joinToString("+")) return true } private val observedProps = mapOf( - "chapter" to MPVLib.mpvFormat.MPV_FORMAT_INT64, - "chapter-list" to MPVLib.mpvFormat.MPV_FORMAT_NONE, - "track-list" to MPVLib.mpvFormat.MPV_FORMAT_NONE, - - "time-pos" to MPVLib.mpvFormat.MPV_FORMAT_INT64, - "demuxer-cache-time" to MPVLib.mpvFormat.MPV_FORMAT_INT64, - "duration" to MPVLib.mpvFormat.MPV_FORMAT_INT64, - "volume" to MPVLib.mpvFormat.MPV_FORMAT_INT64, - "volume-max" to MPVLib.mpvFormat.MPV_FORMAT_INT64, - - "sid" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "secondary-sid" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "aid" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - - "speed" to MPVLib.mpvFormat.MPV_FORMAT_DOUBLE, - "video-params/aspect" to MPVLib.mpvFormat.MPV_FORMAT_DOUBLE, - - "hwdec-current" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "hwdec" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - - "pause" to MPVLib.mpvFormat.MPV_FORMAT_FLAG, - "paused-for-cache" to MPVLib.mpvFormat.MPV_FORMAT_FLAG, - "seeking" to MPVLib.mpvFormat.MPV_FORMAT_FLAG, - "eof-reached" to MPVLib.mpvFormat.MPV_FORMAT_FLAG, - - "user-data/aniyomi/show_text" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/toggle_ui" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/show_panel" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/software_keyboard" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/set_button_title" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/reset_button_title" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/toggle_button" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/switch_episode" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/pause" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/seek_by" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/seek_to" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/seek_by_with_text" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/seek_to_with_text" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - "user-data/aniyomi/launch_int_picker" to MPVLib.mpvFormat.MPV_FORMAT_STRING, - - "user-data/current-anime/intro-length" to MPVLib.mpvFormat.MPV_FORMAT_INT64, + "pause" to MPV.mpvFormat.MPV_FORMAT_FLAG, + "video-params/aspect" to MPV.mpvFormat.MPV_FORMAT_DOUBLE, + "eof-reached" to MPV.mpvFormat.MPV_FORMAT_FLAG, + + "user-data/aniyomi/show_text" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/toggle_ui" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/show_panel" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/software_keyboard" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/set_button_title" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/reset_button_title" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/toggle_button" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/switch_episode" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/pause" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/seek_by" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/seek_to" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/seek_by_with_text" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/seek_to_with_text" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/launch_int_picker" to MPV.mpvFormat.MPV_FORMAT_STRING, + "user-data/aniyomi/show_seek_text" to MPV.mpvFormat.MPV_FORMAT_STRING, ) private fun setupAudioOptions() { - MPVLib.setOptionString("alang", audioPreferences.preferredAudioLanguages().get()) - MPVLib.setOptionString("audio-delay", (audioPreferences.audioDelay().get() / 1000.0).toString()) - MPVLib.setOptionString("audio-pitch-correction", audioPreferences.enablePitchCorrection().get().toString()) - MPVLib.setOptionString("volume-max", (audioPreferences.volumeBoostCap().get() + 100).toString()) + mpv?.setOptionString("alang", audioPreferences.preferredAudioLanguages().get()) + mpv?.setOptionString("audio-delay", (audioPreferences.audioDelay().get() / 1000.0).toString()) + mpv?.setOptionString("audio-pitch-correction", audioPreferences.enablePitchCorrection().get().toString()) + mpv?.setOptionString("volume-max", (audioPreferences.volumeBoostCap().get() + 100).toString()) } private fun setupSubtitlesOptions() { - MPVLib.setOptionString("sub-delay", (subtitlePreferences.subtitlesDelay().get() / 1000.0).toString()) - MPVLib.setOptionString("sub-speed", subtitlePreferences.subtitlesSpeed().get().toString()) - MPVLib.setOptionString( + mpv?.setOptionString("sub-delay", (subtitlePreferences.subtitlesDelay().get() / 1000.0).toString()) + mpv?.setOptionString("sub-speed", subtitlePreferences.subtitlesSpeed().get().toString()) + mpv?.setOptionString( "secondary-sub-delay", (subtitlePreferences.subtitlesSecondaryDelay().get() / 1000.0).toString(), ) - MPVLib.setOptionString("sub-font", subtitlePreferences.subtitleFont().get()) + mpv?.setOptionString("sub-font", subtitlePreferences.subtitleFont().get()) if (subtitlePreferences.overrideSubsASS().get()) { - MPVLib.setOptionString("sub-ass-override", "force") - MPVLib.setOptionString("sub-ass-justify", "yes") + mpv?.setOptionString("sub-ass-override", "force") + mpv?.setOptionString("sub-ass-justify", "yes") } - MPVLib.setOptionString("sub-font-size", subtitlePreferences.subtitleFontSize().get().toString()) - MPVLib.setOptionString("sub-bold", if (subtitlePreferences.boldSubtitles().get()) "yes" else "no") - MPVLib.setOptionString("sub-italic", if (subtitlePreferences.italicSubtitles().get()) "yes" else "no") - MPVLib.setOptionString("sub-justify", subtitlePreferences.subtitleJustification().get().value) - MPVLib.setOptionString("sub-color", subtitlePreferences.textColorSubtitles().get().toColorHexString()) - MPVLib.setOptionString( + mpv?.setOptionString("sub-font-size", subtitlePreferences.subtitleFontSize().get().toString()) + mpv?.setOptionString("sub-bold", if (subtitlePreferences.boldSubtitles().get()) "yes" else "no") + mpv?.setOptionString("sub-italic", if (subtitlePreferences.italicSubtitles().get()) "yes" else "no") + mpv?.setOptionString("sub-justify", subtitlePreferences.subtitleJustification().get().value) + mpv?.setOptionString("sub-color", subtitlePreferences.textColorSubtitles().get().toColorHexString()) + mpv?.setOptionString( "sub-back-color", subtitlePreferences.backgroundColorSubtitles().get().toColorHexString(), ) - MPVLib.setOptionString("sub-border-color", subtitlePreferences.borderColorSubtitles().get().toColorHexString()) - MPVLib.setOptionString("sub-border-size", subtitlePreferences.subtitleBorderSize().get().toString()) - MPVLib.setOptionString("sub-border-style", subtitlePreferences.borderStyleSubtitles().get().value) - MPVLib.setOptionString("sub-shadow-offset", subtitlePreferences.shadowOffsetSubtitles().get().toString()) - MPVLib.setOptionString("sub-pos", subtitlePreferences.subtitlePos().get().toString()) - MPVLib.setOptionString("sub-scale", subtitlePreferences.subtitleFontScale().get().toString()) + mpv?.setOptionString("sub-outline-color", subtitlePreferences.borderColorSubtitles().get().toColorHexString()) + mpv?.setOptionString("sub-outline-size", subtitlePreferences.subtitleBorderSize().get().toString()) + mpv?.setOptionString("sub-border-style", subtitlePreferences.borderStyleSubtitles().get().value) + mpv?.setOptionString("sub-shadow-offset", subtitlePreferences.shadowOffsetSubtitles().get().toString()) + mpv?.setOptionString("sub-pos", subtitlePreferences.subtitlePos().get().toString()) + mpv?.setOptionString("sub-scale", subtitlePreferences.subtitleFontScale().get().toString()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 9990436e0b..b05285492d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -39,15 +39,21 @@ import android.media.session.PlaybackState import android.net.Uri import android.os.Build import android.os.Bundle +import android.util.DisplayMetrics import android.util.Rational import android.view.KeyEvent import android.view.View import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -58,7 +64,6 @@ import androidx.media.AudioAttributesCompat import androidx.media.AudioFocusRequestCompat import androidx.media.AudioManagerCompat import com.hippo.unifile.UniFile -import eu.kanade.domain.connection.service.ConnectionPreferences import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.animesource.model.ChapterType import eu.kanade.tachiyomi.animesource.model.Hoster @@ -67,27 +72,22 @@ import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications -import eu.kanade.tachiyomi.databinding.PlayerLayoutBinding -import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.player.controls.PlayerControls import eu.kanade.tachiyomi.ui.player.settings.AdvancedPlayerPreferences import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences import eu.kanade.tachiyomi.ui.player.settings.GesturePreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences -import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils.Companion.getStringRes import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast -import `is`.xyz.mpv.MPVLib -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import `is`.xyz.mpv.MPV +import `is`.xyz.mpv.MPVNode import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import logcat.LogPriority import tachiyomi.core.common.i18n.stringResource @@ -96,7 +96,6 @@ import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat -import tachiyomi.domain.custombutton.model.CustomButton import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR @@ -109,23 +108,23 @@ import kotlin.math.ceil import kotlin.math.floor class PlayerActivity : BaseActivity() { - private val viewModel by viewModels(factoryProducer = { PlayerViewModelProviderFactory(this) }) - private val binding by lazy { PlayerLayoutBinding.inflate(layoutInflater) } + private val viewModel by viewModels() + private val mpv by lazy { viewModel.mpv } + private val player by lazy { AniyomiMPVView(this, null) } private val playerObserver by lazy { PlayerObserver(this) } - val player by lazy { binding.player } - val windowInsetsController by lazy { WindowCompat.getInsetsController(window, window.decorView) } - val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } + private val windowInsetsController by lazy { WindowCompat.getInsetsController(window, window.decorView) } + private val audioManager by lazy { getSystemService(AUDIO_SERVICE) as AudioManager } + private val inputMethodManager by lazy { getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager } private var mediaSession: MediaSession? = null - private val gesturePreferences: GesturePreferences by lazy { viewModel.gesturePreferences } - private val playerPreferences: PlayerPreferences by lazy { viewModel.playerPreferences } + private val gesturePreferences: GesturePreferences = Injekt.get() + private val playerPreferences: PlayerPreferences = Injekt.get() private val audioPreferences: AudioPreferences = Injekt.get() private val advancedPlayerPreferences: AdvancedPlayerPreferences = Injekt.get() - private val networkPreferences: NetworkPreferences = Injekt.get() private val storageManager: StorageManager = Injekt.get() // AM (DISCORD_RPC) --> - private val connectionPreferences: ConnectionPreferences = Injekt.get() + // private val connectionPreferences: ConnectionPreferences = Injekt.get() // <-- AM (DISCORD_RPC) private var audioFocusRequest: AudioFocusRequestCompat? = null @@ -229,9 +228,9 @@ class PlayerActivity : BaseActivity() { enableEdgeToEdge() registerSecureActivity(this) super.onCreate(savedInstanceState) - setContentView(binding.root) setupPlayerMPV() + setupCustomButtons() setupPlayerAudio() setupMediaSession() setupPlayerOrientation() @@ -256,33 +255,80 @@ class PlayerActivity : BaseActivity() { is PlayerViewModel.Event.SetArtResult -> { onSetAsArtResult(event.result, event.artType) } + is PlayerViewModel.Event.ShowToast -> { + showToast(stringResource(event.stringResource)) + } + is PlayerViewModel.Event.ShowToastString -> { + showToast(event.string) + } + is PlayerViewModel.Event.ChangeEpisode -> { + changeEpisode(event.episodeId, event.autoPlay) + } + is PlayerViewModel.Event.SetVideo -> { + setVideo(event.video) + } + is PlayerViewModel.Event.SetStatusBar -> { + if (event.show) { + windowInsetsController.show(WindowInsetsCompat.Type.statusBars()) + } else { + windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) + } + } + is PlayerViewModel.Event.SetBrightness -> { + window.attributes = window.attributes.apply { + screenBrightness = event.brightness + } + } + is PlayerViewModel.Event.ChangeVideoAspect -> { + changeVideoAspect(event.aspect) + } + PlayerViewModel.Event.CycleRotations -> { + cycleRotations() + } + is PlayerViewModel.Event.SetKeyboard -> { + if (event.show) { + forceShowSoftwareKeyboard() + } else { + forceHideSoftwareKeyboard() + } + } + PlayerViewModel.Event.ToggleKeyboard -> { + toggleShowSoftwareKeyboard() + } } } .launchIn(lifecycleScope) - binding.controls.setContent { + setContent { TachiyomiTheme { - PlayerControls( - viewModel = viewModel, - onBackPress = { - if (isPipSupportedAndEnabled && player.paused == false && playerPreferences.pipOnExit().get()) { - enterPictureInPictureMode(createPipParams()) - } else { - finish() - } - }, - modifier = Modifier.onGloballyPositioned { - pipRect = run { - val boundsInWindow = it.boundsInWindow() - Rect( - boundsInWindow.left.toInt(), - boundsInWindow.top.toInt(), - boundsInWindow.right.toInt(), - boundsInWindow.bottom.toInt(), - ) - } - }, - ) + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + factory = { player }, + modifier = Modifier.onGloballyPositioned { + pipRect = run { + val boundsInWindow = it.boundsInWindow() + Rect( + boundsInWindow.left.toInt(), + boundsInWindow.top.toInt(), + boundsInWindow.right.toInt(), + boundsInWindow.bottom.toInt(), + ) + } + }, + ) + PlayerControls( + viewModel = viewModel, + onBackPress = { + if (isPipSupportedAndEnabled && viewModel.paused == false && + playerPreferences.pipOnExit().get() + ) { + enterPictureInPictureMode(createPipParams()) + } else { + finish() + } + }, + ) + } } } @@ -307,9 +353,9 @@ class PlayerActivity : BaseActivity() { noisyReceiver.initialized = false } - MPVLib.removeLogObserver(playerObserver) - MPVLib.removeObserver(playerObserver) - player.destroy() + mpv.removeLogObserver(playerObserver) + mpv.removeObserver(playerObserver) + mpv.close() super.onDestroy() } @@ -325,7 +371,7 @@ class PlayerActivity : BaseActivity() { player.isExiting = true if (isFinishing) { viewModel.deletePendingEpisodes() - MPVLib.command(arrayOf("stop")) + mpv.command("stop") } else { viewModel.pause() } @@ -348,7 +394,7 @@ class PlayerActivity : BaseActivity() { } override fun onUserLeaveHint() { - if (isPipSupportedAndEnabled && player.paused == false && playerPreferences.pipOnExit().get()) { + if (isPipSupportedAndEnabled && viewModel.paused == false && playerPreferences.pipOnExit().get()) { enterPictureInPictureMode() } super.onUserLeaveHint() @@ -356,7 +402,7 @@ class PlayerActivity : BaseActivity() { @Deprecated("Deprecated in Java") override fun onBackPressed() { - if (isPipSupportedAndEnabled && player.paused == false && playerPreferences.pipOnExit().get()) { + if (isPipSupportedAndEnabled && viewModel.paused == false && playerPreferences.pipOnExit().get()) { if (viewModel.sheetShown.value == Sheets.None && viewModel.panelShown.value == Panels.None && viewModel.dialogShown.value == Dialogs.None @@ -377,7 +423,7 @@ class PlayerActivity : BaseActivity() { WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, ) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - binding.root.systemUiVisibility = + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or @@ -401,12 +447,6 @@ class PlayerActivity : BaseActivity() { } } - private fun executeMPVCommand(commands: Array) { - if (!player.isExiting) { - MPVLib.command(commands) - } - } - private fun UniFile.writeText(text: String) { this.openOutputStream().use { it.write(text.toByteArray()) @@ -414,8 +454,6 @@ class PlayerActivity : BaseActivity() { } private fun setupPlayerMPV() { - val logLevel = if (networkPreferences.verboseLogging().get()) "info" else "warn" - val mpvDir = UniFile.fromFile(applicationContext.filesDir)!!.createDirectory(MPV_DIR)!! val mpvConfFile = mpvDir.createFile("mpv.conf")!! @@ -423,20 +461,15 @@ class PlayerActivity : BaseActivity() { val mpvInputFile = mpvDir.createFile("input.conf")!! advancedPlayerPreferences.mpvInput().get().let { mpvInputFile.writeText(it) } + player.init(mpv) copyUserFiles(mpvDir) copyAssets(mpvDir) copyFontsDirectory(mpvDir) - MPVLib.setOptionString("sub-ass-force-margins", "yes") - MPVLib.setOptionString("sub-use-margins", "yes") - - player.initialize( - configDir = mpvDir.filePath!!, - cacheDir = applicationContext.cacheDir.path, - logLvl = logLevel, - ) - MPVLib.addLogObserver(playerObserver) - MPVLib.addObserver(playerObserver) + mpv.setOptionString("sub-ass-force-margins", "yes") + mpv.setOptionString("sub-use-margins", "yes") + mpv.addLogObserver(playerObserver) + mpv.addObserver(playerObserver) } private fun copyUserFiles(mpvDir: UniFile) { @@ -509,7 +542,7 @@ class PlayerActivity : BaseActivity() { private fun copyFontsDirectory(mpvDir: UniFile) { // TODO: I think this is a bad hack. // We need to find a way to let MPV directly access our fonts directory. - CoroutineScope(Dispatchers.IO).launchIO { + lifecycleScope.launchIO { val fontsDirectory = mpvDir.createDirectory(MPV_FONTS_DIR)!! storageManager.getFontsDirectory()?.listFiles()?.forEach { font -> @@ -519,13 +552,16 @@ class PlayerActivity : BaseActivity() { } } - MPVLib.setPropertyString("sub-fonts-dir", fontsDirectory.filePath!!) - MPVLib.setPropertyString("osd-fonts-dir", fontsDirectory.filePath!!) + mpv.setPropertyString("sub-fonts-dir", fontsDirectory.filePath!!) + mpv.setPropertyString("osd-fonts-dir", fontsDirectory.filePath!!) } } - fun setupCustomButtons(buttons: List) { - CoroutineScope(Dispatchers.IO).launchIO { + fun setupCustomButtons() { + viewModel.viewModelScope.launchIO { + val buttons = viewModel.getCustomButtons() + viewModel.setCustomButtons(buttons) + val scriptsDir = { UniFile.fromFile(applicationContext.filesDir) ?.createDirectory(MPV_DIR) @@ -568,14 +604,14 @@ class PlayerActivity : BaseActivity() { } file?.let { - MPVLib.command(arrayOf("load-script", it.filePath)) + mpv.command("load-script", it.filePath!!) } } } private fun setupPlayerAudio() { with(audioPreferences) { - audioChannels().get().let { MPVLib.setPropertyString(it.property, it.value) } + audioChannels().get().let { mpv.setPropertyString(it.property, it.value) } val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN).also { it.setAudioAttributes( @@ -597,7 +633,7 @@ class PlayerActivity : BaseActivity() { AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, -> { val oldRestore = restoreAudioFocus - val wasPlayerPaused = player.paused ?: false + val wasPlayerPaused = viewModel.paused ?: false viewModel.pause() restoreAudioFocus = { oldRestore() @@ -606,9 +642,9 @@ class PlayerActivity : BaseActivity() { } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - MPVLib.command(arrayOf("multiply", "volume", "0.5")) + mpv.command("multiply", "volume", "0.5") restoreAudioFocus = { - MPVLib.command(arrayOf("multiply", "volume", "2")) + mpv.command("multiply", "volume", "2") } } @@ -654,102 +690,73 @@ class PlayerActivity : BaseActivity() { // A bunch of observers + @Suppress("unused") internal fun onObserverEvent(property: String, value: Long) { if (player.isExiting) return - when (property) { - "time-pos" -> { - viewModel.updatePlayBackPos(value.toFloat()) - viewModel.setChapter(value.toFloat()) - } - "demuxer-cache-time" -> viewModel.updateReadAhead(value = value) - "volume" -> viewModel.setMPVVolume(value.toInt()) - "volume-max" -> viewModel.volumeBoostCap = value.toInt() - 100 - // "chapter" -> viewModel.updateChapter(value) - "duration" -> viewModel.duration.update { value.toFloat() } - "user-data/current-anime/intro-length" -> viewModel.setAnimeSkipIntroLength(value) - } } + @Suppress("unused") internal fun onObserverEvent(property: String) { if (player.isExiting) return - when (property) { - "chapter-list" -> { - viewModel.loadChapters() - viewModel.updateChapter(0) - } - "track-list" -> viewModel.loadTracks() - } } internal fun onObserverEvent(property: String, value: Boolean) { if (player.isExiting) return when (property) { - "pause" -> { - if (value && player.paused == true) { - viewModel.pause() - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else if (!value && player.paused == false) { - viewModel.unpause() - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - - runCatching { - setPictureInPictureParams(createPipParams()) - } - } - - "paused-for-cache" -> { - viewModel.isLoading.update { value } - } - - "seeking" -> { - viewModel.isLoading.update { value } - } - - "eof-reached" -> { - endFile(value) - } - } - } - - val trackId: (String) -> Int? = { - when (it) { - "auto" -> null - "no" -> -1 - else -> it.toInt() + "pause" if value -> window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + "pause" -> window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + "eof-reached" -> endFile(value) } } internal fun onObserverEvent(property: String, value: String) { if (player.isExiting) return when (property.substringBeforeLast("/")) { - "aid" -> trackId(value)?.let { viewModel.updateAudio(it) } - "sid" -> trackId(value)?.let { viewModel.updateSubtitle(it, viewModel.selectedSubtitles.value.second) } - "secondary-sid" -> trackId(value)?.let { - viewModel.updateSubtitle(viewModel.selectedSubtitles.value.first, it) - } - "hwdec", "hwdec-current" -> viewModel.getDecoder() "user-data/aniyomi" -> viewModel.handleLuaInvocation(property, value) } } - @SuppressLint("NewApi") + @Suppress("unused") internal fun onObserverEvent(property: String, value: Double) { if (player.isExiting) return when (property) { - "speed" -> viewModel.playbackSpeed.update { value.toFloat() } "video-params/aspect" -> if (isPipSupportedAndEnabled) createPipParams() } } - internal fun event(eventId: Int) { + @Suppress("unused") + internal fun onObserverEvent(property: String, value: MPVNode) { + if (player.isExiting) return + } + + internal fun event(eventId: Int, node: MPVNode) { if (player.isExiting) return when (eventId) { - MPVLib.mpvEventId.MPV_EVENT_FILE_LOADED -> { + MPV.mpvEvent.MPV_EVENT_FILE_LOADED -> { viewModel.viewModelScope.launchIO { fileLoaded() } } - MPVLib.mpvEventId.MPV_EVENT_SEEK -> viewModel.isLoading.update { true } - MPVLib.mpvEventId.MPV_EVENT_PLAYBACK_RESTART -> player.isExiting = false + MPV.mpvEvent.MPV_EVENT_PLAYBACK_RESTART -> player.isExiting = false + MPV.mpvEvent.MPV_EVENT_END_FILE -> { + val errorNode = node.asMap()?.get("file_error") ?: return + var errorMessage = errorNode.asString() ?: "Error: File ended" + + val httpError = playerObserver.httpError + if (!httpError.isNullOrEmpty()) { + errorMessage += ": $httpError" + playerObserver.httpError = null + } + + logcat(LogPriority.ERROR) { errorMessage } + showToast(errorMessage) + + viewModel.setCurrentVideoError() + + if (playerPreferences.switchOnFailure().get()) { + viewModel.loadBestVideo() + } else { + viewModel.setIsStopped(true) + } + } } } @@ -765,20 +772,20 @@ class PlayerActivity : BaseActivity() { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val autoEnter = playerPreferences.pipOnExit().get() - builder.setAutoEnterEnabled(player.paused == false && autoEnter) - builder.setSeamlessResizeEnabled(player.paused == false && autoEnter) + builder.setAutoEnterEnabled(viewModel.paused == false && autoEnter) + builder.setSeamlessResizeEnabled(viewModel.paused == false && autoEnter) } builder.setActions( createPipActions( context = this, - isPaused = player.paused ?: true, + isPaused = viewModel.paused ?: true, replaceWithPrevious = playerPreferences.pipReplaceWithPrevious().get(), playlistCount = viewModel.currentPlaylist.value.size, playlistPosition = viewModel.getCurrentEpisodeIndex(), ), ) builder.setSourceRectHint(pipRect) - player.videoH?.let { + mpv.getPropertyInt("video-params/h")?.let { val height = it val width = it * player.getVideoOutAspect()!! val rational = Rational(height, width.toInt()).toFloat() @@ -893,7 +900,7 @@ class PlayerActivity : BaseActivity() { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.MediaPlay.keyCode)) + mpv.command("keypress", CustomKeyCodes.MediaPlay.keyCode) } SingleActionGesture.Switch -> {} @@ -910,7 +917,7 @@ class PlayerActivity : BaseActivity() { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.MediaPlay.keyCode)) + mpv.command("keypress", CustomKeyCodes.MediaPlay.keyCode) } SingleActionGesture.Switch -> {} @@ -927,7 +934,7 @@ class PlayerActivity : BaseActivity() { viewModel.pauseUnpause() } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.MediaPrevious.keyCode)) + mpv.command("keypress", CustomKeyCodes.MediaPrevious.keyCode) } SingleActionGesture.Switch -> viewModel.changeEpisode(true) @@ -944,7 +951,7 @@ class PlayerActivity : BaseActivity() { viewModel.pauseUnpause() } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.MediaNext.keyCode)) + mpv.command("keypress", CustomKeyCodes.MediaNext.keyCode) } SingleActionGesture.Switch -> viewModel.changeEpisode(false) @@ -1057,6 +1064,7 @@ class PlayerActivity : BaseActivity() { if (player.isExiting) return if (video == null) return + viewModel.setIsStopped(false) setHttpOptions(video) if (viewModel.isLoadingEpisode.value) { @@ -1068,11 +1076,11 @@ class PlayerActivity : BaseActivity() { } else { episode.last_second_seen } - MPVLib.command(arrayOf("set", "start", "${resumePosition / 1000F}")) + mpv.command("set", "start", "${resumePosition / 1000F}") } } else { - player.timePos?.let { - MPVLib.command(arrayOf("set", "start", "${player.timePos}")) + viewModel.pos?.let { + mpv.command("set", "start", "$it") } } @@ -1080,14 +1088,12 @@ class PlayerActivity : BaseActivity() { "$option=\"$value\"" } - MPVLib.command( - arrayOf( - "loadfile", - parseVideoUrl(video.videoUrl), - "replace", - "0", - videoOptions, - ), + mpv.command( + "loadfile", + parseVideoUrl(video.videoUrl)!!, + "replace", + "0", + videoOptions, ) } @@ -1123,7 +1129,7 @@ class PlayerActivity : BaseActivity() { it.key + ": " + it.value.replace(",", "\\,") }.joinToString(",") - MPVLib.setOptionString("http-header-fields", httpHeaderString) + mpv.setOptionString("http-header-fields", httpHeaderString) // need to fix the cache // MPVLib.setOptionString("cache-on-disk", "yes") @@ -1131,6 +1137,10 @@ class PlayerActivity : BaseActivity() { // MPVLib.setOptionString("cache-dir", cacheDir) } + fun onTrackLoadedFailure(url: String) { + viewModel.onTrackLoadedFailure(url) + } + /** * Called from the presenter when a screenshot is ready to be shared. It shows Android's * default sharing tool. @@ -1180,37 +1190,79 @@ class PlayerActivity : BaseActivity() { ) } - // TODO: exception java.util.ConcurrentModificationException: - // UPDATE: MAY HAVE BEEN FIXED - // at java.lang.Object java.util.ArrayList$Itr.next() (ArrayList.java:860) - // at void eu.kanade.tachiyomi.ui.player.PlayerActivity.fileLoaded() (PlayerActivity.kt:1874) - // at void eu.kanade.tachiyomi.ui.player.PlayerActivity.event(int) (PlayerActivity.kt:1566) - // at void is.xyz.mpv.MPVLib.event(int) (MPVLib.java:86) + private fun changeVideoAspect(aspect: VideoAspect) { + var ratio = -1.0 + val pan: Double + when (aspect) { + VideoAspect.Crop -> { + pan = 1.0 + } + + VideoAspect.Fit -> { + pan = 0.0 + mpv.setPropertyDouble("panscan", 0.0) + } + + VideoAspect.Stretch -> { + val dm = DisplayMetrics() + windowManager.defaultDisplay.getRealMetrics(dm) + ratio = dm.widthPixels / dm.heightPixels.toDouble() + pan = 0.0 + } + } + viewModel.setAspect(aspect, pan, ratio) + } + + private fun cycleRotations() { + requestedOrientation = when (requestedOrientation) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE, + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, + -> { + playerPreferences.defaultPlayerOrientationType().set(PlayerOrientation.SensorPortrait) + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + } + + else -> { + playerPreferences.defaultPlayerOrientationType().set(PlayerOrientation.SensorLandscape) + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } + } + } + + private fun toggleShowSoftwareKeyboard() { + if (inputMethodManager.isActive) { + forceHideSoftwareKeyboard() + } else { + forceShowSoftwareKeyboard() + } + } + + private fun forceShowSoftwareKeyboard() { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) + } + + private fun forceHideSoftwareKeyboard() { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0) + } + private fun fileLoaded() { if (player.isExiting) return + setMpvOptions() setMpvMediaTitle() setupPlayerOrientation() setupChapters() - setupTracks() + viewModel.setPausedState() + viewModel.updateIsLoadingEpisode(false) // aniSkip stuff - viewModel.waitingSkipIntro = playerPreferences.waitingTimeIntroSkip().get() - runBlocking { - if ( - viewModel.introSkipEnabled && - playerPreferences.aniSkipEnabled().get() && - !(playerPreferences.disableAniSkipOnChapters().get() && viewModel.chapters.value.isNotEmpty()) + viewModel.viewModelScope.launchIO { + if (viewModel.introSkipEnabled && playerPreferences.aniSkipEnabled().get() && + !(playerPreferences.disableAniSkipOnChapters().get() && viewModel.getChapterCount() > 0) ) { - viewModel.aniSkipResponse(player.duration)?.let { - viewModel.updateChapters( - ChapterUtils.mergeChapters( - currentChapters = viewModel.chapters.value, - stamps = it, - duration = player.duration, - ), - ) - viewModel.setChapter(viewModel.pos.value) + viewModel.aniSkipResponse(viewModel.duration)?.let { + viewModel.addTimestamps(it) } } } @@ -1226,9 +1278,9 @@ class PlayerActivity : BaseActivity() { } try { - val metadata = Json.decodeFromString>( - MPVLib.getPropertyString("metadata"), - ) + val metadata = mpv.getPropertyString("metadata")?.let { + Json.decodeFromString>(it) + } ?: return val opts = metadata[Video.MPV_ARGS_TAG] ?.split(";") @@ -1236,37 +1288,13 @@ class PlayerActivity : BaseActivity() { ?: return opts.forEach { (option, value) -> - MPVLib.setPropertyString(option, value) + mpv.setPropertyString(option, value) } } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to read video metadata" } } } - private fun setupTracks() { - if (player.isExiting) return - viewModel.isLoadingTracks.update { _ -> true } - - val audioTracks = viewModel.currentVideo.value?.audioTracks?.takeIf { it.isNotEmpty() } - val subtitleTracks = viewModel.currentVideo.value?.subtitleTracks?.takeIf { it.isNotEmpty() } - - // If no external audio or subtitle tracks are present, loadTracks() won't be - // called and we need to call onFinishLoadingTracks() manually - if (audioTracks == null && subtitleTracks == null) { - viewModel.onFinishLoadingTracks() - return - } - - audioTracks?.forEach { audio -> - executeMPVCommand(arrayOf("audio-add", audio.url, "auto", audio.lang)) - } - subtitleTracks?.forEach { sub -> - executeMPVCommand(arrayOf("sub-add", sub.url, "auto", sub.lang)) - } - - viewModel.isLoadingTracks.update { _ -> false } - } - private fun setupChapters() { if (player.isExiting) return @@ -1282,14 +1310,7 @@ class PlayerActivity : BaseActivity() { } ?: return - viewModel.updateChapters( - ChapterUtils.mergeChapters( - currentChapters = viewModel.chapters.value, - stamps = timestamps, - duration = player.duration, - ), - ) - viewModel.setChapter(viewModel.pos.value) + viewModel.addTimestamps(timestamps) } private fun setMpvMediaTitle() { @@ -1298,7 +1319,7 @@ class PlayerActivity : BaseActivity() { val episode = viewModel.currentEpisode.value ?: return // Write to mpv table - MPVLib.setPropertyString("user-data/current-anime/episode-title", episode.name) + mpv.setPropertyString("user-data/current-anime/episode-title", episode.name) val epNumber = episode.episode_number.let { number -> if (ceil(number) == floor(number)) number.toInt() else number @@ -1311,7 +1332,7 @@ class PlayerActivity : BaseActivity() { episode.name, ) - MPVLib.setPropertyString("force-media-title", title) + mpv.setPropertyString("force-media-title", title) } private fun endFile(eofReached: Boolean) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerEnums.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerEnums.kt index 40f87fc965..eb2b47da09 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerEnums.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerEnums.kt @@ -21,6 +21,7 @@ import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.ui.player.settings.DecoderPreferences import tachiyomi.core.common.preference.Preference import tachiyomi.i18n.MR +import tachiyomi.i18n.animiru.AMMR import tachiyomi.i18n.aniyomi.AYMR /** @@ -84,16 +85,19 @@ enum class Decoder(val title: String, val value: String) { SW("SW", "no"), HW("HW", "mediacodec-copy"), HWPlus("HW+", "mediacodec"), -} + ; -fun getDecoderFromValue(value: String): Decoder { - return Decoder.entries.first { it.value == value } + companion object { + fun getDecoderFromValue(value: String): Decoder { + return Decoder.entries.first { it.value == value } + } + } } -enum class Debanding { - None, - CPU, - GPU, +enum class Debanding(val stringRes: StringResource) { + None(AMMR.strings.player_sheets_deband_none), + CPU(AMMR.strings.player_sheets_deband_cpu), + GPU(AMMR.strings.player_sheets_deband_gpu), } enum class Sheets { @@ -169,3 +173,40 @@ enum class VideoFilters( "hue", ), } + +enum class DebandSettings( + val stringRes: StringResource, + val preference: (DecoderPreferences) -> Preference, + val mpvProperty: String, + val start: Int, + val end: Int, +) { + Iterations( + AMMR.strings.player_sheets_deband_iterations, + { it.debandIterations() }, + "deband-iterations", + 0, + 16, + ), + Threshold( + AMMR.strings.player_sheets_deband_threshold, + { it.debandThreshold() }, + "deband-threshold", + 0, + 200, + ), + Range( + AMMR.strings.player_sheets_deband_range, + { it.debandRange() }, + "deband-range", + 1, + 64, + ), + Grain( + AMMR.strings.player_sheets_deband_grain, + { it.debandGrain() }, + "deband-grain", + 0, + 200, + ), +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerModels.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerModels.kt new file mode 100644 index 0000000000..9326ad375f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerModels.kt @@ -0,0 +1,139 @@ +package eu.kanade.tachiyomi.ui.player + +import androidx.compose.runtime.Immutable +import dev.vivvvek.seeker.Segment +import eu.kanade.tachiyomi.animesource.model.ChapterType +import eu.kanade.tachiyomi.animesource.model.Track +import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ChapterNode( + val time: Float, + private val title: String, +) { + val chapterTitle: String + get() = title.substringBeforeLast(ChapterUtils.ANIYOMI_CHAPTER_IDENTIFIER) + + // Ugly hack but the alternative is worse + val chapterType: ChapterType + get() { + val ordinal = title.substringAfterLast( + delimiter = ChapterUtils.ANIYOMI_CHAPTER_IDENTIFIER, + missingDelimiterValue = ChapterType.Other.ordinal.toString(), + ).toInt() + return ChapterType.entries[ordinal] + } + + fun toSegment(): Segment = Segment( + name = title.substringBeforeLast(ChapterUtils.ANIYOMI_CHAPTER_IDENTIFIER), + start = time, + ) +} + +@Serializable +data class TrackNode( + val id: Int, + val type: String, + @SerialName("src-id") val srcId: Long? = null, + val title: String? = null, + val lang: String? = null, + val image: Boolean? = null, + @SerialName("albumArt") val albumArt: Boolean? = null, + val default: Boolean? = null, + val forced: Boolean? = null, + val dependent: Boolean? = null, + @SerialName("visual-impaired") val visualImpaired: Boolean? = null, + @SerialName("hearing-impaired") val hearingImpaired: Boolean? = null, + @SerialName("hls-bitrate") val hlsBitrate: Long? = null, + @SerialName("program-id") val programId: Long? = null, + val selected: Boolean? = null, + @SerialName("main-selection") val mainSelection: Long? = null, + val external: Boolean? = null, + @SerialName("external-filename") val externalFilename: String? = null, + val codec: String? = null, + @SerialName("codec-desc") val codecDesc: String? = null, + @SerialName("codec-profile") val codecProfile: String? = null, + @SerialName("ff-index") val ffIndex: Long? = null, + val decoder: String? = null, + @SerialName("decoder-desc") val decoderDesc: String? = null, + @SerialName("demux-w") val demuxW: Long? = null, + @SerialName("demux-h") val demuxH: Long? = null, + @SerialName("demux-crop-x") val demuxCropX: Long? = null, + @SerialName("demux-crop-y") val demuxCropY: Long? = null, + @SerialName("demux-crop-w") val demuxCropW: Long? = null, + @SerialName("demux-crop-h") val demuxCropH: Long? = null, + @SerialName("demux-channel-count") val demuxChannelCount: Long? = null, + @SerialName("demux-channels") val demuxChannels: String? = null, + @SerialName("demux-samplerate") val demuxSampleRate: Long? = null, + @SerialName("demux-fps") val demuxFps: Double? = null, + @SerialName("demux-bitrate") val demuxBitrate: Long? = null, + @SerialName("demux-rotation") val demuxRotation: Long? = null, + @SerialName("demux-par") val demuxPar: Double? = null, + @SerialName("format-name") val formatName: String? = null, + @SerialName("audio-channels") val audioChannels: Long? = null, + @SerialName("replaygain-track-peak") val replayGainTrackPeak: Double? = null, + @SerialName("replaygain-track-gain") val replayGainTrackGain: Double? = null, + @SerialName("replaygain-album-peak") val replayGainAlbumPeak: Double? = null, + @SerialName("replaygain-album-gain") val replayGainAlbumGain: Double? = null, + @SerialName("dolby-vision-profile") val dolbyVisionProfile: Long? = null, + @SerialName("dolby-vision-level") val dolbyVisionLevel: Long? = null, + val metadata: Map? = null, +) { + val isVideo = type == "video" + val isAudio = type == "audio" + val isSubtitle = type == "sub" + val isSelected = selected == true + + fun getMetadata(key: String): String? = metadata?.get(key) + fun hasMetadata(): Boolean = !metadata.isNullOrEmpty() +} + +enum class TrackState { + Idle, + Loading, + Error, + Loaded, +} + +@Immutable +sealed interface VideoTrack { + companion object { + const val TRACK_TITLE_TAG = "aniyomi-track-index" + } + + data class Internal(val data: TrackNode) : VideoTrack + + data class External( + val data: Track, + val index: Int, + val id: Int? = null, + val mainSelection: Int = -1, + val state: TrackState = TrackState.Idle, + ) : VideoTrack + + val title: String + get() = when (this) { + is External -> data.lang + is Internal -> data.title.orEmpty() + } + + val lang: String + get() = when (this) { + is External -> data.lang + is Internal -> data.lang.orEmpty() + } + + val selection: Int + get() = when (this) { + is External -> mainSelection + is Internal -> data.mainSelection?.toInt() ?: -1 + } + + val trackId: Int? + get() = when (this) { + is External -> id + is Internal -> data.id + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerObserver.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerObserver.kt index 14ac0ed80d..533ac634a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerObserver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerObserver.kt @@ -1,14 +1,12 @@ package eu.kanade.tachiyomi.ui.player -import android.widget.Toast -import eu.kanade.tachiyomi.util.system.toast -import `is`.xyz.mpv.MPVLib +import `is`.xyz.mpv.MPV +import `is`.xyz.mpv.MPVNode import logcat.LogPriority -import tachiyomi.core.common.util.system.logcat class PlayerObserver(val activity: PlayerActivity) : - MPVLib.EventObserver, - MPVLib.LogObserver { + MPV.EventObserver, + MPV.LogObserver { override fun eventProperty(property: String) { activity.runOnUiThread { activity.onObserverEvent(property) } @@ -30,32 +28,35 @@ class PlayerObserver(val activity: PlayerActivity) : activity.runOnUiThread { activity.onObserverEvent(property, value) } } - override fun event(eventId: Int) { - activity.runOnUiThread { activity.event(eventId) } + override fun eventProperty(property: String, value: MPVNode) { + activity.runOnUiThread { activity.onObserverEvent(property, value) } } - override fun efEvent(err: String?) { - var errorMessage = err ?: "Error: File ended" - if (!httpError.isNullOrEmpty()) { - errorMessage += ": $httpError" - httpError = null - } - logcat(LogPriority.ERROR) { errorMessage } - activity.runOnUiThread { - activity.toast(errorMessage, Toast.LENGTH_LONG) - } + override fun event(eventId: Int, data: MPVNode) { + activity.runOnUiThread { activity.event(eventId, data) } } - private var httpError: String? = null + var httpError: String? = null override fun logMessage(prefix: String, level: Int, text: String) { + if (level == MPV.mpvLogLevel.MPV_LOG_LEVEL_ERROR) { + if (text.startsWith(TRACK_LOAD_FAILURE)) { + val url = text.removePrefix(TRACK_LOAD_FAILURE).substringBeforeLast(".") + activity.onTrackLoadedFailure(url) + } + } + val logPriority = when (level) { - MPVLib.mpvLogLevel.MPV_LOG_LEVEL_FATAL, MPVLib.mpvLogLevel.MPV_LOG_LEVEL_ERROR -> LogPriority.ERROR - MPVLib.mpvLogLevel.MPV_LOG_LEVEL_WARN -> LogPriority.WARN - MPVLib.mpvLogLevel.MPV_LOG_LEVEL_INFO -> LogPriority.INFO + MPV.mpvLogLevel.MPV_LOG_LEVEL_FATAL, MPV.mpvLogLevel.MPV_LOG_LEVEL_ERROR -> LogPriority.ERROR + MPV.mpvLogLevel.MPV_LOG_LEVEL_WARN -> LogPriority.WARN + MPV.mpvLogLevel.MPV_LOG_LEVEL_INFO -> LogPriority.INFO else -> LogPriority.VERBOSE } - if (text.contains("HTTP error")) httpError = text + if (text.contains("HTTP error")) httpError = text.removePrefix("http: ") logcat.logcat("mpv/$prefix", logPriority) { text } } + + companion object { + const val TRACK_LOAD_FAILURE = "Can not open external file " + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerUtils.kt index d944f3c5fd..106a2cc737 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerUtils.kt @@ -21,9 +21,16 @@ import android.content.Context import android.net.Uri import android.os.ParcelFileDescriptor import android.provider.OpenableColumns +import `is`.xyz.mpv.MPVNode import `is`.xyz.mpv.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import logcat.LogPriority import logcat.logcat +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty internal fun Uri.openContentFd(context: Context): String? { return context.contentResolver.openFileDescriptor(this, "r")?.detachFd()?.let { @@ -53,3 +60,14 @@ internal fun Uri.getFileName(context: Context): String? { cursor.getString(nameIndex) } } + +inline fun MPVNode.toObject(json: Json): T = json.decodeFromString(toJson()) + +fun Flow.collectAsState(scope: CoroutineScope, initialValue: T? = null) = + object : ReadOnlyProperty { + private var value: T? = initialValue + init { + scope.launch { collect { value = it } } + } + override fun getValue(thisRef: Any?, property: KProperty<*>) = value + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt index 2cb0f221e2..cd5e26f68a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt @@ -23,21 +23,10 @@ package eu.kanade.tachiyomi.ui.player import android.app.Application -import android.content.Context -import android.content.pm.ActivityInfo -import android.media.AudioManager import android.net.Uri -import android.provider.Settings -import android.util.DisplayMetrics -import android.view.inputmethod.InputMethodManager -import androidx.compose.runtime.Immutable -import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import dev.icerock.moko.resources.StringResource import eu.kanade.domain.anime.interactor.SetAnimeViewerFlags import eu.kanade.domain.base.BasePreferences @@ -47,8 +36,6 @@ import eu.kanade.domain.source.interactor.GetIncognitoState import eu.kanade.domain.track.interactor.TrackEpisode import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.ui.UiPreferences -import eu.kanade.presentation.more.settings.screen.player.custombutton.CustomButtonFetchState -import eu.kanade.presentation.more.settings.screen.player.custombutton.getButtons import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.model.ChapterType import eu.kanade.tachiyomi.animesource.model.Hoster @@ -68,16 +55,21 @@ import eu.kanade.tachiyomi.data.saver.Location import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList +import eu.kanade.tachiyomi.ui.player.PlayerActivity.Companion.MPV_DIR import eu.kanade.tachiyomi.ui.player.controls.components.IndexedSegment import eu.kanade.tachiyomi.ui.player.controls.components.sheets.HosterState import eu.kanade.tachiyomi.ui.player.controls.components.sheets.getChangedAt +import eu.kanade.tachiyomi.ui.player.domain.AudioManager +import eu.kanade.tachiyomi.ui.player.domain.BrightnessManager +import eu.kanade.tachiyomi.ui.player.domain.TrackSelect import eu.kanade.tachiyomi.ui.player.loader.EpisodeLoader import eu.kanade.tachiyomi.ui.player.loader.HosterLoader +import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences import eu.kanade.tachiyomi.ui.player.settings.GesturePreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import eu.kanade.tachiyomi.ui.player.utils.AniSkipApi +import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils import eu.kanade.tachiyomi.ui.player.utils.ChapterUtils.Companion.getStringRes -import eu.kanade.tachiyomi.ui.player.utils.TrackSelect import eu.kanade.tachiyomi.util.editBackground import eu.kanade.tachiyomi.util.editCover import eu.kanade.tachiyomi.util.editThumbnail @@ -86,10 +78,13 @@ import eu.kanade.tachiyomi.util.lang.byteSize import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.cacheImageDir -import eu.kanade.tachiyomi.util.system.toast -import `is`.xyz.mpv.MPVLib +import `is`.xyz.mpv.MPV +import `is`.xyz.mpv.MPVNode import `is`.xyz.mpv.Utils +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -98,17 +93,21 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json import logcat.LogPriority import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.anime.interactor.GetAnime import tachiyomi.domain.anime.model.Anime @@ -126,7 +125,6 @@ import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.track.interactor.GetTracks -import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt @@ -137,17 +135,10 @@ import java.util.Date import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException -class PlayerViewModelProviderFactory( - private val activity: PlayerActivity, -) : ViewModelProvider.Factory { - override fun create(modelClass: Class, extras: CreationExtras): T { - return PlayerViewModel(activity, extras.createSavedStateHandle()) as T - } -} - class PlayerViewModel @JvmOverloads constructor( - private val activity: PlayerActivity, + private val context: Application, private val savedState: SavedStateHandle, + private val json: Json = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val imageSaver: ImageSaver = Injekt.get(), @@ -162,20 +153,35 @@ class PlayerViewModel @JvmOverloads constructor( private val upsertHistory: UpsertHistory = Injekt.get(), private val updateEpisode: UpdateEpisode = Injekt.get(), private val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(), - internal val playerPreferences: PlayerPreferences = Injekt.get(), - internal val gesturePreferences: GesturePreferences = Injekt.get(), + private val playerPreferences: PlayerPreferences = Injekt.get(), + private val audioPreferences: AudioPreferences = Injekt.get(), + private val gesturePreferences: GesturePreferences = Injekt.get(), private val basePreferences: BasePreferences = Injekt.get(), private val getCustomButtons: GetCustomButtons = Injekt.get(), private val trackSelect: TrackSelect = Injekt.get(), private val getIncognitoState: GetIncognitoState = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val audioManager: AudioManager = Injekt.get(), + brightnessManager: BrightnessManager = Injekt.get(), // AM (SYNC) --> private val syncPreferences: SyncPreferences = Injekt.get(), // <-- AM (SYNC) uiPreferences: UiPreferences = Injekt.get(), -) : ViewModel() { +) : AndroidViewModel(context) { + + val cachePath: String = context.applicationContext.cacheDir.path + val mpv = MPV(context.applicationContext) { + it.setOptionString("config", "yes") + it.setOptionString("config-dir", context.filesDir.resolve(MPV_DIR).toString()) + it.setOptionString("gpu-shader-cache-dir", cachePath) + it.setOptionString("icc-cache-dir", cachePath) + it.setOptionString("keep-open", "yes") + } + + private val _isStopped = MutableStateFlow(false) + val isStopped = _isStopped.asStateFlow() - private val _currentPlaylist = MutableStateFlow>(emptyList()) + private val _currentPlaylist = MutableStateFlow>(persistentListOf()) val currentPlaylist = _currentPlaylist.asStateFlow() private val _hasPreviousEpisode = MutableStateFlow(false) @@ -199,26 +205,17 @@ class PlayerViewModel @JvmOverloads constructor( private val _isLoadingEpisode = MutableStateFlow(false) val isLoadingEpisode = _isLoadingEpisode.asStateFlow() - private val _currentDecoder = MutableStateFlow(getDecoderFromValue(MPVLib.getPropertyString("hwdec"))) - val currentDecoder = _currentDecoder.asStateFlow() - val mediaTitle = MutableStateFlow("") val animeTitle = MutableStateFlow("") val isLoading = MutableStateFlow(true) - val playbackSpeed = MutableStateFlow(playerPreferences.playerSpeed().get()) - - private val _subtitleTracks = MutableStateFlow>(emptyList()) - val subtitleTracks = _subtitleTracks.asStateFlow() - private val _selectedSubtitles = MutableStateFlow(Pair(-1, -1)) - val selectedSubtitles = _selectedSubtitles.asStateFlow() + val hasLoadedTracks = MutableStateFlow(false) - private val _audioTracks = MutableStateFlow>(emptyList()) - val audioTracks = _audioTracks.asStateFlow() - private val _selectedAudio = MutableStateFlow(-1) - val selectedAudio = _selectedAudio.asStateFlow() + private val _externalSubtitleTracks = MutableStateFlow>(emptyList()) + val externalSubtitleTracks = _externalSubtitleTracks.asStateFlow() - val isLoadingTracks = MutableStateFlow(true) + private val _externalAudioTracks = MutableStateFlow>(emptyList()) + val externalAudioTracks = _externalAudioTracks.asStateFlow() private val _hosterList = MutableStateFlow>(emptyList()) val hosterList = _hosterList.asStateFlow() @@ -233,31 +230,58 @@ class PlayerViewModel @JvmOverloads constructor( private val _currentVideo = MutableStateFlow(null) val currentVideo = _currentVideo.asStateFlow() - private val _chapters = MutableStateFlow>(emptyList()) - val chapters = _chapters.asStateFlow() - private val _currentChapter = MutableStateFlow(null) - val currentChapter = _currentChapter.asStateFlow() - private val _skipIntroText = MutableStateFlow(null) - val skipIntroText = _skipIntroText.asStateFlow() + private val _pausedState = MutableStateFlow(false) + val pausedState = _pausedState.asStateFlow() - private val _pos = MutableStateFlow(0f) - val pos = _pos.asStateFlow() + private val netflixTimeout = MutableStateFlow(null) - val duration = MutableStateFlow(0f) + // Start mpvKt - private val _readAhead = MutableStateFlow(0f) - val readAhead = _readAhead.asStateFlow() + private val _customButtons = MutableStateFlow>(persistentListOf()) + val customButtons = _customButtons.asStateFlow() - private val _paused = MutableStateFlow(false) - val paused = _paused.asStateFlow() + private val _primaryButtonTitle = MutableStateFlow("") + val primaryButtonTitle = _primaryButtonTitle.asStateFlow() - // False because the video shouldn't start paused - private val _pausedState = MutableStateFlow(false) - val pausedState = _pausedState.asStateFlow() + private val _primaryButton = MutableStateFlow(null) + val primaryButton = _primaryButton.asStateFlow() + + val paused by mpv.propFlow("pause").collectAsState(viewModelScope) + val pos by mpv.propFlow("time-pos").collectAsState(viewModelScope) + val duration by mpv.propFlow("duration").collectAsState(viewModelScope) + + val currentVolume = MutableStateFlow(audioManager.getVolume()) + private val volumeBoostCap by mpv.propFlow("volume-max").collectAsState(viewModelScope) + + val subtitleTracks = mpv.propFlow("track-list") + .map { node -> + ( + node?.toObject>(json) + ?.filter { it.isSubtitle } + ?.filterNot { it.title?.startsWith(VideoTrack.TRACK_TITLE_TAG) == true } + ?: persistentListOf() + ).toImmutableList() + } + + val audioTracks = mpv.propFlow("track-list") + .map { node -> + ( + node?.toObject>(json) + ?.filter { it.isAudio } + ?.filterNot { it.title?.startsWith(VideoTrack.TRACK_TITLE_TAG) == true } + ?: persistentListOf() + ).toImmutableList() + } + + val chapters = mpv.propFlow("chapter-list") + .map { (it?.toObject>(json) ?: persistentListOf()).map { it.toSegment() }.toImmutableList() } + + private val _skipIntroText = MutableStateFlow(null) + val skipIntroText = _skipIntroText.asStateFlow() - private val _controlsShown = MutableStateFlow(!playerPreferences.hideControls().get()) + private val _controlsShown = MutableStateFlow(true) val controlsShown = _controlsShown.asStateFlow() - private val _seekBarShown = MutableStateFlow(!playerPreferences.hideControls().get()) + private val _seekBarShown = MutableStateFlow(true) val seekBarShown = _seekBarShown.asStateFlow() private val _areControlsLocked = MutableStateFlow(false) val areControlsLocked = _areControlsLocked.asStateFlow() @@ -265,26 +289,17 @@ class PlayerViewModel @JvmOverloads constructor( val playerUpdate = MutableStateFlow(PlayerUpdates.None) val isBrightnessSliderShown = MutableStateFlow(false) val isVolumeSliderShown = MutableStateFlow(false) - val currentBrightness = MutableStateFlow( - runCatching { - Settings.System.getFloat(activity.contentResolver, Settings.System.SCREEN_BRIGHTNESS) - .normalize(0f, 255f, 0f, 1f) - }.getOrElse { 0f }, - ) - val currentVolume = MutableStateFlow(activity.audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) - val currentMPVVolume = MutableStateFlow(MPVLib.getPropertyInt("volume")) - var volumeBoostCap: Int = MPVLib.getPropertyInt("volume-max") - - // Pair(startingPosition, seekAmount) - val gestureSeekAmount = MutableStateFlow?>(null) + val currentBrightness = MutableStateFlow(brightnessManager.getCurrentBrightness()) val sheetShown = MutableStateFlow(Sheets.None) val panelShown = MutableStateFlow(Panels.None) val dialogShown = MutableStateFlow(Dialogs.None) - private val _dismissSheet = MutableStateFlow(false) val dismissSheet = _dismissSheet.asStateFlow() + // Pair(startingPosition, seekAmount) + val gestureSeekAmount = MutableStateFlow?>(null) + private val _seekText = MutableStateFlow(null) val seekText = _seekText.asStateFlow() private val _doubleTapSeekAmount = MutableStateFlow(0) @@ -296,36 +311,33 @@ class PlayerViewModel @JvmOverloads constructor( private val _remainingTime = MutableStateFlow(0) val remainingTime = _remainingTime.asStateFlow() - val cachePath: String = activity.cacheDir.path + init { + mpv.propFlow("time-pos") + .filterNotNull() + .onEach(::onSecondReached) + .launchIn(viewModelScope) - private val _customButtons = MutableStateFlow(CustomButtonFetchState.Loading) - val customButtons = _customButtons.asStateFlow() + mpv.propFlow("chapter") + .onEach(::onChapterChanged) + .launchIn(viewModelScope) - private val _primaryButtonTitle = MutableStateFlow("") - val primaryButtonTitle = _primaryButtonTitle.asStateFlow() + mpv.propFlow("sid") + .onEach { onSubtitleTrackSelectChange() } + .launchIn(viewModelScope) - private val _primaryButton = MutableStateFlow(null) - val primaryButton = _primaryButton.asStateFlow() + mpv.propFlow("secondary-sid") + .onEach { onSubtitleTrackSelectChange() } + .launchIn(viewModelScope) - init { - viewModelScope.launchIO { - try { - val buttons = getCustomButtons.getAll() - buttons.firstOrNull { it.isFavorite }?.let { - _primaryButton.update { _ -> it } - // If the button text is not empty, it has been set buy a lua script in which - // case we don't want to override it - if (_primaryButtonTitle.value.isEmpty()) { - setPrimaryCustomButtonTitle(it) - } - } - activity.setupCustomButtons(buttons) - _customButtons.update { _ -> CustomButtonFetchState.Success(buttons.toImmutableList()) } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - _customButtons.update { _ -> CustomButtonFetchState.Error(e.message ?: "Unable to fetch buttons") } - } - } + mpv.propFlow("user-data/current-anime/intro-length") + .filterNotNull() + .onEach(::setAnimeSkipIntroLength) + .launchIn(viewModelScope) + + mpv.propFlow("track-list") + .filterNotNull() + .onEach { onTrackListChanged(it) } + .launchIn(viewModelScope) } /** @@ -340,8 +352,22 @@ class PlayerViewModel @JvmOverloads constructor( _remainingTime.value = time delay(1000) } - pause() - withUIContext { Injekt.get().toast(AYMR.strings.toast_sleep_timer_ended) } + mpv.setPropertyBoolean("pause", true) + eventChannel.send(Event.ShowToast(AYMR.strings.toast_sleep_timer_ended)) + } + } + + suspend fun getCustomButtons(): List { + return getCustomButtons.getAll() + } + + fun setCustomButtons(buttons: List) { + _customButtons.update { _ -> buttons.toPersistentList() } + buttons.firstOrNull { it.isFavorite }?.let { + _primaryButton.update { _ -> it } + if (primaryButtonTitle.value.isEmpty()) { + setPrimaryCustomButtonTitle(it) + } } } @@ -361,238 +387,289 @@ class PlayerViewModel @JvmOverloads constructor( } private fun updateEpisodeList(episodeList: List) { - _currentPlaylist.update { _ -> filterEpisodeList(episodeList) } - } - - fun getDecoder() { - _currentDecoder.update { getDecoderFromValue(activity.player.hwdecActive) } + _currentPlaylist.update { _ -> filterEpisodeList(episodeList).toPersistentList() } } - fun updateDecoder(decoder: Decoder) { - MPVLib.setPropertyString("hwdec", decoder.value) - } - - val getTrackLanguage: (Int) -> String = { - if (it != -1) { - MPVLib.getPropertyString("track-list/$it/lang") ?: "" + /** + * When all subtitle/audio tracks are loaded, select the preferred one based on preferences, + * or select the first one in the list if trackSelect fails. + */ + fun onTrackListChanged(tracks: MPVNode) { + val tracks = tracks.toObject>(json).ifEmpty { return } + if (hasLoadedTracks.value) { + onTrackAdded(tracks) } else { - activity.stringResource(MR.strings.off) + hasLoadedTracks.update { _ -> true } + onTracksLoaded(tracks) } } - val getTrackTitle: (Int) -> String = { - if (it != -1) { - MPVLib.getPropertyString("track-list/$it/title") ?: "" - } else { - activity.stringResource(MR.strings.off) + + private fun onTrackAdded(tracks: List) { + val externalSubtitle = tracks.filter { + it.isSubtitle && it.title?.startsWith(VideoTrack.TRACK_TITLE_TAG) == true } - } - val getTrackMPVId: (Int) -> Int = { - if (it != -1) { - MPVLib.getPropertyInt("track-list/$it/id") - } else { - -1 + val externalAudio = tracks.filter { + it.isAudio && it.title?.startsWith(VideoTrack.TRACK_TITLE_TAG) == true } - } - val getTrackType: (Int) -> String? = { - MPVLib.getPropertyString("track-list/$it/type") - } - private var trackLoadingJob: Job? = null - fun loadTracks() { - trackLoadingJob?.cancel() - trackLoadingJob = viewModelScope.launch { - val possibleTrackTypes = listOf("audio", "sub") - val subTracks = mutableListOf() - val audioTracks = mutableListOf( - VideoTrack(-1, activity.stringResource(MR.strings.off), null), - ) - try { - val tracksCount = MPVLib.getPropertyInt("track-list/count") ?: 0 - for (i in 0.. subTracks.add(VideoTrack(getTrackMPVId(i), getTrackTitle(i), getTrackLanguage(i))) - "audio" -> audioTracks.add(VideoTrack(getTrackMPVId(i), getTrackTitle(i), getTrackLanguage(i))) - else -> error("Unrecognized track type") - } - } - } catch (e: NullPointerException) { - logcat(LogPriority.ERROR) { "Couldn't load tracks, probably cause mpv was destroyed" } - return@launch - } - _subtitleTracks.update { subTracks } - _audioTracks.update { audioTracks } + externalSubtitle.forEach { track -> + val idx = track.title!!.split("=")[1].toInt() + val external = externalSubtitleTracks.value[idx] - if (!isLoadingTracks.value) { - onFinishLoadingTracks() + if (external.id != null) { + // External subtitle has already been added + return@forEach } - } - } - /** - * When all subtitle/audio tracks are loaded, select the preferred one based on preferences, - * or select the first one in the list if trackSelect fails. - */ - fun onFinishLoadingTracks() { - val preferredSubtitle = trackSelect.getPreferredTrackIndex(subtitleTracks.value) - (preferredSubtitle ?: subtitleTracks.value.firstOrNull())?.let { - activity.player.sid = it.id - activity.player.secondarySid = -1 + updateSubtitleTrackAt(idx) { + it.copy(id = track.id, state = TrackState.Loaded) + } + selectSubById(track.id) } - val preferredAudio = trackSelect.getPreferredTrackIndex(audioTracks.value, subtitle = false) - (preferredAudio ?: audioTracks.value.getOrNull(1))?.let { - activity.player.aid = it.id - } + externalAudio.forEach { track -> + val idx = track.title!!.split("=")[1].toInt() + val external = externalAudioTracks.value[idx] - isLoadingTracks.update { _ -> true } - updateIsLoadingEpisode(false) - setPausedState() - } - - @Immutable - data class VideoTrack( - val id: Int, - val name: String, - val language: String?, - ) + if (external.id != null) { + // External audio has already been added + return@forEach + } - fun loadChapters() { - val chapters = mutableListOf() - val count = MPVLib.getPropertyInt("chapter-list/count")!! - for (i in 0 until count) { - val title = MPVLib.getPropertyString("chapter-list/$i/title") - val time = MPVLib.getPropertyInt("chapter-list/$i/time")!! - chapters.add( - IndexedSegment( - name = title, - start = time.toFloat(), - index = 0, - ), - ) + updateAudioTrackAt(idx) { + it.copy(id = track.id, state = TrackState.Loaded) + } + selectAudioById(track.id) } - updateChapters(chapters.sortedBy { it.start }) - } - - fun updateChapters(chapters: List) { - _chapters.update { _ -> chapters } } - fun selectChapter(index: Int) { - val time = chapters.value[index].start - seekTo(time.toInt()) - } + /** + * Called when embedded tracks are first loaded + */ + private fun onTracksLoaded(tracks: List) { + val embeddedSubs = tracks.filter { it.isSubtitle } + val embeddedAudio = tracks.filter { it.isAudio } + val externalSubs = currentVideo.value?.subtitleTracks.orEmpty().distinctBy { it.url } + .mapIndexed { idx, track -> VideoTrack.External(track, idx) } + val externalAudio = currentVideo.value?.audioTracks.orEmpty().distinctBy { it.url } + .mapIndexed { idx, track -> VideoTrack.External(track, idx) } + + _externalSubtitleTracks.update { _ -> externalSubs } + _externalAudioTracks.update { _ -> externalAudio } + + val preferredSubtitle = trackSelect.getPreferredTrackIndex( + tracks = embeddedSubs.map { VideoTrack.Internal(it) } + externalSubs, + subtitle = true, + ) + preferredSubtitle?.let { + selectSub(it) + } - fun updateChapter(index: Long) { - if (chapters.value.isEmpty() || index == -1L) return - _currentChapter.update { chapters.value.getOrNull(index.toInt()) ?: return } + val preferredAudio = trackSelect.getPreferredTrackIndex( + tracks = embeddedAudio.map { VideoTrack.Internal(it) } + externalAudio, + subtitle = false, + ) + preferredAudio?.let { + selectAudio(it) + } } fun addAudio(uri: Uri) { val url = uri.toString() val isContentUri = url.startsWith("content://") - val path = (if (isContentUri) uri.openContentFd(activity) else url) + val path = (if (isContentUri) uri.openContentFd(context.applicationContext) else url) ?: return - val name = if (isContentUri) uri.getFileName(activity) else null + val name = if (isContentUri) uri.getFileName(context.applicationContext) else null if (name == null) { - MPVLib.command(arrayOf("audio-add", path, "cached")) + mpv.command("audio-add", path, "cached") } else { - MPVLib.command(arrayOf("audio-add", path, "cached", name)) + mpv.command("audio-add", path, "cached", name) } } - fun selectAudio(id: Int) { - activity.player.aid = id - } - - fun updateAudio(id: Int) { - _selectedAudio.update { id } - } - fun addSubtitle(uri: Uri) { val url = uri.toString() val isContentUri = url.startsWith("content://") - val path = (if (isContentUri) uri.openContentFd(activity) else url) + val path = (if (isContentUri) uri.openContentFd(context.applicationContext) else url) ?: return - val name = if (isContentUri) uri.getFileName(activity) else null + val name = if (isContentUri) uri.getFileName(context.applicationContext) else null if (name == null) { - MPVLib.command(arrayOf("sub-add", path, "cached")) + mpv.command("sub-add", path, "cached") } else { - MPVLib.command(arrayOf("sub-add", path, "cached", name)) + mpv.command("sub-add", path, "cached", name) } } - fun selectSub(id: Int) { - val selectedSubs = selectedSubtitles.value - _selectedSubtitles.update { - when (id) { - selectedSubs.first -> Pair(selectedSubs.second, -1) - selectedSubs.second -> Pair(selectedSubs.first, -1) - else -> { - if (selectedSubs.first != -1) { - Pair(selectedSubs.first, id) - } else { - Pair(id, -1) + fun selectSub(track: VideoTrack) { + when (track) { + is VideoTrack.External -> { + if (track.id == null) { + updateSubtitleTrackAt(track.index) { + it.copy(state = TrackState.Loading) + } + viewModelScope.launchIO { + mpv.command( + "sub-add", + track.data.url, + "auto", + "${VideoTrack.TRACK_TITLE_TAG}=${track.index}", + ) } + } else { + selectSubById(track.id) } } + is VideoTrack.Internal -> { + selectSubById(track.data.id) + } + } + } + + fun selectAudio(track: VideoTrack) { + when (track) { + is VideoTrack.External -> { + if (track.id == null) { + updateAudioTrackAt(track.index) { + it.copy(state = TrackState.Loading) + } + viewModelScope.launchIO { + mpv.command( + "audio-add", + track.data.url, + "auto", + "${VideoTrack.TRACK_TITLE_TAG}=${track.index}", + ) + } + } else { + selectAudioById(track.id) + } + } + is VideoTrack.Internal -> { + selectAudioById(track.data.id) + } + } + } + + fun onTrackLoadedFailure(url: String) { + val subtitleIdx = externalSubtitleTracks.value.indexOfFirst { + it.data.url == url + } + if (subtitleIdx != -1) { + updateSubtitleTrackAt(subtitleIdx) { + it.copy(state = TrackState.Error) + } + } + val audioIdx = externalAudioTracks.value.indexOfFirst { + it.data.url == url + } + if (audioIdx != -1) { + updateAudioTrackAt(audioIdx) { + it.copy(state = TrackState.Error) + } + } + } + + fun onSubtitleTrackSelectChange() { + val id = mpv.getPropertyInt("sid") + val sid = mpv.getPropertyInt("secondary-sid") + + _externalSubtitleTracks.update { subtitleTracks -> + subtitleTracks.map { + it.copy( + mainSelection = when (it.id) { + null -> -1 + id -> 0 + sid -> 1 + else -> -1 + }, + ) + } + } + } + + private fun selectSubById(id: Int) { + val selectedSubs = Pair(mpv.getPropertyInt("sid"), mpv.getPropertyInt("secondary-sid")) + when (id) { + selectedSubs.first -> Pair(selectedSubs.second, null) + selectedSubs.second -> Pair(selectedSubs.first, null) + else -> if (selectedSubs.first != null) Pair(selectedSubs.first, id) else Pair(id, null) + }.let { + it.second?.let { mpv.setPropertyInt("secondary-sid", it) } + ?: mpv.setPropertyBoolean("secondary-sid", false) + it.first?.let { mpv.setPropertyInt("sid", it) } ?: mpv.setPropertyBoolean("sid", false) + } + } + + private fun selectAudioById(id: Int) { + if (id == mpv.getPropertyInt("aid")) { + mpv.setPropertyBoolean("aid", false) + } else { + mpv.setPropertyInt("aid", id) + } + } + + private fun updateSubtitleTrackAt(index: Int, transform: (VideoTrack.External) -> VideoTrack.External) { + _externalSubtitleTracks.update { externalSubtitles -> + externalSubtitles.toMutableList().apply { + this[index] = transform(this[index]) + } } - activity.player.secondarySid = _selectedSubtitles.value.second - activity.player.sid = _selectedSubtitles.value.first } - fun updateSubtitle(sid: Int, secondarySid: Int) { - _selectedSubtitles.update { Pair(sid, secondarySid) } + private fun updateAudioTrackAt(index: Int, transform: (VideoTrack.External) -> VideoTrack.External) { + _externalAudioTracks.update { externalAudio -> + externalAudio.toMutableList().apply { + this[index] = transform(this[index]) + } + } } - fun updatePlayBackPos(pos: Float) { - onSecondReached(pos.toInt(), duration.value.toInt()) - _pos.update { pos } + fun getChapterCount(): Int { + return mpv.getPropertyInt("chapter-list/count") ?: 0 } - fun updateReadAhead(value: Long) { - _readAhead.update { value.toFloat() } + fun addTimestamps(timestamps: List) { + if (timestamps.isEmpty()) return + val current = ( + mpv.getPropertyNode("chapter-list") + ?.toObject>(json) ?: emptyList() + ) + .map { IndexedSegment(name = it.chapterTitle, start = it.time, index = 0) } + val merged = ChapterUtils.mergeChapters(current, timestamps, duration) + + val node = MPVNode.ArrayNode( + merged.map { c -> + MPVNode.MapNode( + value = mapOf( + "time" to MPVNode.DoubleNode(c.start.toDouble()), + "title" to MPVNode.StringNode(c.name), + ), + ) + }.toTypedArray(), + ) + mpv.setPropertyNode("chapter-list", node) } private fun updatePausedState() { if (pausedState.value == null) { - _pausedState.update { _ -> paused.value } + _pausedState.update { _ -> paused } } } - private fun setPausedState() { + fun setPausedState() { pausedState.value?.let { if (it) { pause() } else { unpause() } - _pausedState.update { _ -> null } } } - fun pauseUnpause() { - if (paused.value) { - unpause() - } else { - pause() - } - } - - fun pause() { - activity.player.paused = true - _paused.update { true } - runCatching { - activity.setPictureInPictureParams(activity.createPipParams()) - } - } - - fun unpause() { - activity.player.paused = false - _paused.update { false } - } + fun pauseUnpause() = mpv.command("cycle", "pause") + fun pause() = mpv.setPropertyBoolean("pause", true) + fun unpause() = mpv.setPropertyBoolean("pause", false) private val showStatusBar = playerPreferences.showSystemStatusBar().get() fun showControls() { @@ -603,13 +680,17 @@ class PlayerViewModel @JvmOverloads constructor( return } if (showStatusBar) { - activity.windowInsetsController.show(WindowInsetsCompat.Type.statusBars()) + viewModelScope.launch { + eventChannel.send(Event.SetStatusBar(true)) + } } _controlsShown.update { true } } fun hideControls() { - activity.windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) + viewModelScope.launch { + eventChannel.send(Event.SetStatusBar(false)) + } _controlsShown.update { false } } @@ -673,20 +754,20 @@ class PlayerViewModel @JvmOverloads constructor( } fun seekBy(offset: Int, precise: Boolean = false) { - MPVLib.command(arrayOf("seek", offset.toString(), if (precise) "relative+exact" else "relative")) + mpv.command("seek", offset.toString(), if (precise) "relative+exact" else "relative") } fun seekTo(position: Int, precise: Boolean = true) { - if (position !in 0..(activity.player.duration ?: 0)) return - MPVLib.command(arrayOf("seek", position.toString(), if (precise) "absolute" else "absolute+keyframes")) + if (position !in 0..(mpv.getPropertyInt("duration") ?: 0)) return + mpv.command("seek", position.toString(), if (precise) "absolute" else "absolute+keyframes") } fun changeBrightnessTo( brightness: Float, ) { currentBrightness.update { _ -> brightness.coerceIn(-0.75f, 1f) } - activity.window.attributes = activity.window.attributes.apply { - screenBrightness = brightness.coerceIn(0f, 1f) + viewModelScope.launch { + eventChannel.send(Event.SetBrightness(brightness.coerceIn(0f, 1f))) } } @@ -694,13 +775,13 @@ class PlayerViewModel @JvmOverloads constructor( isBrightnessSliderShown.update { true } } - val maxVolume = activity.audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val maxVolume = audioManager.getMaxVolume() fun changeVolumeBy(change: Int) { - val mpvVolume = MPVLib.getPropertyInt("volume") - if (volumeBoostCap > 0 && currentVolume.value == maxVolume) { + val mpvVolume = mpv.getPropertyInt("volume") + if ((volumeBoostCap ?: audioPreferences.volumeBoostCap().get()) > 0 && currentVolume.value == maxVolume) { if (mpvVolume == 100 && change < 0) changeVolumeTo(currentVolume.value + change) - val finalMPVVolume = (mpvVolume + change).coerceAtLeast(100) - if (finalMPVVolume in 100..volumeBoostCap + 100) { + val finalMPVVolume = (mpvVolume?.plus(change))?.coerceAtLeast(100) ?: 100 + if (finalMPVVolume in 100..(volumeBoostCap ?: audioPreferences.volumeBoostCap().get()) + 100) { changeMPVVolumeTo(finalMPVVolume) return } @@ -710,21 +791,12 @@ class PlayerViewModel @JvmOverloads constructor( fun changeVolumeTo(volume: Int) { val newVolume = volume.coerceIn(0..maxVolume) - activity.audioManager.setStreamVolume( - AudioManager.STREAM_MUSIC, - newVolume, - 0, - ) + audioManager.setVolume(newVolume) currentVolume.update { newVolume } } fun changeMPVVolumeTo(volume: Int) { - MPVLib.setPropertyInt("volume", volume) - } - - fun setMPVVolume(volume: Int) { - if (volume != currentMPVVolume.value) displayVolumeSlider() - currentMPVVolume.update { volume } + mpv.setPropertyInt("volume", volume) } fun displayVolumeSlider() { @@ -743,45 +815,21 @@ class PlayerViewModel @JvmOverloads constructor( @Suppress("DEPRECATION") fun changeVideoAspect(aspect: VideoAspect) { - var ratio = -1.0 - var pan = 1.0 - when (aspect) { - VideoAspect.Crop -> { - pan = 1.0 - } - - VideoAspect.Fit -> { - pan = 0.0 - MPVLib.setPropertyDouble("panscan", 0.0) - } - - VideoAspect.Stretch -> { - val dm = DisplayMetrics() - activity.windowManager.defaultDisplay.getRealMetrics(dm) - ratio = dm.widthPixels / dm.heightPixels.toDouble() - pan = 0.0 - } + viewModelScope.launch { + eventChannel.send(Event.ChangeVideoAspect(aspect)) } - MPVLib.setPropertyDouble("panscan", pan) - MPVLib.setPropertyDouble("video-aspect-override", ratio) + } + + fun setAspect(aspect: VideoAspect, pan: Double, ratio: Double) { + mpv.setPropertyDouble("panscan", pan) + mpv.setPropertyDouble("video-aspect-override", ratio) playerPreferences.aspectState().set(aspect) playerUpdate.update { PlayerUpdates.AspectRatio } } fun cycleScreenRotations() { - activity.requestedOrientation = when (activity.requestedOrientation) { - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, - ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE, - ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, - -> { - playerPreferences.defaultPlayerOrientationType().set(PlayerOrientation.SensorPortrait) - ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - } - - else -> { - playerPreferences.defaultPlayerOrientationType().set(PlayerOrientation.SensorLandscape) - ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - } + viewModelScope.launch { + eventChannel.send(Event.CycleRotations) } } @@ -819,7 +867,7 @@ class PlayerViewModel @JvmOverloads constructor( _primaryButtonTitle.update { _ -> data } } "reset_button_title" -> { - _customButtons.value.getButtons().firstOrNull { it.isFavorite }?.let { + _customButtons.value.firstOrNull { it.isFavorite }?.let { setPrimaryCustomButtonTitle(it) } } @@ -831,7 +879,7 @@ class PlayerViewModel @JvmOverloads constructor( } "launch_int_picker" -> { val (title, nameFormat, start, stop, step, pickerProperty) = data.split("|") - val defaultValue = MPVLib.getPropertyInt(pickerProperty) + val defaultValue = mpv.getPropertyInt(pickerProperty)!! showDialog( Dialogs.IntegerPicker( defaultValue = defaultValue, @@ -840,11 +888,15 @@ class PlayerViewModel @JvmOverloads constructor( step = step.toInt(), nameFormat = nameFormat, title = title, - onChange = { MPVLib.setPropertyInt(pickerProperty, it) }, + onChange = { mpv.setPropertyInt(pickerProperty, it) }, onDismissRequest = { showDialog(Dialogs.None) }, ), ) } + "show_seek_text" -> { + val (forward, text) = data.split("|", limit = 2) + showSeekText(forward == "true", text) + } "pause" -> { when (data) { "pause" -> pause() @@ -866,7 +918,7 @@ class PlayerViewModel @JvmOverloads constructor( fun showButton() { if (_primaryButton.value == null) { _primaryButton.update { - customButtons.value.getButtons().firstOrNull { it.isFavorite } + customButtons.value.firstOrNull { it.isFavorite } } } } @@ -877,39 +929,36 @@ class PlayerViewModel @JvmOverloads constructor( "toggle" -> if (_primaryButton.value == null) showButton() else _primaryButton.update { null } } } - - "software_keyboard" -> when (data) { - "show" -> forceShowSoftwareKeyboard() - "hide" -> forceHideSoftwareKeyboard() - "toggle" -> if (inputMethodManager.isActive) { - forceHideSoftwareKeyboard() - } else { - forceShowSoftwareKeyboard() + "software_keyboard" -> { + viewModelScope.launch { + when (data) { + "show" -> eventChannel.send(Event.SetKeyboard(true)) + "hide" -> eventChannel.send(Event.SetKeyboard(false)) + "toggle" -> eventChannel.send(Event.ToggleKeyboard) + } } } } - MPVLib.setPropertyString(property, "") + mpv.setPropertyString(property, "") } private operator fun List.component6(): T = get(5) - private val inputMethodManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - private fun forceShowSoftwareKeyboard() { - inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) - } - - private fun forceHideSoftwareKeyboard() { - inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0) - } - private val doubleTapToSeekDuration = gesturePreferences.skipLengthPreference().get() private val preciseSeek = gesturePreferences.playerSmoothSeek().get() private val showSeekBar = gesturePreferences.showSeekBar().get() + private fun showSeekText(isForward: Boolean, text: String) { + _seekText.update { _ -> text } + _isSeekingForwards.update { _ -> isForward } + _doubleTapSeekAmount.update { _ -> if (isForward) 1 else -1 } + if (showSeekBar) showSeekBar() + } + private fun seekToWithText(seekValue: Int, text: String?) { _isSeekingForwards.value = seekValue > 0 - _doubleTapSeekAmount.value = seekValue - pos.value.toInt() + _doubleTapSeekAmount.value = seekValue - (pos ?: return) _seekText.update { _ -> text } seekTo(seekValue, preciseSeek) if (showSeekBar) showSeekBar() @@ -917,13 +966,7 @@ class PlayerViewModel @JvmOverloads constructor( private fun seekByWithText(value: Int, text: String?) { _doubleTapSeekAmount.update { - if (value < 0 && - (it < 0 || pos.value + value > duration.value) - ) { - 0 - } else { - it + value - } + if ((value < 0 && it < 0) || (pos ?: return) + value > (duration ?: return)) 0 else it + value } _seekText.update { text } _isSeekingForwards.value = value > 0 @@ -940,16 +983,14 @@ class PlayerViewModel @JvmOverloads constructor( } fun leftSeek() { - if (pos.value > 0) { - _doubleTapSeekAmount.value -= doubleTapToSeekDuration - } + if ((pos ?: return) > 0) _doubleTapSeekAmount.value -= doubleTapToSeekDuration _isSeekingForwards.value = false seekBy(-doubleTapToSeekDuration, preciseSeek) if (showSeekBar) showSeekBar() } fun rightSeek() { - if (pos.value < duration.value) { + if ((pos ?: return) < (duration ?: return)) { _doubleTapSeekAmount.value += doubleTapToSeekDuration } _isSeekingForwards.value = true @@ -966,20 +1007,24 @@ class PlayerViewModel @JvmOverloads constructor( } fun changeEpisode(previous: Boolean, autoPlay: Boolean = false) { - if (previous && !hasPreviousEpisode.value) { - activity.showToast(activity.stringResource(AYMR.strings.no_prev_episode)) - return - } + viewModelScope.launch { + if (previous && !hasPreviousEpisode.value) { + eventChannel.send(Event.ShowToast(AYMR.strings.no_prev_episode)) + return@launch + } - if (!previous && !hasNextEpisode.value) { - activity.showToast(activity.stringResource(AYMR.strings.no_next_episode)) - return - } + if (!previous && !hasNextEpisode.value) { + eventChannel.send(Event.ShowToast(AYMR.strings.no_next_episode)) + return@launch + } - activity.changeEpisode( - episodeId = getAdjacentEpisodeId(previous = previous), - autoPlay = autoPlay, - ) + eventChannel.send( + Event.ChangeEpisode( + episodeId = getAdjacentEpisodeId(previous = previous), + autoPlay = autoPlay, + ), + ) + } } fun handleLeftDoubleTap() { @@ -991,7 +1036,7 @@ class PlayerViewModel @JvmOverloads constructor( pauseUnpause() } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.DoubleTapLeft.keyCode)) + mpv.command("keypress", CustomKeyCodes.DoubleTapLeft.keyCode) } SingleActionGesture.None -> {} SingleActionGesture.Switch -> changeEpisode(true) @@ -1004,7 +1049,7 @@ class PlayerViewModel @JvmOverloads constructor( pauseUnpause() } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.DoubleTapCenter.keyCode)) + mpv.command("keypress", CustomKeyCodes.DoubleTapCenter.keyCode) } SingleActionGesture.Seek -> {} SingleActionGesture.None -> {} @@ -1021,7 +1066,7 @@ class PlayerViewModel @JvmOverloads constructor( pauseUnpause() } SingleActionGesture.Custom -> { - MPVLib.command(arrayOf("keypress", CustomKeyCodes.DoubleTapRight.keyCode)) + mpv.command("keypress", CustomKeyCodes.DoubleTapRight.keyCode) } SingleActionGesture.None -> {} SingleActionGesture.Switch -> changeEpisode(false) @@ -1046,7 +1091,7 @@ class PlayerViewModel @JvmOverloads constructor( private val downloadAheadAmount = downloadPreferences.autoDownloadWhileWatching().get() internal val relativeTime = uiPreferences.relativeTime().get() - internal val dateFormat = UiPreferences.dateFormat(uiPreferences.dateFormat().get()) + internal val dateFormat = uiPreferences.dateFormat().get() /** * The position in the current video. Used to restore from process kill. @@ -1232,9 +1277,9 @@ class PlayerViewModel @JvmOverloads constructor( _hasNextEpisode.update { _ -> getCurrentEpisodeIndex() != currentPlaylist.value.size - 1 } // Write to mpv table - MPVLib.setPropertyString("user-data/current-anime/anime-title", anime.title) - MPVLib.setPropertyInt("user-data/current-anime/intro-length", getAnimeSkipIntroLength()) - MPVLib.setPropertyString( + mpv.setPropertyString("user-data/current-anime/anime-title", anime.title) + mpv.setPropertyInt("user-data/current-anime/intro-length", getAnimeSkipIntroLength()) + mpv.setPropertyString( "user-data/current-anime/category", getCategories.await(anime.id).joinToString { it.name @@ -1280,7 +1325,7 @@ class PlayerViewModel @JvmOverloads constructor( private fun updateEpisode(episode: Episode) { mediaTitle.update { _ -> episode.name } _isEpisodeOnline.update { _ -> isEpisodeOnline() == true } - MPVLib.setPropertyDouble("user-data/current-anime/episode-number", episode.episode_number.toDouble()) + mpv.setPropertyDouble("user-data/current-anime/episode-number", episode.episode_number.toDouble()) } private fun initEpisodeList(anime: Anime): List { @@ -1401,6 +1446,30 @@ class PlayerViewModel @JvmOverloads constructor( } } + fun setIsStopped(value: Boolean) { + _isStopped.update { _ -> value } + } + + fun setCurrentVideoError() { + val (hosterIdx, videoIdx) = selectedHosterVideoIndex.value + val currentHosterState = (hosterState.value[hosterIdx] as? HosterState.Ready) ?: return + val currentVideo = currentHosterState.videoList[videoIdx] + + _hosterState.updateAt( + hosterIdx, + currentHosterState.getChangedAt(videoIdx, currentVideo, Video.State.ERROR), + ) + } + + fun loadBestVideo() { + val source = currentSource.value ?: return + val (hosterIdx, videoIdx) = HosterLoader.selectBestVideo(hosterState.value) + val newVideo = (hosterState.value[hosterIdx] as HosterState.Ready).videoList[videoIdx] + viewModelScope.launchIO { + loadVideo(source, newVideo, hosterIdx, videoIdx) + } + } + private suspend fun loadVideo(source: AnimeSource?, video: Video, hosterIndex: Int, videoIndex: Int): Boolean { val selectedHosterState = (_hosterState.value[hosterIndex] as? HosterState.Ready) ?: return false updateIsLoadingEpisode(true) @@ -1462,7 +1531,7 @@ class PlayerViewModel @JvmOverloads constructor( qualityIndex = Pair(hosterIndex, videoIndex) - activity.setVideo(resolvedVideo) + eventChannel.send(Event.SetVideo(resolvedVideo)) return true } @@ -1486,8 +1555,6 @@ class PlayerViewModel @JvmOverloads constructor( if (sheetShown.value == Sheets.QualityTracks) { dismissSheet() } - } else { - updateIsLoadingEpisode(false) } } } @@ -1565,14 +1632,24 @@ class PlayerViewModel @JvmOverloads constructor( * Called every time a second is reached in the player. Used to mark the flag of episode being * seen, update tracking services, enqueue downloaded episode deletion and download next episode. */ - private fun onSecondReached(position: Int, duration: Int) { + fun onSecondReached(position: Long) { if (isLoadingEpisode.value) return val currentEp = currentEpisode.value ?: return if (episodeId == -1L) return - if (duration == 0) return + val dur = duration ?: return + if (dur == 0) return + + // Set netflix-style timeout + netflixTimeout.value?.let { + if (it > 0) { + netflixTimeout.value = it - 1 + } else { + onSkipIntro() + } + } val seconds = position * 1000L - val totalSeconds = duration * 1000L + val totalSeconds = dur * 1000L // Save last second seen and mark as seen if needed currentEp.last_second_seen = seconds currentEp.total_seconds = totalSeconds @@ -1759,7 +1836,7 @@ class PlayerViewModel @JvmOverloads constructor( val filename = cachePath + "/${System.currentTimeMillis()}_mpv_screenshot_tmp.png" val subtitleFlag = if (showSubtitles) "subtitles" else "video" - MPVLib.command(arrayOf("screenshot-to-file", filename, subtitleFlag)) + mpv.command("screenshot-to-file", filename, subtitleFlag) val tempFile = File(filename).takeIf { it.exists() } ?: return null val newFile = File("$cachePath/mpv_screenshot.png") @@ -1808,7 +1885,7 @@ class PlayerViewModel @JvmOverloads constructor( * Shares the screenshot and notifies the UI with the path of the file to share. * The image must be first copied to the internal partition because there are many possible * formats it can come from, like a zipped chapter, in which case it's not possible to directly - * get a path to the file and it has to be decompressed somewhere first. Only the last shared + * get a path to the file, and it has to be decompressed somewhere first. Only the last shared * image will be kept so it won't be taking lots of internal disk space. */ fun shareImage(imageStream: () -> InputStream, timePos: Int?) { @@ -1857,7 +1934,7 @@ class PlayerViewModel @JvmOverloads constructor( } else { SetAsArt.AddToLibraryFirst } - } catch (e: Exception) { + } catch (_: Exception) { SetAsArt.Error } eventChannel.send(Event.SetArtResult(result, artType)) @@ -1965,7 +2042,7 @@ class PlayerViewModel @JvmOverloads constructor( return null } - getTracks.await(animeId).map { track -> + getTracks.await(animeId).forEach { track -> val tracker = trackerManager.get(track.trackerId) malId = when (tracker) { is MyAnimeList -> track.remoteId @@ -1985,99 +2062,73 @@ class PlayerViewModel @JvmOverloads constructor( private val netflixStyle = playerPreferences.enableNetflixStyleIntroSkip().get() private val defaultWaitingTime = playerPreferences.waitingTimeIntroSkip().get() - var waitingSkipIntro = defaultWaitingTime - fun setChapter(position: Float) { - getCurrentChapter(position)?.let { (chapterIndex, chapter) -> - if (currentChapter.value != chapter) { - _currentChapter.update { _ -> chapter } - } + fun onChapterChanged(chapterIndex: Int?) { + if (chapterIndex == null) return + if (!introSkipEnabled) return - if (!introSkipEnabled) { - return - } + val chapterList = (mpv.getPropertyNode("chapter-list")?.toObject>(json) ?: emptyList()) + val chapter = chapterList.getOrNull(chapterIndex) ?: return + val chapterType = chapter.chapterType - if (chapter.chapterType == ChapterType.Other) { - _skipIntroText.update { _ -> null } - waitingSkipIntro = defaultWaitingTime - } else { - val nextChapterPos = chapters.value.getOrNull(chapterIndex + 1)?.start ?: pos.value - - if (netflixStyle) { - // show a toast with the seconds before the skip - if (waitingSkipIntro == defaultWaitingTime) { - activity.showToast( - "Skip Intro: ${activity.stringResource( - AYMR.strings.player_aniskip_dontskip_toast, - chapter.name, - waitingSkipIntro, - )}", - ) - } - showSkipIntroButton(chapter, nextChapterPos, waitingSkipIntro) - waitingSkipIntro-- - } else if (autoSkip) { - seekToWithText( - seekValue = nextChapterPos.toInt(), - text = activity.stringResource(AYMR.strings.player_intro_skipped, chapter.name), - ) - } else { - updateSkipIntroButton(chapter.chapterType) + if (chapterType == ChapterType.Other) { + _skipIntroText.update { _ -> null } + netflixTimeout.update { _ -> null } + } else { + if (netflixStyle) { + // show a toast with the seconds before the skip + eventChannel.trySend( + Event.ShowToastString( + "Skip Intro: ${context.applicationContext.stringResource( + AYMR.strings.player_aniskip_dontskip_toast, + chapter.chapterTitle, + defaultWaitingTime, + )}", + ), + ) + _skipIntroText.update { _ -> + context.applicationContext.stringResource(AYMR.strings.player_aniskip_dontskip) } + netflixTimeout.update { _ -> defaultWaitingTime } + } else if (autoSkip) { + skipIntro(chapter.chapterTitle) + } else { + updateSkipIntroButton(chapterType) } } } + private fun skipIntro(chapterName: String) { + mpv.command("add", "chapter", "1") + showSeekText(true, context.applicationContext.stringResource(AYMR.strings.player_intro_skipped, chapterName)) + } + private fun updateSkipIntroButton(chapterType: ChapterType) { val skipButtonString = chapterType.getStringRes() _skipIntroText.update { _ -> skipButtonString?.let { - activity.stringResource( + context.applicationContext.stringResource( AYMR.strings.player_skip_action, - activity.stringResource(skipButtonString), + context.applicationContext.stringResource(skipButtonString), ) } } } - private fun showSkipIntroButton(chapter: IndexedSegment, nextChapterPos: Float, waitingTime: Int) { - if (waitingTime > -1) { - if (waitingTime > 0) { - _skipIntroText.update { _ -> activity.stringResource(AYMR.strings.player_aniskip_dontskip) } - } else { - seekToWithText( - seekValue = nextChapterPos.toInt(), - text = activity.stringResource(AYMR.strings.player_aniskip_skip, chapter.name), - ) - } - } else { - // when waitingTime is -1, it means that the user cancelled the skip - updateSkipIntroButton(chapter.chapterType) - } - } - fun onSkipIntro() { - getCurrentChapter()?.let { (chapterIndex, chapter) -> - // this stops the counter - if (waitingSkipIntro > 0 && netflixStyle) { - waitingSkipIntro = -1 - return - } + val chapterIndex = mpv.getPropertyInt("chapter") ?: return + val chapterList = (mpv.getPropertyNode("chapter-list")?.toObject>(json) ?: emptyList()) + val chapter = chapterList.getOrNull(chapterIndex) ?: return - val nextChapterPos = chapters.value.getOrNull(chapterIndex + 1)?.start ?: pos.value - - seekToWithText( - seekValue = nextChapterPos.toInt(), - text = activity.stringResource(AYMR.strings.player_aniskip_skip, chapter.name), - ) + if ((netflixTimeout.value ?: 0) > 0 && netflixStyle) { + netflixTimeout.update { _ -> null } + updateSkipIntroButton(chapter.chapterType) + return } - } - private fun getCurrentChapter(position: Float? = null): IndexedValue? { - return chapters.value.withIndex() - .filter { it.value.start <= (position ?: pos.value) } - .maxByOrNull { it.value.start } + netflixTimeout.update { _ -> null } + skipIntro(chapter.chapterTitle) } fun setPrimaryCustomButtonTitle(button: CustomButton) { @@ -2088,15 +2139,25 @@ class PlayerViewModel @JvmOverloads constructor( data class SetArtResult(val result: SetAsArt, val artType: ArtType) : Event() data class SavedImage(val result: SaveImageResult) : Event() data class ShareImage(val uri: Uri, val seconds: String) : Event() + data class ShowToast(val stringResource: StringResource) : Event() + data class ShowToastString(val string: String) : Event() + data class ChangeEpisode(val episodeId: Long, val autoPlay: Boolean) : Event() + data class SetVideo(val video: Video?) : Event() + data class SetStatusBar(val show: Boolean) : Event() + data class SetBrightness(val brightness: Float) : Event() + data class ChangeVideoAspect(val aspect: VideoAspect) : Event() + data object CycleRotations : Event() + data object ToggleKeyboard : Event() + data class SetKeyboard(val show: Boolean) : Event() } } -fun CustomButton.execute() { - MPVLib.command(arrayOf("script-message", "call_button_$id")) +fun CustomButton.execute(mpv: MPV) { + mpv.command("script-message", "call_button_$id") } -fun CustomButton.executeLongPress() { - MPVLib.command(arrayOf("script-message", "call_button_${id}_long")) +fun CustomButton.executeLongPress(mpv: MPV) { + mpv.command("script-message", "call_button_${id}_long") } fun Float.normalize(inMin: Float, inMax: Float, outMin: Float, outMax: Float): Float { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomLeftPlayerControls.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomLeftPlayerControls.kt index 34ee10b99c..5c2e52ebcd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomLeftPlayerControls.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomLeftPlayerControls.kt @@ -26,31 +26,26 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.ScreenRotation import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.vivvvek.seeker.Segment import eu.kanade.tachiyomi.ui.player.Sheets import eu.kanade.tachiyomi.ui.player.controls.components.ControlsButton import eu.kanade.tachiyomi.ui.player.controls.components.CurrentChapter -import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.i18n.stringResource -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get @Composable fun BottomLeftPlayerControls( playbackSpeed: Float, currentChapter: Segment?, + showChapterIndicator: Boolean, onLockControls: () -> Unit, onCycleRotation: () -> Unit, onPlaybackSpeedChange: (Float) -> Unit, onOpenSheet: (Sheets) -> Unit, modifier: Modifier = Modifier, ) { - val playerPreferences = remember { Injekt.get() } - Row( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -65,20 +60,16 @@ fun BottomLeftPlayerControls( ) ControlsButton( text = stringResource(AYMR.strings.player_speed, playbackSpeed), - onClick = { - val newSpeed = if (playbackSpeed >= 2) 0.25f else playbackSpeed + 0.25f - onPlaybackSpeedChange(newSpeed) - playerPreferences.playerSpeed().set(newSpeed) - }, + onClick = { onPlaybackSpeedChange(if (playbackSpeed >= 2) 0.25f else playbackSpeed + 0.25f) }, onLongClick = { onOpenSheet(Sheets.PlaybackSpeed) }, ) AnimatedVisibility( - currentChapter != null && playerPreferences.showCurrentChapter().get(), + showChapterIndicator && currentChapter != null, enter = fadeIn(), exit = fadeOut(), ) { CurrentChapter( - chapter = currentChapter!!, + chapter = currentChapter ?: return@AnimatedVisibility, onClick = { onOpenSheet(Sheets.Chapters) }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomRightPlayerControls.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomRightPlayerControls.kt index dc30ac8c09..cf87965ef5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomRightPlayerControls.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/BottomRightPlayerControls.kt @@ -25,8 +25,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import eu.kanade.tachiyomi.ui.player.controls.components.ControlsButton import eu.kanade.tachiyomi.ui.player.controls.components.FilledControlsButton -import eu.kanade.tachiyomi.ui.player.execute -import eu.kanade.tachiyomi.ui.player.executeLongPress import tachiyomi.domain.custombutton.model.CustomButton @Composable @@ -36,6 +34,8 @@ fun BottomRightPlayerControls( skipIntroButton: String?, onPressSkipIntroButton: () -> Unit, isPipAvailable: Boolean, + onCustomButtonClick: () -> Unit, + onCustomButtonLongClick: () -> Unit, onAspectClick: () -> Unit, onPipClick: () -> Unit, modifier: Modifier = Modifier, @@ -50,8 +50,8 @@ fun BottomRightPlayerControls( } else if (customButton != null) { FilledControlsButton( text = customButtonTitle, - onClick = customButton::execute, - onLongClick = customButton::executeLongPress, + onClick = onCustomButtonClick, + onLongClick = onCustomButtonLongClick, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/GestureHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/GestureHandler.kt index 9786b08abd..c715a19cf2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/GestureHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/GestureHandler.kt @@ -64,7 +64,6 @@ import eu.kanade.tachiyomi.ui.player.controls.components.DoubleTapSeekTriangles import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences import eu.kanade.tachiyomi.ui.player.settings.GesturePreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences -import `is`.xyz.mpv.MPVLib import kotlinx.coroutines.delay import kotlinx.coroutines.flow.update import tachiyomi.i18n.aniyomi.AYMR @@ -85,8 +84,10 @@ fun GestureHandler( val panelShown by viewModel.panelShown.collectAsState() val allowGesturesInPanels by playerPreferences.allowGestures().collectAsState() - val duration by viewModel.duration.collectAsState() - val position by viewModel.pos.collectAsState() + val paused by viewModel.mpv.propFlow("pause").collectAsState() + val duration by viewModel.mpv.propFlow("duration").collectAsState() + val position by viewModel.mpv.propFlow("time-pos").collectAsState() + val playbackSpeed by viewModel.mpv.propFlow("speed").collectAsState() val controlsShown by viewModel.controlsShown.collectAsState() val areControlsLocked by viewModel.areControlsLocked.collectAsState() val seekAmount by viewModel.doubleTapSeekAmount.collectAsState() @@ -109,7 +110,7 @@ fun GestureHandler( val showSeekbar by gesturePreferences.showSeekBar().collectAsState() var isLongPressing by remember { mutableStateOf(false) } val currentVolume by viewModel.currentVolume.collectAsState() - val currentMPVVolume by viewModel.currentMPVVolume.collectAsState() + val currentMPVVolume by viewModel.mpv.propFlow("volume").collectAsState() val currentBrightness by viewModel.currentBrightness.collectAsState() val volumeBoostingCap = audioPreferences.volumeBoostCap().get() val haptics = LocalHapticFeedback.current @@ -119,7 +120,7 @@ fun GestureHandler( .fillMaxSize() .windowInsetsPadding(WindowInsets.safeGestures) .pointerInput(Unit) { - val originalSpeed = viewModel.playbackSpeed.value + val originalSpeed = viewModel.mpv.getPropertyFloat("speed") ?: 1f detectTapGestures( onTap = { if (controlsShown) viewModel.hideControls() else viewModel.showControls() @@ -162,7 +163,7 @@ fun GestureHandler( tryAwaitRelease() if (isLongPressing) { isLongPressing = false - MPVLib.setPropertyDouble("speed", originalSpeed.toDouble()) + viewModel.mpv.setPropertyFloat("speed", originalSpeed) viewModel.playerUpdate.update { PlayerUpdates.None } } interactionSource.emit(PressInteraction.Release(press)) @@ -180,14 +181,14 @@ fun GestureHandler( } .pointerInput(areControlsLocked) { if (!seekGesture || areControlsLocked) return@pointerInput - var startingPosition = position.toInt() + var startingPosition = position ?: 0 var startingX = 0f var wasPlayerAlreadyPause = false detectHorizontalDragGestures( onDragStart = { - startingPosition = position.toInt() + startingPosition = position ?: 0 startingX = it.x - wasPlayerAlreadyPause = viewModel.paused.value + wasPlayerAlreadyPause = paused ?: false viewModel.pause() }, onDragEnd = { @@ -196,17 +197,17 @@ fun GestureHandler( if (!wasPlayerAlreadyPause) viewModel.unpause() }, ) { change, dragAmount -> - if (position <= 0f && dragAmount < 0) return@detectHorizontalDragGestures - if (position >= duration && dragAmount > 0) return@detectHorizontalDragGestures + if ((position ?: 0) <= 0f && dragAmount < 0) return@detectHorizontalDragGestures + if ((position ?: 0) >= (duration ?: 0) && dragAmount > 0) return@detectHorizontalDragGestures calculateNewHorizontalGestureValue(startingPosition, startingX, change.position.x, 0.15f).let { viewModel.gestureSeekAmount.update { _ -> Pair( startingPosition, (it - startingPosition) - .coerceIn(0 - startingPosition, (duration - startingPosition).toInt()), + .coerceIn(0 - startingPosition, ((duration ?: 0) - startingPosition)), ) } - viewModel.seekTo(it.coerceIn(0, duration.toInt()), preciseSeeking) + viewModel.seekTo(it.coerceIn(0, (duration ?: 0)), preciseSeeking) } if (showSeekbar) viewModel.showSeekBar() @@ -225,13 +226,13 @@ fun GestureHandler( val isIncreasingVolumeBoost: (Float) -> Boolean = { volumeBoostingCap > 0 && currentVolume == viewModel.maxVolume && - currentMPVVolume - 100 < volumeBoostingCap && + (currentMPVVolume ?: 100) - 100 < volumeBoostingCap && it < 0 } val isDecreasingVolumeBoost: (Float) -> Boolean = { volumeBoostingCap > 0 && currentVolume == viewModel.maxVolume && - currentMPVVolume - 100 in 1..volumeBoostingCap && + (currentMPVVolume ?: 100) - 100 in 1..volumeBoostingCap && it > 0 } detectVerticalDragGestures( @@ -253,7 +254,7 @@ fun GestureHandler( } viewModel.changeMPVVolumeTo( calculateNewVerticalGestureValue( - originalMPVVolume, + originalMPVVolume ?: 100, mpvVolumeStartingY, change.position.y, mpvVolumeGestureSens, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/MiddlePlayerControls.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/MiddlePlayerControls.kt index 8f07c54816..cb54bd8eeb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/MiddlePlayerControls.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/MiddlePlayerControls.kt @@ -11,8 +11,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SkipNext @@ -46,6 +48,7 @@ fun MiddlePlayerControls( onSkipPrevious: () -> Unit, // middle + isStopped: Boolean, isLoading: Boolean, isLoadingEpisode: Boolean, controlsShown: Boolean, @@ -86,6 +89,9 @@ fun MiddlePlayerControls( val icon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_play_to_pause) val interaction = remember { MutableInteractionSource() } when { + isStopped -> { + Spacer(Modifier.width(96.dp)) + } gestureSeekAmount != null -> { Text( stringResource( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt index 6c1e538c27..386e459a03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerControls.kt @@ -17,6 +17,7 @@ package eu.kanade.tachiyomi.ui.player.controls +import androidx.activity.compose.LocalActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FiniteAnimationSpec @@ -42,6 +43,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -50,13 +52,15 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import eu.kanade.presentation.more.settings.screen.player.custombutton.getButtons +import androidx.core.graphics.toColorInt import eu.kanade.presentation.theme.playerRippleConfiguration +import eu.kanade.tachiyomi.ui.player.DebandSettings +import eu.kanade.tachiyomi.ui.player.Debanding +import eu.kanade.tachiyomi.ui.player.Decoder.Companion.getDecoderFromValue import eu.kanade.tachiyomi.ui.player.Dialogs import eu.kanade.tachiyomi.ui.player.Panels import eu.kanade.tachiyomi.ui.player.PlayerActivity @@ -64,27 +68,45 @@ import eu.kanade.tachiyomi.ui.player.PlayerUpdates import eu.kanade.tachiyomi.ui.player.PlayerViewModel import eu.kanade.tachiyomi.ui.player.Sheets import eu.kanade.tachiyomi.ui.player.VideoAspect +import eu.kanade.tachiyomi.ui.player.VideoFilters +import eu.kanade.tachiyomi.ui.player.VideoTrack import eu.kanade.tachiyomi.ui.player.controls.components.BrightnessOverlay import eu.kanade.tachiyomi.ui.player.controls.components.BrightnessSlider import eu.kanade.tachiyomi.ui.player.controls.components.ControlsButton import eu.kanade.tachiyomi.ui.player.controls.components.SeekbarWithTimers import eu.kanade.tachiyomi.ui.player.controls.components.TextPlayerUpdate import eu.kanade.tachiyomi.ui.player.controls.components.VolumeSlider +import eu.kanade.tachiyomi.ui.player.controls.components.panels.SubColorType +import eu.kanade.tachiyomi.ui.player.controls.components.panels.SubtitlesBorderStyle +import eu.kanade.tachiyomi.ui.player.controls.components.panels.resetColors +import eu.kanade.tachiyomi.ui.player.controls.components.panels.resetTypography +import eu.kanade.tachiyomi.ui.player.controls.components.panels.toColorHexString import eu.kanade.tachiyomi.ui.player.controls.components.sheets.toFixed +import eu.kanade.tachiyomi.ui.player.execute +import eu.kanade.tachiyomi.ui.player.executeLongPress +import eu.kanade.tachiyomi.ui.player.settings.AdvancedPlayerPreferences +import eu.kanade.tachiyomi.ui.player.settings.AudioChannels import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences +import eu.kanade.tachiyomi.ui.player.settings.DecoderPreferences import eu.kanade.tachiyomi.ui.player.settings.GesturePreferences import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences -import `is`.xyz.mpv.MPVLib +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.update +import tachiyomi.core.common.preference.deleteAndGet +import tachiyomi.core.common.preference.minusAssign +import tachiyomi.core.common.preference.plusAssign import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.math.roundToInt @Suppress("CompositionLocalAllowlist") val LocalPlayerButtonsClickEvent = staticCompositionLocalOf { {} } @@ -97,6 +119,8 @@ fun PlayerControls( ) { val spacing = MaterialTheme.padding val playerPreferences = remember { Injekt.get() } + val advancedPreferences = remember { Injekt.get() } + val decoderPreferences = remember { Injekt.get() } val gesturePreferences = remember { Injekt.get() } val audioPreferences = remember { Injekt.get() } val subtitlePreferences = remember { Injekt.get() } @@ -105,16 +129,21 @@ fun PlayerControls( val controlsShown by viewModel.controlsShown.collectAsState() val areControlsLocked by viewModel.areControlsLocked.collectAsState() val seekBarShown by viewModel.seekBarShown.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() val isLoadingEpisode by viewModel.isLoadingEpisode.collectAsState() - val duration by viewModel.duration.collectAsState() - val position by viewModel.pos.collectAsState() - val paused by viewModel.paused.collectAsState() + val isStopped by viewModel.isStopped.collectAsState() + val pausedForCache by viewModel.mpv.propFlow("paused-for-cache").collectAsState() + val coreIdle by viewModel.mpv.propFlow("core-idle").collectAsState() + val paused by viewModel.mpv.propFlow("pause").collectAsState() + val duration by viewModel.mpv.propFlow("duration").collectAsState() + val position by viewModel.mpv.propFlow("time-pos").collectAsState() + val playbackSpeed by viewModel.mpv.propFlow("speed").collectAsState() val gestureSeekAmount by viewModel.gestureSeekAmount.collectAsState() val doubleTapSeekAmount by viewModel.doubleTapSeekAmount.collectAsState() val seekText by viewModel.seekText.collectAsState() - val currentChapter by viewModel.currentChapter.collectAsState() - val chapters by viewModel.chapters.collectAsState() + val currentChapter by viewModel.mpv.propFlow("chapter").collectAsState() + val mpvDecoder by viewModel.mpv.propFlow("hwdec-current").collectAsState() + val decoder by remember { derivedStateOf { getDecoderFromValue(mpvDecoder ?: "auto") } } + val chapters by viewModel.chapters.collectAsState(persistentListOf()) val currentBrightness by viewModel.currentBrightness.collectAsState() val playerTimeToDisappear by playerPreferences.playerTimeToDisappear().collectAsState() @@ -130,7 +159,7 @@ fun PlayerControls( isSeeking, resetControls, ) { - if (controlsShown && !paused && !isSeeking) { + if (controlsShown && paused == false && !isSeeking) { delay(playerTimeToDisappear.toLong()) viewModel.hideControls() } @@ -182,7 +211,7 @@ fun PlayerControls( val isVolumeSliderShown by viewModel.isVolumeSliderShown.collectAsState() val brightness by viewModel.currentBrightness.collectAsState() val volume by viewModel.currentVolume.collectAsState() - val mpvVolume by viewModel.currentMPVVolume.collectAsState() + val mpvVolume by viewModel.mpv.propFlow("volume").collectAsState() val swapVolumeAndBrightness by gesturePreferences.swapVolumeBrightness().collectAsState() val reduceMotion by playerPreferences.reduceMotion().collectAsState() @@ -273,7 +302,7 @@ fun PlayerControls( val displayVolumeAsPercentage by playerPreferences.displayVolPer().collectAsState() VolumeSlider( volume = volume, - mpvVolume = mpvVolume, + mpvVolume = mpvVolume ?: 100, range = 0..viewModel.maxVolume, boostRange = if (boostCap > 0) 0..audioPreferences.volumeBoostCap().get() else null, displayAsPercentage = displayVolumeAsPercentage, @@ -328,7 +357,7 @@ fun PlayerControls( AnimatedVisibility( visible = (controlsShown && (!areControlsLocked || gestureSeekAmount != null)) || - isLoading || + (pausedForCache == true || (coreIdle == true && paused == false)) || isLoadingEpisode, enter = fadeIn(playerControlsEnterAnimationSpec()), exit = fadeOut(playerControlsExitAnimationSpec()), @@ -345,12 +374,13 @@ fun PlayerControls( onSkipPrevious = { viewModel.changeEpisode(true) }, hasNext = hasNextEpisode, onSkipNext = { viewModel.changeEpisode(false) }, - isLoading = isLoading, + isStopped = isStopped, + isLoading = pausedForCache == true || (coreIdle == true && paused == false), isLoadingEpisode = isLoadingEpisode, controlsShown = controlsShown, areControlsLocked = areControlsLocked, showLoadingCircle = showLoadingCircle, - paused = paused, + paused = paused == true, gestureSeekAmount = gestureSeekAmount, onPlayPauseClick = viewModel::pauseUnpause, enter = fadeIn(playerControlsEnterAnimationSpec()), @@ -376,22 +406,23 @@ fun PlayerControls( }, ) { val invertDuration by playerPreferences.invertDuration().collectAsState() - val readAhead by viewModel.readAhead.collectAsState() + val readAhead by viewModel.mpv.propFlow("demuxer-cache-time").collectAsState() + val remaining by viewModel.mpv.propFlow("playtime-remaining").collectAsState() val preciseSeeking by gesturePreferences.playerSmoothSeek().collectAsState() SeekbarWithTimers( - position = position, - duration = duration, - readAheadValue = readAhead, + position = position?.toFloat() ?: 0f, + duration = duration?.toFloat() ?: 0f, + remaining = remaining ?: 0f, + readAheadValue = readAhead ?: 0f, onValueChange = { isSeeking = true - viewModel.updatePlayBackPos(it) - viewModel.seekTo(it.toInt(), preciseSeeking) + viewModel.seekTo(it.roundToInt(), preciseSeeking) }, onValueChangeFinished = { isSeeking = false }, timersInverted = Pair(false, invertDuration), durationTimerOnCLick = { playerPreferences.invertDuration().set(!invertDuration) }, positionTimerOnClick = {}, - chapters = chapters.map { it.toSegment() }.toImmutableList(), + chapters = chapters, ) } val mediaTitle by viewModel.mediaTitle.collectAsState() @@ -481,7 +512,7 @@ fun PlayerControls( end.linkTo(seekbar.end) }, ) { - val activity = LocalContext.current as PlayerActivity + val activity = LocalActivity.current as PlayerActivity BottomRightPlayerControls( customButton = customButton, customButtonTitle = customButtonTitle, @@ -493,6 +524,12 @@ fun PlayerControls( activity.enterPictureInPictureMode(activity.createPipParams()) } }, + onCustomButtonClick = { + customButton?.execute(viewModel.mpv) + }, + onCustomButtonLongClick = { + customButton?.executeLongPress(viewModel.mpv) + }, onAspectClick = { viewModel.changeVideoAspect( when (aspectRatio) { @@ -505,7 +542,6 @@ fun PlayerControls( ) } // Bottom left controls - val playbackSpeed by viewModel.playbackSpeed.collectAsState() AnimatedVisibility( controlsShown && !areControlsLocked, enter = if (!reduceMotion) { @@ -527,13 +563,16 @@ fun PlayerControls( end.linkTo(bottomRightControls.start) }, ) { + val showChapterIndicator by playerPreferences.showCurrentChapter().collectAsState() BottomLeftPlayerControls( - playbackSpeed, - currentChapter = currentChapter?.toSegment(), + playbackSpeed = playbackSpeed ?: playerPreferences.playerSpeed().get(), + showChapterIndicator = showChapterIndicator, + currentChapter = chapters.getOrNull(currentChapter ?: 0), onLockControls = viewModel::lockControls, onCycleRotation = viewModel::cycleScreenRotations, onPlaybackSpeedChange = { - MPVLib.setPropertyDouble("speed", it.toDouble()) + viewModel.mpv.setPropertyFloat("speed", it) + playerPreferences.playerSpeed().set(it) }, onOpenSheet = viewModel::showSheet, ) @@ -543,64 +582,113 @@ fun PlayerControls( val sheetShown by viewModel.sheetShown.collectAsState() val dismissSheet by viewModel.dismissSheet.collectAsState() - val subtitles by viewModel.subtitleTracks.collectAsState() - val selectedSubtitles by viewModel.selectedSubtitles.collectAsState() - val audioTracks by viewModel.audioTracks.collectAsState() - val selectedAudio by viewModel.selectedAudio.collectAsState() val isLoadingHosters by viewModel.isLoadingHosters.collectAsState() val hosterState by viewModel.hosterState.collectAsState() val expandedState by viewModel.hosterExpandedList.collectAsState() val selectedHosterVideoIndex by viewModel.selectedHosterVideoIndex.collectAsState() - val decoder by viewModel.currentDecoder.collectAsState() - val speed by viewModel.playbackSpeed.collectAsState() val sleepTimerTimeRemaining by viewModel.remainingTime.collectAsState() + val speedPresets by playerPreferences.speedPresets().collectAsState() + val showSubtitles by subtitlePreferences.screenshotSubtitles().collectAsState() val currentSource by viewModel.currentSource.collectAsState() val showFailedHosters by playerPreferences.showFailedHosters().collectAsState() val emptyHosters by playerPreferences.showEmptyHosters().collectAsState() + val internalSubtitles by viewModel.subtitleTracks.collectAsState(persistentListOf()) + val externalSubtitles by viewModel.externalSubtitleTracks.collectAsState() + val subtitles = remember(internalSubtitles, externalSubtitles) { + internalSubtitles.map { VideoTrack.Internal(it) } + externalSubtitles + } + + val audioChannels by audioPreferences.audioChannels().collectAsState() + val pitchCorrection by audioPreferences.enablePitchCorrection().collectAsState() + val mpvAudioPitchCorrection by viewModel.mpv.propFlow("audio-pitch-correction").collectAsState() + val internalAudioTracks by viewModel.audioTracks.collectAsState(persistentListOf()) + val externalAudioTracks by viewModel.externalAudioTracks.collectAsState() + val audioTracks = remember(internalAudioTracks, externalAudioTracks) { + internalAudioTracks.map { VideoTrack.Internal(it) } + externalAudioTracks + } + + val statisticsPage by advancedPreferences.playerStatisticsPage().collectAsState() + PlayerSheets( sheetShown = sheetShown, subtitles = subtitles.toImmutableList(), - selectedSubtitles = selectedSubtitles.toList().toImmutableList(), onAddSubtitle = viewModel::addSubtitle, onSelectSubtitle = viewModel::selectSub, audioTracks = audioTracks.toImmutableList(), - selectedAudio = selectedAudio, onAddAudio = viewModel::addAudio, onSelectAudio = viewModel::selectAudio, isLoadingHosters = isLoadingHosters, - hosterState = hosterState, - expandedState = expandedState, + hosterState = hosterState.toPersistentList(), + expandedState = expandedState.toPersistentList(), selectedVideoIndex = selectedHosterVideoIndex, onClickHoster = viewModel::onHosterClicked, onClickVideo = viewModel::onVideoClicked, displayHosters = Pair(showFailedHosters, emptyHosters), - chapter = currentChapter?.toSegment(), - chapters = chapters.map { it.toSegment() }.toImmutableList(), + chapter = chapters.getOrNull(currentChapter ?: 0), + chapters = chapters, onSeekToChapter = { - viewModel.selectChapter(it) + viewModel.mpv.setPropertyInt("chapter", it) viewModel.dismissSheet() viewModel.unpause() }, decoder = decoder, - onUpdateDecoder = viewModel::updateDecoder, - speed = speed, - onSpeedChange = { MPVLib.setPropertyDouble("speed", it.toFixed(2).toDouble()) }, + onUpdateDecoder = { viewModel.mpv.setPropertyString("hwdec", it.value) }, + + speed = playbackSpeed ?: playerPreferences.playerSpeed().get(), + speedPresets = speedPresets.map { it.toFloat() }.sorted().toPersistentList(), + onSpeedChange = { viewModel.mpv.setPropertyFloat("speed", it.toFixed(2)) }, + onMakeDefaultSpeed = { playerPreferences.playerSpeed().set(it.toFixed(2)) }, + onAddSpeedPreset = { playerPreferences.speedPresets() += it.toFixed(2).toString() }, + onRemoveSpeedPreset = { playerPreferences.speedPresets() -= it.toFixed(2).toString() }, + onResetSpeedPresets = playerPreferences.speedPresets()::delete, + onResetDefaultSpeed = { + viewModel.mpv.setPropertyFloat("speed", playerPreferences.playerSpeed().deleteAndGet().toFixed(2)) + }, + + // More sheet state + statisticsPage = statisticsPage, + audioChannels = audioChannels, sleepTimerTimeRemaining = sleepTimerTimeRemaining, onStartSleepTimer = viewModel::startTimer, - buttons = customButtons.getButtons().toImmutableList(), + onStatisticsPageChange = { page -> + if ((page == 0) xor + (statisticsPage == 0) + ) { + viewModel.mpv.command("script-binding", "stats/display-stats-toggle") + } + if (page != 0) viewModel.mpv.command("script-binding", "stats/display-page-$page") + advancedPreferences.playerStatisticsPage().set(page) + }, + onAudioChannelsChange = { + audioPreferences.audioChannels().set(it) + if (it == AudioChannels.ReverseStereo) { + viewModel.mpv.setPropertyString(AudioChannels.AutoSafe.property, AudioChannels.AutoSafe.value) + } else { + viewModel.mpv.setPropertyString(AudioChannels.ReverseStereo.property, "") + } + viewModel.mpv.setPropertyString(it.property, it.value) + }, + onCustomButtonClick = { it.execute(viewModel.mpv) }, + onCustomButtonLongClick = { it.executeLongPress(viewModel.mpv) }, + buttons = customButtons, + onPitchCorrectionChange = { + audioPreferences.enablePitchCorrection().set(it) + viewModel.mpv.setPropertyBoolean("audio-pitch-correction", it) + }, + pitchCorrection = pitchCorrection || mpvAudioPitchCorrection == true, isLocalSource = currentSource?.isLocal() == true, showSubtitles = showSubtitles, onToggleShowSubtitles = { subtitlePreferences.screenshotSubtitles().set(it) }, cachePath = viewModel.cachePath, onSetAsArt = viewModel::setAsArt, - onShare = { viewModel.shareImage(it, viewModel.pos.value.toInt()) }, - onSave = { viewModel.saveImage(it, viewModel.pos.value.toInt()) }, + onShare = { viewModel.shareImage(it, viewModel.pos) }, + onSave = { viewModel.saveImage(it, viewModel.pos) }, takeScreenshot = viewModel::takeScreenshot, onDismissScreenshot = { viewModel.showSheet(Sheets.None) @@ -610,13 +698,221 @@ fun PlayerControls( onDismissRequest = { viewModel.showSheet(Sheets.None) }, dismissSheet = dismissSheet, ) + val panel by viewModel.panelShown.collectAsState() + val subDelayPref by subtitlePreferences.subtitlesDelay().collectAsState() + val subDelay by viewModel.mpv.propFlow("sub-delay").collectAsState() + val subDelaySecondary by viewModel.mpv.propFlow("secondary-sub-delay").collectAsState() + val subDelaySecondaryPref by subtitlePreferences.subtitlesSecondaryDelay().collectAsState() + val subSpeed by viewModel.mpv.propFlow("sub-speed").collectAsState() + val audioDelay by viewModel.mpv.propFlow("audio-delay").collectAsState() + val isBold by viewModel.mpv.propFlow("sub-bold").collectAsState() + val isItalic by viewModel.mpv.propFlow("sub-italic").collectAsState() + val subJustify by viewModel.mpv.propFlow("sub-justify").collectAsState() + val subFont by viewModel.mpv.propFlow("sub-font").collectAsState() + val subFontSize by viewModel.mpv.propFlow("sub-font-size").collectAsState() + val subBorderStyle by viewModel.mpv.propFlow("sub-border-style").collectAsState() + val subBorderSize by viewModel.mpv.propFlow("sub-outline-size").collectAsState() + val subShadowOffset by viewModel.mpv.propFlow("sub-shadow-offset").collectAsState() + val subColor by viewModel.mpv.propFlow("sub-color").collectAsState() + val subBorderColor by viewModel.mpv.propFlow("sub-outline-color").collectAsState() + val subBackgroundColor by viewModel.mpv.propFlow("sub-back-color").collectAsState() + val overrideAssSubs by viewModel.mpv.propFlow("sub-ass-override").collectAsState() + val subScale by viewModel.mpv.propFlow("sub-scale").collectAsState() + val subPos by viewModel.mpv.propFlow("sub-pos").collectAsState() + val deband by decoderPreferences.debanding().collectAsState() + val mpvGpuNext by viewModel.mpv.propFlow("vo").collectAsState() + val debandSettingsMap = DebandSettings.entries.associateWith { setting -> + viewModel.mpv.propFlow(setting.mpvProperty).collectAsState().value ?: 0 + } + val filterValuesMap = VideoFilters.entries.associateWith { filter -> + viewModel.mpv.propFlow(filter.mpvProperty).collectAsState().value ?: 0 + } + var subtitleColorType by remember { mutableStateOf(SubColorType.Text) } + PlayerPanels( panelShown = panel, onDismissRequest = { viewModel.showPanel(Panels.None) }, + // Subtitle settings panel state + isBold = isBold ?: subtitlePreferences.boldSubtitles().get(), + isItalic = isItalic ?: subtitlePreferences.italicSubtitles().get(), + subJustify = + subJustify?.let { SubtitleJustification.byValue(it) } + ?: subtitlePreferences.subtitleJustification().get(), + subFont = subFont ?: subtitlePreferences.subtitleFont().get(), + subFontSize = subFontSize ?: subtitlePreferences.subtitleFontSize().get(), + subBorderStyle = subBorderStyle?.let { SubtitlesBorderStyle.byValue(it) } + ?: subtitlePreferences.borderStyleSubtitles().get(), + subBorderSize = subBorderSize ?: subtitlePreferences.subtitleBorderSize().get(), + subShadowOffset = subShadowOffset ?: subtitlePreferences.shadowOffsetSubtitles().get(), + subColor = subtitleColorType, + currentSubtitleColor = when (subtitleColorType) { + SubColorType.Text -> subColor?.toColorInt() ?: subtitlePreferences.textColorSubtitles().get() + SubColorType.Border -> subBorderColor?.toColorInt() ?: subtitlePreferences.borderColorSubtitles().get() + SubColorType.Background -> subBackgroundColor?.toColorInt() + ?: subtitlePreferences.backgroundColorSubtitles().get() + }, + overrideAssSubs = overrideAssSubs ?: subtitlePreferences.overrideSubsASS().get(), + subScale = subScale ?: subtitlePreferences.subtitleFontScale().get(), + subPos = subPos ?: subtitlePreferences.subtitlePos().get(), + onSubBoldChange = { + viewModel.mpv.setPropertyBoolean("sub-bold", it) + subtitlePreferences.boldSubtitles().set(it) + }, + onSubItalicChange = { + viewModel.mpv.setPropertyBoolean("sub-italic", it) + subtitlePreferences.italicSubtitles().set(it) + }, + onSubJustifyChange = { + viewModel.mpv.setPropertyString("sub-justify", it.value) + subtitlePreferences.subtitleJustification().set(it) + }, + onSubFontChange = { + viewModel.mpv.setPropertyString("sub-font", it) + subtitlePreferences.subtitleFont().set(it) + }, + onSubFontSizeChange = { + viewModel.mpv.setPropertyInt("sub-font-size", it) + subtitlePreferences.subtitleFontSize().set(it) + }, + onSubBorderStyleChange = { + viewModel.mpv.setPropertyString("sub-border-style", it.value) + subtitlePreferences.borderStyleSubtitles().set(it) + }, + onSubBorderSizeChange = { + viewModel.mpv.setPropertyInt("sub-outline-size", it) + subtitlePreferences.subtitleBorderSize().set(it) + }, + onSubShadowOffsetChange = { + viewModel.mpv.setPropertyInt("sub-shadow-offset", it) + subtitlePreferences.shadowOffsetSubtitles().set(it) + }, + onSubColorChange = { + when (subtitleColorType) { + SubColorType.Text -> { + viewModel.mpv.setPropertyString("sub-color", it.toColorHexString()) + subtitlePreferences.textColorSubtitles().set(it) + } + + SubColorType.Border -> { + viewModel.mpv.setPropertyString("sub-outline-color", it.toColorHexString()) + subtitlePreferences.borderColorSubtitles().set(it) + } + + SubColorType.Background -> { + viewModel.mpv.setPropertyString("sub-back-color", it.toColorHexString()) + subtitlePreferences.backgroundColorSubtitles().set(it) + } + } + }, + onOverrideAssSubsChange = { + viewModel.mpv.setPropertyBoolean("sub-ass-override", it) + subtitlePreferences.overrideSubsASS().set(it) + }, + onSubScaleChange = { + viewModel.mpv.setPropertyFloat("sub-scale", it) + subtitlePreferences.subtitleFontScale().set(it) + }, + onSubPosChange = { + viewModel.mpv.setPropertyInt("sub-pos", it) + subtitlePreferences.subtitlePos().set(it) + }, + onSubColorTypeChange = { subtitleColorType = it }, + onSubColorReset = { + resetColors(subtitlePreferences, viewModel.mpv, subtitleColorType) + }, + onSubtitleSettingsReset = { + resetTypography(viewModel.mpv, subtitlePreferences) + }, + onSubtitleMiscReset = { + subtitlePreferences.subtitlePos().deleteAndGet().let { + viewModel.mpv.setPropertyInt("sub-pos", it) + } + subtitlePreferences.subtitleFontScale().deleteAndGet().let { + viewModel.mpv.setPropertyFloat("sub-scale", it) + } + subtitlePreferences.overrideSubsASS().delete() + viewModel.mpv.setPropertyString("sub-ass-override", "scale") + }, + subDelayMsPrimary = subDelay?.times(1000)?.roundToInt() ?: subDelayPref, + subDelayMsSecondary = subDelaySecondary?.times(1000)?.roundToInt() ?: subDelaySecondaryPref, + subSpeed = subSpeed ?: subtitlePreferences.subtitlesSpeed().get().toDouble(), + onSubDelayPrimaryChange = { + viewModel.mpv.setPropertyDouble("sub-delay", it / 1000.0) + }, + onSubDelaySecondaryChange = { + viewModel.mpv.setPropertyDouble("secondary-sub-delay", it / 1000.0) + }, + onSubSpeedChange = { + viewModel.mpv.setPropertyDouble("sub-speed", it) + }, + onSubDelayApply = { + subtitlePreferences.subtitlesDelay().set((subDelay?.times(1000)?.roundToInt()) ?: 0) + subtitlePreferences.subtitlesSecondaryDelay().set((subDelaySecondary?.times(1000)?.roundToInt()) ?: 0) + }, + onSubDelayReset = { + viewModel.mpv.setPropertyDouble("sub-delay", subtitlePreferences.subtitlesDelay().get() / 1000.0) + viewModel.mpv.setPropertyDouble( + "secondary-sub-delay", + subtitlePreferences.subtitlesSecondaryDelay().get() / 1000.0, + ) + viewModel.mpv.setPropertyDouble("sub-speed", subtitlePreferences.subtitlesSpeed().get().toDouble()) + }, + audioDelayMs = (audioDelay?.times(1000))?.roundToInt() ?: audioPreferences.audioDelay().get(), + onAudioDelayChange = { viewModel.mpv.setPropertyDouble("audio-delay", it / 1000.0) }, + onAudioDelayApply = { + audioPreferences.audioDelay().set((audioDelay?.times(1000)?.roundToInt()) ?: 0) + }, + onAudioDelayReset = { + viewModel.mpv.setPropertyDouble("audio-delay", audioPreferences.audioDelay().get() / 1000.0) + }, + onDebandChange = { + decoderPreferences.debanding().set(it) + when (it) { + Debanding.None -> { + viewModel.mpv.setPropertyString("deband", "no") + viewModel.mpv.command("vf", "remove", "@deband") + } + + Debanding.CPU -> { + viewModel.mpv.setPropertyString("deband", "no") + viewModel.mpv.command("vf", "add", "@deband:gradfun=radius=12") + } + + Debanding.GPU -> { + viewModel.mpv.setPropertyString("deband", "yes") + viewModel.mpv.command("vf", "remove", "@deband") + } + } + }, + onDebandReset = { + viewModel.mpv.setPropertyString("deband", "no") + viewModel.mpv.command("vf", "remove", "@deband") + DebandSettings.entries.forEach { + viewModel.mpv.setPropertyInt(it.mpvProperty, it.preference(decoderPreferences).deleteAndGet()) + } + }, + onDebandSettingsChange = { setting, value -> + setting.preference(decoderPreferences).set(value) + viewModel.mpv.setPropertyInt(setting.mpvProperty, value) + }, + onVideoFilterChange = { filter, value -> + filter.preference(decoderPreferences).set(value) + viewModel.mpv.setPropertyInt(filter.mpvProperty, value) + }, + onFilterReset = { + VideoFilters.entries.forEach { + viewModel.mpv.setPropertyInt(it.mpvProperty, it.preference(decoderPreferences).deleteAndGet()) + } + }, + deband = deband, + isGpuNextEnabled = mpvGpuNext == "gpu-next", + filterValue = { filterValuesMap[it] ?: 0 }, + debandSettings = { debandSettingsMap[it] ?: 0 }, + modifier = Modifier, ) - val activity = LocalContext.current as PlayerActivity + val activity = LocalActivity.current as PlayerActivity val dialog by viewModel.dialogShown.collectAsState() val anime by viewModel.currentAnime.collectAsState() val playlist by viewModel.currentPlaylist.collectAsState() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerDialogs.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerDialogs.kt index 9e21182eb4..d10d3c5e7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerDialogs.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerDialogs.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Episode import eu.kanade.tachiyomi.ui.player.Dialogs import eu.kanade.tachiyomi.ui.player.controls.components.dialogs.EpisodeListDialog import eu.kanade.tachiyomi.ui.player.controls.components.dialogs.IntegerPickerDialog +import kotlinx.collections.immutable.ImmutableList import java.time.format.DateTimeFormatter @Composable @@ -14,9 +15,9 @@ fun PlayerDialogs( // Episode list episodeDisplayMode: Long?, currentEpisodeIndex: Int, - episodeList: List, + episodeList: ImmutableList, dateRelativeTime: Boolean, - dateFormat: DateTimeFormatter, + dateFormat: String, onBookmarkClicked: (Long?, Boolean) -> Unit, onFillermarkClicked: (Long?, Boolean) -> Unit, onEpisodeClicked: (Long?) -> Unit, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt index 373b94ade3..169cdb0986 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerPanels.kt @@ -33,12 +33,18 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import eu.kanade.tachiyomi.ui.player.DebandSettings +import eu.kanade.tachiyomi.ui.player.Debanding import eu.kanade.tachiyomi.ui.player.Panels +import eu.kanade.tachiyomi.ui.player.VideoFilters import eu.kanade.tachiyomi.ui.player.controls.components.panels.AudioDelayPanel +import eu.kanade.tachiyomi.ui.player.controls.components.panels.SubColorType import eu.kanade.tachiyomi.ui.player.controls.components.panels.SubtitleDelayPanel import eu.kanade.tachiyomi.ui.player.controls.components.panels.SubtitleSettingsPanel -import eu.kanade.tachiyomi.ui.player.controls.components.panels.VideoFiltersPanel +import eu.kanade.tachiyomi.ui.player.controls.components.panels.SubtitlesBorderStyle +import eu.kanade.tachiyomi.ui.player.controls.components.panels.VideoSettingsPanel import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -46,7 +52,60 @@ import uy.kohesive.injekt.api.get fun PlayerPanels( panelShown: Panels, onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, + // Subtitle settings panel state + isBold: Boolean, + isItalic: Boolean, + subJustify: SubtitleJustification, + subFont: String, + subFontSize: Int, + subBorderStyle: SubtitlesBorderStyle, + subBorderSize: Int, + subShadowOffset: Int, + subColor: SubColorType, + currentSubtitleColor: Int, + overrideAssSubs: Boolean, + subScale: Float, + subPos: Int, + onSubBoldChange: (Boolean) -> Unit, + onSubItalicChange: (Boolean) -> Unit, + onSubJustifyChange: (SubtitleJustification) -> Unit, + onSubFontChange: (String) -> Unit, + onSubFontSizeChange: (Int) -> Unit, + onSubBorderStyleChange: (SubtitlesBorderStyle) -> Unit, + onSubBorderSizeChange: (Int) -> Unit, + onSubShadowOffsetChange: (Int) -> Unit, + onSubColorChange: (Int) -> Unit, + onSubColorTypeChange: (SubColorType) -> Unit, + onOverrideAssSubsChange: (Boolean) -> Unit, + onSubScaleChange: (Float) -> Unit, + onSubPosChange: (Int) -> Unit, + onSubtitleSettingsReset: () -> Unit, + onSubtitleMiscReset: () -> Unit, + subDelayMsPrimary: Int, + subDelayMsSecondary: Int, + subSpeed: Double, + onSubDelayPrimaryChange: (Int) -> Unit, + onSubDelaySecondaryChange: (Int) -> Unit, + onSubSpeedChange: (Double) -> Unit, + onSubDelayApply: () -> Unit, + onSubDelayReset: () -> Unit, + onSubColorReset: (SubColorType) -> Unit, + // Audio delay panel state + audioDelayMs: Int, + onAudioDelayChange: (Int) -> Unit, + onAudioDelayApply: () -> Unit, + onAudioDelayReset: () -> Unit, + // Video settings panel state + deband: Debanding, + onDebandChange: (Debanding) -> Unit, + onVideoFilterChange: (VideoFilters, Int) -> Unit, + debandSettings: (DebandSettings) -> Int, + onDebandSettingsChange: (DebandSettings, Int) -> Unit, + onDebandReset: () -> Unit, + isGpuNextEnabled: Boolean, + filterValue: (VideoFilters) -> Int, + onFilterReset: () -> Unit, + modifier: Modifier, ) { AnimatedContent( targetState = panelShown, @@ -63,16 +122,75 @@ fun PlayerPanels( Box(Modifier.fillMaxHeight()) } Panels.SubtitleSettings -> { - SubtitleSettingsPanel(onDismissRequest) + SubtitleSettingsPanel( + onDismissRequest = onDismissRequest, + isBold = isBold, + isItalic = isItalic, + justify = subJustify, + font = subFont, + fontSize = subFontSize, + borderStyle = subBorderStyle, + borderSize = subBorderSize, + shadowOffset = subShadowOffset, + onIsBoldChange = onSubBoldChange, + onIsItalicChange = onSubItalicChange, + onJustificationChange = onSubJustifyChange, + onFontChange = onSubFontChange, + onFontSizeChange = onSubFontSizeChange, + onBorderStyleChange = onSubBorderStyleChange, + onBorderSizeChange = onSubBorderSizeChange, + onShadowOffsetChange = onSubShadowOffsetChange, + onTypographyReset = onSubtitleSettingsReset, + currentColorType = subColor, + currentSubtitleColor = currentSubtitleColor, + onColorChange = onSubColorChange, + onColorReset = onSubColorReset, + onColorTypeChange = onSubColorTypeChange, + overrideAssSubs = overrideAssSubs, + subScale = subScale, + subPos = subPos, + onOverrideAssSubsChange = onOverrideAssSubsChange, + onSubScaleChange = onSubScaleChange, + onSubPosChange = onSubPosChange, + onMiscReset = onSubtitleMiscReset, + modifier = Modifier, + ) } Panels.SubtitleDelay -> { - SubtitleDelayPanel(onDismissRequest) + SubtitleDelayPanel( + delayMs = subDelayMsPrimary, + secondaryDelayMs = subDelayMsSecondary, + speed = subSpeed, + onSpeedChange = onSubSpeedChange, + onDelayChange = onSubDelayPrimaryChange, + onSecondaryDelayChange = onSubDelaySecondaryChange, + onApply = onSubDelayApply, + onReset = onSubDelayReset, + onDismissRequest = onDismissRequest, + ) } Panels.AudioDelay -> { - AudioDelayPanel(onDismissRequest) + AudioDelayPanel( + delayMs = audioDelayMs, + onDelayChange = onAudioDelayChange, + onApply = onAudioDelayApply, + onReset = onAudioDelayReset, + onDismissRequest = onDismissRequest, + ) } Panels.VideoFilters -> { - VideoFiltersPanel(onDismissRequest) + VideoSettingsPanel( + onDismissRequest = onDismissRequest, + onVideoFilterChange = onVideoFilterChange, + deband = deband, + onDebandChange = onDebandChange, + debandSettings = debandSettings, + onDebandSettingsChange = onDebandSettingsChange, + onDebandReset = onDebandReset, + isGpuNextEnabled = isGpuNextEnabled, + filterValue = filterValue, + onFilterReset = onFilterReset, + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt index 2f0d9fe1ad..2106fac7bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/PlayerSheets.kt @@ -25,8 +25,8 @@ import dev.vivvvek.seeker.Segment import eu.kanade.tachiyomi.ui.player.ArtType import eu.kanade.tachiyomi.ui.player.Decoder import eu.kanade.tachiyomi.ui.player.Panels -import eu.kanade.tachiyomi.ui.player.PlayerViewModel.VideoTrack import eu.kanade.tachiyomi.ui.player.Sheets +import eu.kanade.tachiyomi.ui.player.VideoTrack import eu.kanade.tachiyomi.ui.player.controls.components.sheets.AudioTracksSheet import eu.kanade.tachiyomi.ui.player.controls.components.sheets.ChaptersSheet import eu.kanade.tachiyomi.ui.player.controls.components.sheets.HosterState @@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.ui.player.controls.components.sheets.PlaybackSpeedShe import eu.kanade.tachiyomi.ui.player.controls.components.sheets.QualitySheet import eu.kanade.tachiyomi.ui.player.controls.components.sheets.ScreenshotSheet import eu.kanade.tachiyomi.ui.player.controls.components.sheets.SubtitlesSheet +import eu.kanade.tachiyomi.ui.player.settings.AudioChannels import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import tachiyomi.domain.custombutton.model.CustomButton @@ -46,20 +47,18 @@ fun PlayerSheets( // subtitles sheet subtitles: ImmutableList, - selectedSubtitles: ImmutableList, onAddSubtitle: (Uri) -> Unit, - onSelectSubtitle: (Int) -> Unit, + onSelectSubtitle: (VideoTrack) -> Unit, // audio sheet audioTracks: ImmutableList, - selectedAudio: Int, onAddAudio: (Uri) -> Unit, - onSelectAudio: (Int) -> Unit, + onSelectAudio: (VideoTrack) -> Unit, // video sheet isLoadingHosters: Boolean, - hosterState: List, - expandedState: List, + hosterState: ImmutableList, + expandedState: ImmutableList, selectedVideoIndex: Pair, onClickHoster: (Int) -> Unit, onClickVideo: (Int, Int) -> Unit, @@ -75,12 +74,26 @@ fun PlayerSheets( onUpdateDecoder: (Decoder) -> Unit, // Speed sheet + pitchCorrection: Boolean, + onPitchCorrectionChange: (Boolean) -> Unit, speed: Float, + speedPresets: ImmutableList, onSpeedChange: (Float) -> Unit, + onAddSpeedPreset: (Float) -> Unit, + onRemoveSpeedPreset: (Float) -> Unit, + onResetSpeedPresets: () -> Unit, + onMakeDefaultSpeed: (Float) -> Unit, + onResetDefaultSpeed: () -> Unit, // More sheet + statisticsPage: Int, + audioChannels: AudioChannels, sleepTimerTimeRemaining: Int, onStartSleepTimer: (Int) -> Unit, + onStatisticsPageChange: (Int) -> Unit, + onAudioChannelsChange: (AudioChannels) -> Unit, + onCustomButtonClick: (CustomButton) -> Unit, + onCustomButtonLongClick: (CustomButton) -> Unit, buttons: ImmutableList, // Screenshot sheet @@ -109,7 +122,6 @@ fun PlayerSheets( } SubtitlesSheet( tracks = subtitles.toImmutableList(), - selectedTracks = selectedSubtitles, onSelect = onSelectSubtitle, onAddSubtitle = { subtitlesPicker.launch(arrayOf("*/*")) }, onOpenSubtitleSettings = { onOpenPanel(Panels.SubtitleSettings) }, @@ -127,7 +139,6 @@ fun PlayerSheets( } AudioTracksSheet( tracks = audioTracks, - selectedId = selectedAudio, onSelect = onSelectAudio, onAddAudioTrack = { audioPicker.launch(arrayOf("*/*")) }, onOpenDelayPanel = { onOpenPanel(Panels.AudioDelay) }, @@ -162,10 +173,16 @@ fun PlayerSheets( Sheets.More -> { MoreSheet( + statisticsPage = statisticsPage, + audioChannels = audioChannels, selectedDecoder = decoder, onSelectDecoder = onUpdateDecoder, remainingTime = sleepTimerTimeRemaining, onStartTimer = onStartSleepTimer, + onStatisticsPageChange = onStatisticsPageChange, + onCustomButtonClick = onCustomButtonClick, + onCustomButtonLongClick = onCustomButtonLongClick, + onAudioChannelsChange = onAudioChannelsChange, onDismissRequest = onDismissRequest, onEnterFiltersPanel = { onOpenPanel(Panels.VideoFilters) }, customButtons = buttons, @@ -174,8 +191,16 @@ fun PlayerSheets( Sheets.PlaybackSpeed -> { PlaybackSpeedSheet( - speed, + pitchCorrection = pitchCorrection, + onPitchCorrectionChange = onPitchCorrectionChange, + speed = speed, onSpeedChange = onSpeedChange, + speedPresets = speedPresets, + onAddSpeedPreset = onAddSpeedPreset, + onRemoveSpeedPreset = onRemoveSpeedPreset, + onResetPresets = onResetSpeedPresets, + onMakeDefault = onMakeDefaultSpeed, + onResetDefault = onResetDefaultSpeed, onDismissRequest = onDismissRequest, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/CurrentChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/CurrentChapter.kt index fa55d18f59..6ab6ab1ddd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/CurrentChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/CurrentChapter.kt @@ -97,25 +97,23 @@ fun CurrentChapter( overflow = TextOverflow.Clip, color = MaterialTheme.colorScheme.tertiary, ) - currentChapter.name.let { - Text( - text = Typography.bullet.toString(), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Clip, - ) - Text( - text = it, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - ) - } + Text( + text = Typography.bullet.toString(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Clip, + ) + Text( + text = chapter.name, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/SeekBar.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/SeekBar.kt index d98e6ed005..12d4290488 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/SeekBar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/SeekBar.kt @@ -66,6 +66,7 @@ data class IndexedSegment( fun SeekbarWithTimers( position: Float, duration: Float, + remaining: Float, readAheadValue: Float, onValueChange: (Float) -> Unit, onValueChangeFinished: () -> Unit, @@ -115,7 +116,7 @@ fun SeekbarWithTimers( ), ) VideoTimer( - value = if (timersInverted.second) position - duration else duration, + value = if (timersInverted.second) -remaining else duration, isInverted = timersInverted.second, onClick = { clickEvent() @@ -155,6 +156,7 @@ private fun PreviewSeekBar() { SeekbarWithTimers( 5f, 20f, + 15f, 4f, {}, {}, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/dialogs/EpisodeListDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/dialogs/EpisodeListDialog.kt index f7c59f4b65..1b5d1dd628 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/dialogs/EpisodeListDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/dialogs/EpisodeListDialog.kt @@ -33,10 +33,12 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.anime.components.DotSeparatorText import eu.kanade.presentation.util.formatEpisodeNumber import eu.kanade.tachiyomi.data.database.models.Episode import eu.kanade.tachiyomi.util.lang.toRelativeString +import kotlinx.collections.immutable.ImmutableList import tachiyomi.domain.anime.model.Anime import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.VerticalFastScroller @@ -52,9 +54,9 @@ import java.time.format.DateTimeFormatter fun EpisodeListDialog( displayMode: Long?, currentEpisodeIndex: Int, - episodeList: List, + episodeList: ImmutableList, dateRelativeTime: Boolean, - dateFormat: DateTimeFormatter, + dateFormat: String, onBookmarkClicked: (Long?, Boolean) -> Unit, onFillermarkClicked: (Long?, Boolean) -> Unit, onEpisodeClicked: (Long?) -> Unit, @@ -63,6 +65,7 @@ fun EpisodeListDialog( val context = LocalContext.current val itemScrollIndex = (episodeList.size - currentEpisodeIndex) - 1 val episodeListState = rememberLazyListState(initialFirstVisibleItemIndex = itemScrollIndex) + val dateFormatter = remember(dateFormat) { UiPreferences.dateFormat(dateFormat) } PlayerDialog( title = stringResource(AYMR.strings.episodes), @@ -102,7 +105,7 @@ fun EpisodeListDialog( ).toRelativeString( context = context, relative = dateRelativeTime, - dateFormat = dateFormat, + dateFormat = dateFormatter, ) } ?: "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/AudioDelayPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/AudioDelayPanel.kt index 77cf8717d8..783a0d698a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/AudioDelayPanel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/AudioDelayPanel.kt @@ -30,30 +30,23 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences -import `is`.xyz.mpv.MPVLib import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get @Composable fun AudioDelayPanel( + delayMs: Int, + onDelayChange: (Int) -> Unit, + onApply: () -> Unit, + onReset: () -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ) { - val preferences = remember { Injekt.get() } - ConstraintLayout( modifier = modifier .fillMaxSize() @@ -61,15 +54,11 @@ fun AudioDelayPanel( ) { val delayControlCard = createRef() - var delay by remember { mutableIntStateOf((MPVLib.getPropertyDouble("audio-delay") * 1000).toInt()) } - LaunchedEffect(delay) { - MPVLib.setPropertyDouble("audio-delay", delay / 1000.0) - } DelayCard( - delay = delay, - onDelayChange = { delay = it }, - onApply = { preferences.audioDelay().set(delay) }, - onReset = { delay = 0 }, + delayMs = delayMs, + onDelayChange = onDelayChange, + onApply = onApply, + onReset = onReset, title = { AudioDelayCardTitle(onClose = onDismissRequest) }, delayType = DelayType.Audio, modifier = Modifier.constrainAs(delayControlCard) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleDelayPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleDelayPanel.kt index 8a7c3403e8..b23ea81cf9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleDelayPanel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleDelayPanel.kt @@ -47,7 +47,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -60,24 +59,25 @@ import dev.icerock.moko.resources.StringResource import eu.kanade.presentation.player.components.OutlinedNumericChooser import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors -import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences -import `is`.xyz.mpv.MPVLib import kotlinx.coroutines.delay import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import kotlin.math.round -import kotlin.math.roundToInt @Composable fun SubtitleDelayPanel( + delayMs: Int, + secondaryDelayMs: Int, + speed: Double, + onSpeedChange: (Double) -> Unit, + onDelayChange: (Int) -> Unit, + onSecondaryDelayChange: (Int) -> Unit, + onApply: () -> Unit, + onReset: () -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ) { - val preferences = remember { Injekt.get() } - ConstraintLayout( modifier = modifier .fillMaxSize() @@ -86,55 +86,25 @@ fun SubtitleDelayPanel( val delayControlCard = createRef() var affectedSubtitle by remember { mutableStateOf(SubtitleDelayType.Primary) } - var delay by remember { mutableIntStateOf((MPVLib.getPropertyDouble("sub-delay") * 1000).roundToInt()) } - var secondaryDelay by remember { - mutableIntStateOf((MPVLib.getPropertyDouble("secondary-sub-delay") * 1000).roundToInt()) - } - var speed by remember { mutableFloatStateOf(MPVLib.getPropertyDouble("sub-speed").toFloat()) } - LaunchedEffect(speed) { - if (speed in 0.1f..1f) MPVLib.setPropertyDouble("sub-speed", speed.toDouble()) - } - LaunchedEffect(delay, secondaryDelay) { - val finalDelay = (if (affectedSubtitle == SubtitleDelayType.Secondary) secondaryDelay else delay) / 1000.0 - when (affectedSubtitle) { - SubtitleDelayType.Primary -> MPVLib.setPropertyDouble("sub-delay", finalDelay) - SubtitleDelayType.Secondary -> MPVLib.setPropertyDouble("secondary-sub-delay", finalDelay) - else -> { - MPVLib.setPropertyDouble("sub-delay", finalDelay) - MPVLib.setPropertyDouble("secondary-sub-delay", finalDelay) - } - } - } - LaunchedEffect(affectedSubtitle) { - secondaryDelay = ( - MPVLib.getPropertyDouble( - if (affectedSubtitle == SubtitleDelayType.Both) "sub-delay" else "secondary-sub-delay", - ) * 1000 - ).toInt() - delay = (MPVLib.getPropertyDouble("sub-delay") * 1000).toInt() - } SubtitleDelayCard( - delay = if (affectedSubtitle == SubtitleDelayType.Secondary) secondaryDelay else delay, + delayMs = if (affectedSubtitle == SubtitleDelayType.Secondary) secondaryDelayMs else delayMs, onDelayChange = { - if (affectedSubtitle == SubtitleDelayType.Secondary) { - secondaryDelay = it - } else { - delay = it + when (affectedSubtitle) { + SubtitleDelayType.Both -> { + onDelayChange(it) + onSecondaryDelayChange(it) + } + + SubtitleDelayType.Primary -> onDelayChange(it) + else -> onSecondaryDelayChange(it) } }, - speed = speed, - onSpeedChange = { speed = round(it * 1000) / 1000f }, + speed = speed.toFloat(), + onSpeedChange = { onSpeedChange(round(it * 1000) / 1000.0) }, affectedSubtitle = affectedSubtitle, onTypeChange = { affectedSubtitle = it }, - onApply = { - preferences.subtitlesDelay().set(delay) - if (speed in 0.1f..10f) preferences.subtitlesSpeed().set(speed) - }, - onReset = { - delay = 0 - secondaryDelay = 0 - speed = 1f - }, + onApply = onApply, + onReset = onReset, onClose = onDismissRequest, modifier = Modifier.constrainAs(delayControlCard) { linkTo(parent.top, parent.bottom, bias = 0.8f) @@ -146,7 +116,7 @@ fun SubtitleDelayPanel( @Composable fun SubtitleDelayCard( - delay: Int, + delayMs: Int, onDelayChange: (Int) -> Unit, speed: Float, onSpeedChange: (Float) -> Unit, @@ -158,7 +128,7 @@ fun SubtitleDelayCard( modifier: Modifier = Modifier, ) { DelayCard( - delay = delay, + delayMs = delayMs, onDelayChange = onDelayChange, onApply = onApply, onReset = onReset, @@ -201,7 +171,7 @@ enum class SubtitleDelayType( @Suppress("LambdaParameterInRestartableEffect") // Intentional @Composable fun DelayCard( - delay: Int, + delayMs: Int, onDelayChange: (Int) -> Unit, onApply: () -> Unit, onReset: () -> Unit, @@ -228,7 +198,7 @@ fun DelayCard( title() OutlinedNumericChooser( label = { Text(stringResource(AYMR.strings.player_sheets_sub_delay_delay)) }, - value = delay, + value = delayMs, onChange = onDelayChange, step = 50, min = Int.MIN_VALUE, @@ -244,13 +214,13 @@ fun DelayCard( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { var timerStart by remember { mutableStateOf(null) } - var finalDelay by remember { mutableIntStateOf(delay) } + var finalDelay by remember { mutableIntStateOf(delayMs) } LaunchedEffect(isDirectionPositive) { if (isDirectionPositive == null) { onDelayChange(finalDelay) return@LaunchedEffect } - finalDelay = delay + finalDelay = delayMs timerStart = System.currentTimeMillis() val startingDelay: Int = finalDelay while (isDirectionPositive != null && timerStart != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsColorsCard.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsColorsCard.kt index 3faacd4870..cd1d19d697 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsColorsCard.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsColorsCard.kt @@ -38,9 +38,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -57,21 +55,23 @@ import eu.kanade.presentation.player.components.TintedSliderItem import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences -import `is`.xyz.mpv.MPVLib +import `is`.xyz.mpv.MPV import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.deleteAndGet import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get @Composable fun SubtitleSettingsColorsCard( + currentColor: Int, + currentColorType: SubColorType, + onColorChange: (Int) -> Unit, + onColorReset: (SubColorType) -> Unit, + onColorTypeChange: (SubColorType) -> Unit, modifier: Modifier = Modifier, ) { - val preferences = remember { Injekt.get() } var isExpanded by remember { mutableStateOf(true) } ExpandableCard( isExpanded = isExpanded, @@ -88,11 +88,6 @@ fun SubtitleSettingsColorsCard( colors = panelCardsColors(), ) { Column { - var currentColorType by remember { mutableStateOf(SubColorType.Text) } - var currentColor by remember { mutableIntStateOf(getCurrentMPVColor(currentColorType)) } - LaunchedEffect(currentColorType) { - currentColor = getCurrentMPVColor(currentColorType) - } Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, @@ -104,7 +99,7 @@ fun SubtitleSettingsColorsCard( SubColorType.entries.forEach { type -> IconToggleButton( checked = currentColorType == type, - onCheckedChange = { currentColorType = type }, + onCheckedChange = { onColorTypeChange(type) }, ) { Icon( when (type) { @@ -119,10 +114,7 @@ fun SubtitleSettingsColorsCard( Text(stringResource(currentColorType.titleRes)) Spacer(Modifier.weight(1f)) TextButton( - onClick = { - resetColors(preferences, currentColorType) - currentColor = getCurrentMPVColor(currentColorType) - }, + onClick = { onColorReset(currentColorType) }, ) { Row( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), @@ -134,12 +126,8 @@ fun SubtitleSettingsColorsCard( } } SubtitlesColorPicker( - currentColor, - onColorChange = { - currentColor = it - currentColorType.preference(preferences).set(it) - MPVLib.setPropertyString(currentColorType.property, it.toColorHexString()) - }, + color = currentColor, + onColorChange = onColorChange, ) } } @@ -167,7 +155,7 @@ enum class SubColorType( ), Border( AYMR.strings.player_sheets_subtitles_color_border, - "sub-border-color", + "sub-outline-color", preference = SubtitlePreferences::borderColorSubtitles, ), Background( @@ -177,34 +165,29 @@ enum class SubColorType( ), } -fun resetColors(preferences: SubtitlePreferences, type: SubColorType) { +fun resetColors( + preferences: SubtitlePreferences, + mpv: MPV?, + type: SubColorType, +) { when (type) { SubColorType.Text -> { - MPVLib.setPropertyString("sub-color", preferences.textColorSubtitles().deleteAndGet().toColorHexString()) + val textColor = preferences.textColorSubtitles().deleteAndGet().toColorHexString() + mpv?.setPropertyString("sub-color", textColor) } SubColorType.Border -> { - MPVLib.setPropertyString( - "sub-border-color", - preferences.borderColorSubtitles().deleteAndGet().toColorHexString(), - ) + val borderColor = preferences.borderColorSubtitles().deleteAndGet().toColorHexString() + mpv?.setPropertyString("sub-outline-color", borderColor) } SubColorType.Background -> { - MPVLib.setPropertyString( - "sub-back-color", - preferences.backgroundColorSubtitles().deleteAndGet().toColorHexString(), - ) + val backgroundColor = preferences.backgroundColorSubtitles().deleteAndGet().toColorHexString() + mpv?.setPropertyString("sub-back-color", backgroundColor) } } } -val getCurrentMPVColor: (SubColorType) -> Int = { colorType -> - MPVLib.getPropertyString(colorType.property)?.let { - android.graphics.Color.parseColor(it.uppercase()) - }!! -} - @Composable fun SubtitlesColorPicker( color: Int, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsMiscellaneousCard.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsMiscellaneousCard.kt index 8eb903450d..ac2fb20f22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsMiscellaneousCard.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsMiscellaneousCard.kt @@ -45,8 +45,6 @@ import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH import eu.kanade.tachiyomi.ui.player.controls.components.sheets.toFixed import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences -import `is`.xyz.mpv.MPVLib -import tachiyomi.core.common.preference.deleteAndGet import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding @@ -55,7 +53,16 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @Composable -fun SubtitlesMiscellaneousCard(modifier: Modifier = Modifier) { +fun SubtitlesMiscellaneousCard( + overrideAssSubs: Boolean, + subScale: Float, + subPos: Int, + onOverrideAssSubsChange: (Boolean) -> Unit, + onSubScaleChange: (Float) -> Unit, + onSubPosChange: (Int) -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier, +) { val preferences = remember { Injekt.get() } var isExpanded by remember { mutableStateOf(true) } ExpandableCard( @@ -71,34 +78,17 @@ fun SubtitlesMiscellaneousCard(modifier: Modifier = Modifier) { colors = panelCardsColors(), ) { Column { - var overrideAssSubs by remember { - mutableStateOf(MPVLib.getPropertyString("sub-ass-override").also { println(it) } == "force") - } SwitchPreference( - overrideAssSubs, - onValueChange = { - overrideAssSubs = it - preferences.overrideSubsASS().set(it) - MPVLib.setPropertyString("sub-ass-override", if (it) "force" else "scale") - }, + value = overrideAssSubs, + onValueChange = onOverrideAssSubsChange, content = { Text(stringResource(AYMR.strings.player_sheets_sub_override_ass)) }, modifier = Modifier.fillMaxWidth(), ) - var subScale by remember { - mutableStateOf(MPVLib.getPropertyDouble("sub-scale").toFloat()) - } - var subPos by remember { - mutableStateOf(MPVLib.getPropertyInt("sub-pos")) - } SliderItem( label = stringResource(AYMR.strings.player_sheets_sub_scale), value = subScale, valueText = subScale.toFixed(2).toString(), - onChange = { - subScale = it - preferences.subtitleFontScale().set(it) - MPVLib.setPropertyDouble("sub-scale", it.toDouble()) - }, + onChange = onSubScaleChange, max = 5f, icon = { Icon( @@ -111,11 +101,7 @@ fun SubtitlesMiscellaneousCard(modifier: Modifier = Modifier) { label = stringResource(AYMR.strings.player_sheets_sub_position), value = subPos, valueText = subPos.toString(), - onChange = { - subPos = it - preferences.subtitlePos().set(it) - MPVLib.setPropertyInt("sub-pos", it) - }, + onChange = onSubPosChange, max = 150, icon = { Icon( @@ -130,20 +116,7 @@ fun SubtitlesMiscellaneousCard(modifier: Modifier = Modifier) { .padding(end = MaterialTheme.padding.medium, bottom = MaterialTheme.padding.medium), horizontalArrangement = Arrangement.End, ) { - TextButton( - onClick = { - preferences.subtitlePos().deleteAndGet().let { - subPos = it - MPVLib.setPropertyInt("sub-pos", it) - } - preferences.subtitleFontScale().deleteAndGet().let { - subScale = it - MPVLib.setPropertyDouble("sub-scale", it.toDouble()) - } - preferences.overrideSubsASS().deleteAndGet().let { overrideAssSubs = it } - MPVLib.setPropertyString("sub-ass-override", "scale") // mpv's default is 'scale' - }, - ) { + TextButton(onClick = onReset) { Row { Icon(Icons.Default.EditOff, null) Text(stringResource(MR.strings.action_reset)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt index 8e2d464f82..3d171487e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsPanel.kt @@ -17,127 +17,98 @@ package eu.kanade.tachiyomi.ui.player.controls.components.panels -import android.content.res.Configuration.ORIENTATION_PORTRAIT -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PageSize -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shadow -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH +import eu.kanade.tachiyomi.ui.player.controls.components.panels.components.MultiCardPanel +import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification import tachiyomi.i18n.aniyomi.AYMR -import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable fun SubtitleSettingsPanel( onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, + // Typography card state + isBold: Boolean, + isItalic: Boolean, + justify: SubtitleJustification, + font: String, + fontSize: Int, + borderStyle: SubtitlesBorderStyle, + borderSize: Int, + shadowOffset: Int, + onIsBoldChange: (Boolean) -> Unit, + onIsItalicChange: (Boolean) -> Unit, + onJustificationChange: (SubtitleJustification) -> Unit, + onFontChange: (String) -> Unit, + onFontSizeChange: (Int) -> Unit, + onBorderStyleChange: (SubtitlesBorderStyle) -> Unit, + onBorderSizeChange: (Int) -> Unit, + onShadowOffsetChange: (Int) -> Unit, + onTypographyReset: () -> Unit, + // Colors card state + currentSubtitleColor: Int, + currentColorType: SubColorType, + onColorChange: (Int) -> Unit, + onColorReset: (SubColorType) -> Unit, + onColorTypeChange: (SubColorType) -> Unit, + // Misc card state + overrideAssSubs: Boolean, + subScale: Float, + subPos: Int, + onOverrideAssSubsChange: (Boolean) -> Unit, + onSubScaleChange: (Float) -> Unit, + onSubPosChange: (Int) -> Unit, + onMiscReset: () -> Unit, + modifier: Modifier, ) { - BackHandler(onBack = onDismissRequest) - val orientation = LocalConfiguration.current.orientation - - ConstraintLayout(modifier = modifier.fillMaxSize()) { - val subSettingsCards = createRef() - val cards: @Composable (Int, Modifier) -> Unit = { value, cardsModifier -> - when (value) { - 0 -> SubtitleSettingsTypographyCard(cardsModifier) - 1 -> SubtitleSettingsColorsCard(cardsModifier) - 2 -> SubtitlesMiscellaneousCard(cardsModifier) - else -> {} - } - } - - val pagerState = rememberPagerState { 3 } - if (orientation == ORIENTATION_PORTRAIT) { - Column( - modifier = Modifier.constrainAs(subSettingsCards) { - top.linkTo(parent.top, 32.dp) - start.linkTo(parent.start) - }, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), - ) { - TopAppBar( - title = { - Text( - text = stringResource(AYMR.strings.player_sheets_subtitles_settings_title), - style = MaterialTheme.typography.headlineMedium.copy(shadow = Shadow(blurRadius = 20f)), - ) - }, - navigationIcon = { - IconButton(onClick = onDismissRequest) { - Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) - } - }, - colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Transparent), + MultiCardPanel( + onDismissRequest = onDismissRequest, + title = stringResource(AYMR.strings.player_sheets_subtitles_settings_title), + cardCount = 3, + modifier = modifier, + ) { index, cardModifier -> + when (index) { + 0 -> SubtitleSettingsTypographyCard( + isBold = isBold, + isItalic = isItalic, + justify = justify, + font = font, + fontSize = fontSize, + borderStyle = borderStyle, + borderSize = borderSize, + shadowOffset = shadowOffset, + onIsBoldChange = onIsBoldChange, + onIsItalicChange = onIsItalicChange, + onJustificationChange = onJustificationChange, + onFontChange = onFontChange, + onFontSizeChange = onFontSizeChange, + onBorderStyleChange = onBorderStyleChange, + onBorderSizeChange = onBorderSizeChange, + onShadowOffsetChange = onShadowOffsetChange, + onReset = onTypographyReset, + modifier = cardModifier, + ) + 1 -> { + SubtitleSettingsColorsCard( + currentColor = currentSubtitleColor, + currentColorType = currentColorType, + onColorChange = onColorChange, + onColorReset = onColorReset, + onColorTypeChange = onColorTypeChange, + modifier = cardModifier, ) - HorizontalPager( - state = pagerState, - pageSize = PageSize.Fixed(LocalConfiguration.current.screenWidthDp.dp * 0.9f), - verticalAlignment = Alignment.Top, - pageSpacing = MaterialTheme.padding.small, - contentPadding = PaddingValues(horizontal = MaterialTheme.padding.small), - beyondViewportPageCount = 1, - ) { page -> - cards(page, Modifier.fillMaxWidth()) - } - } - } else { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - modifier = Modifier - .constrainAs(subSettingsCards) { - top.linkTo(parent.top) - end.linkTo(parent.end, 32.dp) - } - .verticalScroll(rememberScrollState()), - ) { - Spacer(Modifier.height(16.dp)) - Row( - Modifier - .width(CARDS_MAX_WIDTH), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(AYMR.strings.player_sheets_subtitles_settings_title), - style = MaterialTheme.typography.headlineMedium.copy( - shadow = Shadow(blurRadius = 20f), - ), - ) - IconButton(onDismissRequest) { - Icon(imageVector = Icons.Default.Close, contentDescription = null) - } - } - repeat(3) { cards(it, Modifier) } - Spacer(Modifier.height(16.dp)) } + 2 -> SubtitlesMiscellaneousCard( + overrideAssSubs = overrideAssSubs, + subScale = subScale, + subPos = subPos, + onOverrideAssSubsChange = onOverrideAssSubsChange, + onSubScaleChange = onSubScaleChange, + onSubPosChange = onSubPosChange, + onReset = onMiscReset, + modifier = cardModifier, + ) + else -> {} } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt index 4b0c7d904a..f1a2cee077 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/SubtitleSettingsTypographyCard.kt @@ -17,7 +17,6 @@ package eu.kanade.tachiyomi.ui.player.controls.components.panels -import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -67,7 +66,7 @@ import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors import eu.kanade.tachiyomi.ui.player.settings.SubtitleJustification import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences -import `is`.xyz.mpv.MPVLib +import `is`.xyz.mpv.MPV import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -80,9 +79,25 @@ import tachiyomi.presentation.core.i18n.stringResource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -@SuppressLint("MutableCollectionMutableState") @Composable fun SubtitleSettingsTypographyCard( + isBold: Boolean, + isItalic: Boolean, + justify: SubtitleJustification, + font: String, + fontSize: Int, + borderStyle: SubtitlesBorderStyle, + borderSize: Int, + shadowOffset: Int, + onIsBoldChange: (Boolean) -> Unit, + onIsItalicChange: (Boolean) -> Unit, + onJustificationChange: (SubtitleJustification) -> Unit, + onFontChange: (String) -> Unit, + onFontSizeChange: (Int) -> Unit, + onBorderStyleChange: (SubtitlesBorderStyle) -> Unit, + onBorderSizeChange: (Int) -> Unit, + onShadowOffsetChange: (Int) -> Unit, + onReset: () -> Unit, modifier: Modifier = Modifier, ) { val preferences = remember { Injekt.get() } @@ -131,34 +146,6 @@ fun SubtitleSettingsTypographyCard( colors = panelCardsColors(), ) { Column { - var isBold by remember { mutableStateOf(MPVLib.getPropertyBoolean("sub-bold")) } - var isItalic by remember { mutableStateOf(MPVLib.getPropertyBoolean("sub-italic")) } - var justify by remember { - mutableStateOf( - SubtitleJustification.entries.first { - it.value == MPVLib.getPropertyString("sub-justify") - }, - ) - } - var font by remember { mutableStateOf(MPVLib.getPropertyString("sub-font")) } - var fontSize by remember { - mutableStateOf(MPVLib.getPropertyInt("sub-font-size")) - } - var borderStyle by remember { - mutableStateOf( - SubtitlesBorderStyle.entries.first { it.value == MPVLib.getPropertyString("sub-border-style") }, - ) - } - var borderSize by remember { - mutableStateOf( - MPVLib.getPropertyInt("sub-border-size"), - ) - } - var shadowOffset by remember { - mutableStateOf( - MPVLib.getPropertyInt("sub-shadow-offset"), - ) - } Row( Modifier .fillMaxWidth() @@ -168,11 +155,7 @@ fun SubtitleSettingsTypographyCard( ) { IconToggleButton( checked = isBold, - onCheckedChange = { - isBold = it - preferences.boldSubtitles().set(it) - MPVLib.setPropertyBoolean("sub-bold", it) - }, + onCheckedChange = onIsBoldChange, ) { Icon( Icons.Default.FormatBold, @@ -182,11 +165,7 @@ fun SubtitleSettingsTypographyCard( } IconToggleButton( checked = isItalic, - onCheckedChange = { - isItalic = it - preferences.italicSubtitles().set(it) - MPVLib.setPropertyBoolean("sub-italic", it) - }, + onCheckedChange = onIsItalicChange, ) { Icon( Icons.Default.FormatItalic, @@ -197,35 +176,13 @@ fun SubtitleSettingsTypographyCard( SubtitleJustification.entries.minus(SubtitleJustification.Auto).forEach { justification -> IconToggleButton( checked = justify == justification, - onCheckedChange = { - justify = justification - MPVLib.setPropertyBoolean("sub-ass-justify", it) - if (it) { - preferences.subtitleJustification().set(justification) - MPVLib.setPropertyString("sub-justify", justification.value) - } else { - preferences.subtitleJustification().set(SubtitleJustification.Auto) - MPVLib.setPropertyString("sub-justify", SubtitleJustification.Auto.value) - } - }, + onCheckedChange = { onJustificationChange(justification) }, ) { Icon(justification.icon, null) } } Spacer(Modifier.weight(1f)) - TextButton(onClick = { - resetTypography(preferences) - isBold = MPVLib.getPropertyBoolean("sub-bold") - isItalic = MPVLib.getPropertyBoolean("sub-italic") - justify = - SubtitleJustification.entries.first { it.value == MPVLib.getPropertyString("sub-justify") } - font = MPVLib.getPropertyString("sub-font") - fontSize = MPVLib.getPropertyInt("sub-font-size") - borderStyle = - SubtitlesBorderStyle.entries.first { it.value == MPVLib.getPropertyString("sub-border-style") } - borderSize = MPVLib.getPropertyInt("sub-border-size") - shadowOffset = MPVLib.getPropertyInt("sub-shadow-offset") - }) { + TextButton(onClick = onReset) { Row( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), verticalAlignment = Alignment.CenterVertically, @@ -249,11 +206,7 @@ fun SubtitleSettingsTypographyCard( selectedValue = font, options = fonts.toImmutableList(), label = stringResource(AYMR.strings.player_sheets_sub_typography_font), - onValueChangedEvent = { - font = it - preferences.subtitleFont().set(it) - MPVLib.setPropertyString("sub-font", it) - }, + onValueChangedEvent = onFontChange, leadingIcon = fontsLoadingIndicator, ) } @@ -263,11 +216,7 @@ fun SubtitleSettingsTypographyCard( min = 1, value = fontSize, valueText = fontSize.toString(), - onChange = { - fontSize = it - preferences.subtitleFontSize().set(it) - MPVLib.setPropertyInt("sub-font-size", it) - }, + onChange = onFontSizeChange, ) { Icon(Icons.Default.FormatSize, null) } @@ -305,12 +254,7 @@ fun SubtitleSettingsTypographyCard( SubtitlesBorderStyle.entries.map { DropdownMenuItem( text = { Text(stringResource(it.titleRes)) }, - onClick = { - borderStyle = it - preferences.borderStyleSubtitles().set(it) - MPVLib.setPropertyString("sub-border-style", it.value) - selectingBorderStyle = false - }, + onClick = { onBorderStyleChange(it) }, trailingIcon = { if (borderStyle == it) { Icon( @@ -327,11 +271,7 @@ fun SubtitleSettingsTypographyCard( stringResource(AYMR.strings.player_sheets_sub_typography_border_size), value = borderSize, valueText = borderSize.toString(), - onChange = { - borderSize = it - preferences.subtitleBorderSize().set(it) - MPVLib.setPropertyInt("sub-border-size", it) - }, + onChange = onBorderSizeChange, max = 100, icon = { Icon(Icons.Default.BorderColor, null) }, ) @@ -339,11 +279,7 @@ fun SubtitleSettingsTypographyCard( stringResource(AYMR.strings.player_sheets_subtitles_shadow_offset), value = shadowOffset, valueText = shadowOffset.toString(), - onChange = { - shadowOffset = it - preferences.shadowOffsetSubtitles().set(it) - MPVLib.setPropertyInt("sub-shadow-offset", it) - }, + onChange = onShadowOffsetChange, max = 100, icon = { Icon(painterResource(R.drawable.sharp_shadow_24), null) }, ) @@ -351,18 +287,21 @@ fun SubtitleSettingsTypographyCard( } } -private val FONT_EXTENSION_REGEX = Regex(""".*\.[ot]tf${'$'}""") +private val FONT_EXTENSION_REGEX = Regex($$""".*\.[ot]tf$""") -fun resetTypography(preferences: SubtitlePreferences) { - MPVLib.setPropertyBoolean("sub-bold", preferences.boldSubtitles().deleteAndGet()) - MPVLib.setPropertyBoolean("sub-italic", preferences.italicSubtitles().deleteAndGet()) - MPVLib.setPropertyBoolean("sub-ass-justify", preferences.overrideSubsASS().deleteAndGet()) - MPVLib.setPropertyString("sub-justify", preferences.subtitleJustification().deleteAndGet().value) - MPVLib.setPropertyString("sub-font", preferences.subtitleFont().deleteAndGet()) - MPVLib.setPropertyInt("sub-font-size", preferences.subtitleFontSize().deleteAndGet()) - MPVLib.setPropertyInt("sub-border-size", preferences.subtitleBorderSize().deleteAndGet()) - MPVLib.setPropertyInt("sub-shadow-offset", preferences.shadowOffsetSubtitles().deleteAndGet()) - MPVLib.setPropertyString("sub-border-style", preferences.borderStyleSubtitles().deleteAndGet().value) +fun resetTypography( + mpv: MPV, + preferences: SubtitlePreferences, +) { + mpv.setPropertyBoolean("sub-bold", preferences.boldSubtitles().deleteAndGet()) + mpv.setPropertyBoolean("sub-italic", preferences.italicSubtitles().deleteAndGet()) + mpv.setPropertyBoolean("sub-ass-justify", preferences.overrideSubsASS().deleteAndGet()) + mpv.setPropertyString("sub-justify", preferences.subtitleJustification().deleteAndGet().value) + mpv.setPropertyString("sub-font", preferences.subtitleFont().deleteAndGet()) + mpv.setPropertyInt("sub-font-size", preferences.subtitleFontSize().deleteAndGet()) + mpv.setPropertyInt("sub-outline-size", preferences.subtitleBorderSize().deleteAndGet()) + mpv.setPropertyInt("sub-shadow-offset", preferences.shadowOffsetSubtitles().deleteAndGet()) + mpv.setPropertyString("sub-border-style", preferences.borderStyleSubtitles().deleteAndGet().value) } enum class SubtitlesBorderStyle( @@ -372,4 +311,16 @@ enum class SubtitlesBorderStyle( OutlineAndShadow("outline-and-shadow", AYMR.strings.player_sheets_subtitles_border_style_outline_and_shadow), OpaqueBox("opaque-box", AYMR.strings.player_sheets_subtitles_border_style_opaque_box), BackgroundBox("background-box", AYMR.strings.player_sheets_subtitles_border_style_background_box), + ; + + companion object { + fun byValue(value: String): SubtitlesBorderStyle { + return when (value) { + "outline-and-shadow" -> OutlineAndShadow + "opaque-box" -> OpaqueBox + "background-box" -> BackgroundBox + else -> throw IllegalArgumentException("Unsupported border style: $value") + } + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoFiltersPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoFiltersPanel.kt deleted file mode 100644 index 61b6327b90..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoFiltersPanel.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2024 Abdallah Mehiz - * https://github.com/abdallahmehiz/mpvKt - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.kanade.tachiyomi.ui.player.controls.components.panels - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.constraintlayout.compose.ConstraintLayout -import eu.kanade.presentation.player.components.SliderItem -import eu.kanade.tachiyomi.ui.player.VideoFilters -import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH -import eu.kanade.tachiyomi.ui.player.controls.components.ControlsButton -import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors -import eu.kanade.tachiyomi.ui.player.settings.DecoderPreferences -import `is`.xyz.mpv.MPVLib -import tachiyomi.core.common.preference.deleteAndGet -import tachiyomi.i18n.MR -import tachiyomi.i18n.aniyomi.AYMR -import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.collectAsState -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -@Composable -fun VideoFiltersPanel( - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, -) { - ConstraintLayout( - modifier = modifier - .fillMaxSize() - .padding(MaterialTheme.padding.medium), - ) { - val filtersCard = createRef() - - FiltersCard( - Modifier.constrainAs(filtersCard) { - linkTo(parent.top, parent.bottom, bias = 0.8f) - end.linkTo(parent.end) - }, - onClose = onDismissRequest, - ) - } -} - -@Composable -fun FiltersCard( - modifier: Modifier = Modifier, - onClose: () -> Unit, -) { - val decoderPreferences = remember { Injekt.get() } - Card( - colors = panelCardsColors(), - modifier = modifier - .widthIn(max = CARDS_MAX_WIDTH), - ) { - Row( - Modifier - .fillMaxWidth() - .padding(start = MaterialTheme.padding.medium), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - stringResource(AYMR.strings.player_sheets_filters_title), - style = MaterialTheme.typography.headlineMedium, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), - ) { - TextButton( - onClick = { - VideoFilters.entries.forEach { - MPVLib.setPropertyInt(it.mpvProperty, it.preference(decoderPreferences).deleteAndGet()) - } - }, - ) { - Text(text = stringResource(MR.strings.action_reset)) - } - ControlsButton(Icons.Default.Close, onClose) - } - } - LazyColumn { - items(VideoFilters.entries) { filter -> - val value by filter.preference(decoderPreferences).collectAsState() - SliderItem( - label = stringResource(filter.titleRes), - value = value, - valueText = value.toString(), - onChange = { - filter.preference(decoderPreferences).set(it) - MPVLib.setPropertyInt(filter.mpvProperty, it) - }, - max = 100, - min = -100, - ) - } - item { - if (decoderPreferences.gpuNext().get()) return@item - Column( - modifier = Modifier - .padding(MaterialTheme.padding.medium) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), - horizontalAlignment = Alignment.Start, - ) { - Icon(Icons.Outlined.Info, null) - Text(stringResource(AYMR.strings.player_sheets_filters_warning)) - } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsDebandCard.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsDebandCard.kt new file mode 100644 index 0000000000..146a0a7b6f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsDebandCard.kt @@ -0,0 +1,111 @@ +package eu.kanade.tachiyomi.ui.player.controls.components.panels + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Gradient +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material.icons.filled.NotInterested +import androidx.compose.material3.Icon +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import eu.kanade.presentation.player.components.ExpandableCard +import eu.kanade.presentation.player.components.SliderItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.player.DebandSettings +import eu.kanade.tachiyomi.ui.player.Debanding +import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH +import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors +import tachiyomi.i18n.MR +import tachiyomi.i18n.animiru.AMMR +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource + +@Composable +fun VideoSettingsDebandCard( + deband: Debanding, + onDebandingChange: (Debanding) -> Unit, + debandSettingsValue: (DebandSettings) -> Int, + onDebandingSettingsChange: (DebandSettings, Int) -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier, +) { + var isExpanded by remember { mutableStateOf(true) } + + ExpandableCard( + isExpanded, + title = { + Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium)) { + Icon(Icons.Default.Gradient, null) + Text(stringResource(AMMR.strings.player_sheets_deband_title)) + } + }, + onExpand = { isExpanded = !isExpanded }, + modifier.widthIn(max = CARDS_MAX_WIDTH), + colors = panelCardsColors(), + ) { + Column { + Row( + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(start = MaterialTheme.padding.extraSmall, end = MaterialTheme.padding.medium), + verticalAlignment = Alignment.CenterVertically, + ) { + Debanding.entries.forEach { + IconToggleButton( + checked = deband == it, + onCheckedChange = { _ -> onDebandingChange(it) }, + ) { + when (it) { + Debanding.None -> Icon(Icons.Default.NotInterested, null) + Debanding.CPU -> Icon(Icons.Default.Memory, null) + Debanding.GPU -> Icon(painterResource(R.drawable.expansion_card), null) + } + } + } + + Text(stringResource(deband.stringRes)) + + Spacer(Modifier.weight(1f)) + TextButton(onClick = onReset) { + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(painterResource(R.drawable.reset_iso_24px), null) + Text(stringResource(MR.strings.action_reset)) + } + } + } + + DebandSettings.entries.forEach { debandSettings -> + SliderItem( + label = stringResource(debandSettings.stringRes), + value = debandSettingsValue(debandSettings), + valueText = debandSettingsValue(debandSettings).toString(), + onChange = { onDebandingSettingsChange(debandSettings, it) }, + min = debandSettings.start, + max = debandSettings.end, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsFiltersCard.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsFiltersCard.kt new file mode 100644 index 0000000000..2c9ea678f6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsFiltersCard.kt @@ -0,0 +1,88 @@ +package eu.kanade.tachiyomi.ui.player.controls.components.panels + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.player.components.ExpandableCard +import eu.kanade.presentation.player.components.SliderItem +import eu.kanade.tachiyomi.ui.player.VideoFilters +import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH +import eu.kanade.tachiyomi.ui.player.controls.panelCardsColors +import tachiyomi.i18n.MR +import tachiyomi.i18n.aniyomi.AYMR +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource + +@Composable +fun VideoSettingsFiltersCard( + isGpuNextEnabled: Boolean, + filterValue: (VideoFilters) -> Int, + onFilterValueChange: (VideoFilters, Int) -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier, +) { + var isExpanded by remember { mutableStateOf(true) } + + ExpandableCard( + isExpanded = isExpanded, + onExpand = { isExpanded = !isExpanded }, + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), + ) { + Icon(Icons.Default.Tune, null) + Text(stringResource(AYMR.strings.player_sheets_filters_title)) + } + }, + colors = panelCardsColors(), + modifier = modifier.widthIn(max = CARDS_MAX_WIDTH), + ) { + Column { + TextButton(onClick = onReset) { + Text(text = stringResource(MR.strings.action_reset)) + } + + VideoFilters.entries.forEach { filter -> + val value = filterValue(filter) + SliderItem( + label = stringResource(filter.titleRes), + value = value, + valueText = value.toString(), + onChange = { onFilterValueChange(filter, it) }, + max = 100, + min = -100, + ) + } + + if (!isGpuNextEnabled) { + Column( + modifier = Modifier + .padding(MaterialTheme.padding.medium) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), + horizontalAlignment = Alignment.Start, + ) { + Icon(Icons.Outlined.Info, null) + Text(stringResource(AYMR.strings.player_sheets_filters_warning)) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsPanel.kt new file mode 100644 index 0000000000..43e0ff35fd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/VideoSettingsPanel.kt @@ -0,0 +1,53 @@ +package eu.kanade.tachiyomi.ui.player.controls.components.panels + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import eu.kanade.tachiyomi.ui.player.DebandSettings +import eu.kanade.tachiyomi.ui.player.Debanding +import eu.kanade.tachiyomi.ui.player.VideoFilters +import eu.kanade.tachiyomi.ui.player.controls.components.panels.components.MultiCardPanel +import tachiyomi.i18n.animiru.AMMR +import tachiyomi.presentation.core.i18n.stringResource + +@Composable +fun VideoSettingsPanel( + onDismissRequest: () -> Unit, + onVideoFilterChange: (VideoFilters, Int) -> Unit, + // Deband settings + deband: Debanding, + onDebandChange: (Debanding) -> Unit, + debandSettings: (DebandSettings) -> Int, + onDebandSettingsChange: (DebandSettings, Int) -> Unit, + onDebandReset: () -> Unit, + // Filter settings + isGpuNextEnabled: Boolean, + filterValue: (VideoFilters) -> Int, + onFilterReset: () -> Unit, + modifier: Modifier = Modifier, +) { + MultiCardPanel( + onDismissRequest = onDismissRequest, + title = stringResource(AMMR.strings.player_sheets_video_settings_title), + cardCount = 2, + modifier = modifier, + ) { index, cardModifier -> + when (index) { + 0 -> VideoSettingsDebandCard( + deband = deband, + onDebandingChange = onDebandChange, + debandSettingsValue = debandSettings, + onDebandingSettingsChange = onDebandSettingsChange, + onReset = onDebandReset, + modifier = cardModifier, + ) + 1 -> VideoSettingsFiltersCard( + isGpuNextEnabled = isGpuNextEnabled, + filterValue = filterValue, + onFilterValueChange = onVideoFilterChange, + onReset = onFilterReset, + modifier = cardModifier, + ) + else -> {} + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/components/MultiCardPanel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/components/MultiCardPanel.kt new file mode 100644 index 0000000000..232da270e0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/panels/components/MultiCardPanel.kt @@ -0,0 +1,126 @@ +package eu.kanade.tachiyomi.ui.player.controls.components.panels.components + +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import eu.kanade.tachiyomi.ui.player.controls.CARDS_MAX_WIDTH +import tachiyomi.presentation.core.components.material.padding + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun MultiCardPanel( + onDismissRequest: () -> Unit, + title: String, + cardCount: Int, + modifier: Modifier = Modifier, + cards: @Composable (Int, Modifier) -> Unit, +) { + BackHandler(onBack = onDismissRequest) + val orientation = LocalConfiguration.current.orientation + val cards = remember { movableContentOf { p1: Int, p2: Modifier -> cards(p1, p2) } } + + ConstraintLayout(modifier = modifier.fillMaxSize()) { + val settingsCards = createRef() + + val pagerState = rememberPagerState { cardCount } + if (orientation == ORIENTATION_PORTRAIT) { + Column( + modifier = Modifier.constrainAs(settingsCards) { + top.linkTo(parent.top, 32.dp) + start.linkTo(parent.start) + }, + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + ) { + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineMedium.copy(shadow = Shadow(blurRadius = 20f)), + ) + }, + navigationIcon = { + IconButton(onClick = onDismissRequest) { + Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) + } + }, + colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Transparent), + ) + HorizontalPager( + state = pagerState, + pageSize = PageSize.Fixed(LocalConfiguration.current.screenWidthDp.dp * 0.9f), + verticalAlignment = Alignment.Top, + pageSpacing = MaterialTheme.padding.small, + contentPadding = PaddingValues(horizontal = MaterialTheme.padding.small), + beyondViewportPageCount = 1, + ) { page -> + cards(page, Modifier.fillMaxWidth()) + } + } + } else { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + modifier = Modifier + .constrainAs(settingsCards) { + top.linkTo(parent.top) + end.linkTo(parent.end, 32.dp) + } + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(16.dp)) + Row( + Modifier + .width(CARDS_MAX_WIDTH), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineMedium.copy( + shadow = Shadow(blurRadius = 20f), + ), + ) + IconButton(onDismissRequest) { + Icon(imageVector = Icons.Default.Close, contentDescription = null) + } + } + repeat(cardCount) { cards(it, Modifier) } + Spacer(Modifier.height(16.dp)) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/AudioTracksSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/AudioTracksSheet.kt index f1fa90e567..22225395d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/AudioTracksSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/AudioTracksSheet.kt @@ -20,10 +20,14 @@ package eu.kanade.tachiyomi.ui.player.controls.components.sheets import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material.icons.filled.MoreTime +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -34,7 +38,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import eu.kanade.tachiyomi.ui.player.PlayerViewModel.VideoTrack +import androidx.compose.ui.unit.dp +import eu.kanade.tachiyomi.ui.player.TrackState +import eu.kanade.tachiyomi.ui.player.VideoTrack import kotlinx.collections.immutable.ImmutableList import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding @@ -43,8 +49,7 @@ import tachiyomi.presentation.core.i18n.stringResource @Composable fun AudioTracksSheet( tracks: ImmutableList, - selectedId: Int, - onSelect: (Int) -> Unit, + onSelect: (VideoTrack) -> Unit, onAddAudioTrack: () -> Unit, onOpenDelayPanel: () -> Unit, onDismissRequest: () -> Unit, @@ -76,9 +81,9 @@ fun AudioTracksSheet( }, track = { AudioTrackRow( - title = getTrackTitle(it), - isSelected = selectedId == it.id, - onClick = { onSelect(it.id) }, + track = it, + isSelected = it.selection > -1, + onClick = { onSelect(it) }, ) }, modifier = modifier, @@ -87,7 +92,7 @@ fun AudioTracksSheet( @Composable fun AudioTrackRow( - title: String, + track: VideoTrack, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -105,9 +110,15 @@ fun AudioTrackRow( onClick = onClick, ) Text( - title, + text = getTrackTitle(track), fontWeight = if (isSelected) FontWeight.ExtraBold else FontWeight.Normal, fontStyle = if (isSelected) FontStyle.Italic else FontStyle.Normal, ) + Spacer(modifier = Modifier.weight(1f)) + if (track is VideoTrack.External && track.state == TrackState.Loading) { + CircularProgressIndicator(modifier = Modifier.then(Modifier.size(24.dp))) + } else if (track is VideoTrack.External && track.state == TrackState.Error) { + Icon(Icons.Default.ErrorOutline, null, tint = MaterialTheme.colorScheme.error) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/GenericTracksSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/GenericTracksSheet.kt index e5e87363a3..33ad4721b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/GenericTracksSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/GenericTracksSheet.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import eu.kanade.presentation.player.components.PlayerSheet -import eu.kanade.tachiyomi.ui.player.PlayerViewModel.VideoTrack +import eu.kanade.tachiyomi.ui.player.VideoTrack import kotlinx.collections.immutable.ImmutableList import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding @@ -106,25 +106,40 @@ fun AddTrackRow( } @Composable -fun getTrackTitle(track: VideoTrack): String { - return when { - track.id == -1 -> { - track.name - } - - track.language.isNullOrBlank() && track.name.isNotBlank() -> { - stringResource(AYMR.strings.player_sheets_track_title_wo_lang, track.id, track.name) - } +fun getTrackTitle(videoTrack: VideoTrack): String { + return when (videoTrack) { + is VideoTrack.External -> videoTrack.title + is VideoTrack.Internal -> { + val track = videoTrack.data - !track.language.isNullOrBlank() && track.name.isNotBlank() -> { - stringResource(AYMR.strings.player_sheets_track_title_w_lang, track.id, track.name, track.language) - } + val hasTitle = !track.title.isNullOrBlank() + val hasLang = !track.lang.isNullOrBlank() - !track.language.isNullOrBlank() && track.name.isBlank() -> { - stringResource(AYMR.strings.player_sheets_track_lang_wo_title, track.id, track.language) + when { + hasTitle && hasLang -> stringResource( + AYMR.strings.player_sheets_track_title_w_lang, + track.id, + track.title, + track.lang, + ) + hasTitle && !hasLang -> stringResource( + AYMR.strings.player_sheets_track_title_wo_lang, + track.id, + track.title, + ) + !hasTitle && hasLang -> stringResource( + AYMR.strings.player_sheets_track_lang_wo_title, + track.id, + track.lang, + ) + track.isSubtitle -> stringResource( + AYMR.strings.player_sheets_chapter_title_substitute_subtitle, + track.id, + ) + track.isAudio -> stringResource(AYMR.strings.player_sheets_chapter_title_substitute_audio, track.id) + else -> "" // idk what to show tbh + } } - - else -> stringResource(AYMR.strings.player_sheets_track_title_wo_lang, track.id, track.name) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/MoreSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/MoreSheet.kt index 5f86aa79e5..2a9673bad3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/MoreSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/MoreSheet.kt @@ -65,37 +65,31 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import eu.kanade.presentation.player.components.PlayerSheet import eu.kanade.tachiyomi.ui.player.Decoder -import eu.kanade.tachiyomi.ui.player.execute -import eu.kanade.tachiyomi.ui.player.executeLongPress -import eu.kanade.tachiyomi.ui.player.settings.AdvancedPlayerPreferences import eu.kanade.tachiyomi.ui.player.settings.AudioChannels -import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences -import `is`.xyz.mpv.MPVLib import kotlinx.collections.immutable.ImmutableList import tachiyomi.domain.custombutton.model.CustomButton import tachiyomi.i18n.MR import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.collectAsState -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get @Composable fun MoreSheet( + statisticsPage: Int, + audioChannels: AudioChannels, selectedDecoder: Decoder, onSelectDecoder: (Decoder) -> Unit, remainingTime: Int, onStartTimer: (Int) -> Unit, + onStatisticsPageChange: (Int) -> Unit, + onCustomButtonClick: (CustomButton) -> Unit, + onCustomButtonLongClick: (CustomButton) -> Unit, + onAudioChannelsChange: (AudioChannels) -> Unit, onDismissRequest: () -> Unit, onEnterFiltersPanel: () -> Unit, customButtons: ImmutableList, modifier: Modifier = Modifier, ) { - val advancedPreferences = remember { Injekt.get() } - val audioPreferences = remember { Injekt.get() } - val statisticsPage by advancedPreferences.playerStatisticsPage().collectAsState() - PlayerSheet( onDismissRequest = onDismissRequest, modifier = modifier, @@ -192,15 +186,7 @@ fun MoreSheet( ), ) }, - onClick = { - if ((page == 0) xor (statisticsPage == 0)) { - MPVLib.command(arrayOf("script-binding", "stats/display-stats-toggle")) - } - if (page != 0) { - MPVLib.command(arrayOf("script-binding", "stats/display-page-$page")) - } - advancedPreferences.playerStatisticsPage().set(page) - }, + onClick = { onStatisticsPageChange(page) }, selected = statisticsPage == page, ) } @@ -228,8 +214,8 @@ fun MoreSheet( modifier = Modifier .matchParentSize() .combinedClickable( - onClick = { button.execute() }, - onLongClick = { button.executeLongPress() }, + onClick = { onCustomButtonClick(button) }, + onLongClick = { onCustomButtonLongClick(button) }, interactionSource = inputChipInteractionSource, indication = null, ), @@ -239,22 +225,13 @@ fun MoreSheet( } } Text(text = stringResource(AYMR.strings.pref_audio_channels)) - val audioChannels by audioPreferences.audioChannels().collectAsState() LazyRow( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { items(AudioChannels.entries) { FilterChip( selected = audioChannels == it, - onClick = { - audioPreferences.audioChannels().set(it) - if (it == AudioChannels.ReverseStereo) { - MPVLib.setPropertyString(AudioChannels.AutoSafe.property, AudioChannels.AutoSafe.value) - } else { - MPVLib.setPropertyString(AudioChannels.ReverseStereo.property, "") - } - MPVLib.setPropertyString(it.property, it.value) - }, + onClick = { onAudioChannelsChange(it) }, label = { Text(text = stringResource(it.titleRes)) }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/PlaybackSpeedSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/PlaybackSpeedSheet.kt index b7d210d1e3..9f4637a4f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/PlaybackSpeedSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/PlaybackSpeedSheet.kt @@ -39,34 +39,32 @@ import androidx.compose.material3.InputChip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import eu.kanade.presentation.player.components.PlayerSheet import eu.kanade.presentation.player.components.SliderItem import eu.kanade.presentation.player.components.SwitchPreference -import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences -import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences -import `is`.xyz.mpv.MPVLib import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.collectAsState -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import kotlin.math.pow import kotlin.math.roundToInt @Composable fun PlaybackSpeedSheet( + pitchCorrection: Boolean, + onPitchCorrectionChange: (Boolean) -> Unit, speed: Float, + speedPresets: List, onSpeedChange: (Float) -> Unit, + onAddSpeedPreset: (Float) -> Unit, + onRemoveSpeedPreset: (Float) -> Unit, + onResetPresets: () -> Unit, + onMakeDefault: (Float) -> Unit, + onResetDefault: () -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ) { - val preferences = remember { Injekt.get() } - val audioPreferences = remember { Injekt.get() } PlayerSheet(onDismissRequest = onDismissRequest) { Column( modifier @@ -81,7 +79,6 @@ fun PlaybackSpeedSheet( max = 6f, min = 0.01f, ) - val playbackSpeedPresets by preferences.speedPresets().collectAsState() Row( modifier = Modifier .fillMaxWidth() @@ -89,9 +86,7 @@ fun PlaybackSpeedSheet( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), ) { - FilledTonalIconButton(onClick = { - preferences.speedPresets().delete() - }) { + FilledTonalIconButton(onClick = onResetPresets) { Icon(Icons.Default.RestartAlt, null) } LazyRow( @@ -99,10 +94,7 @@ fun PlaybackSpeedSheet( .weight(1f), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), ) { - items( - playbackSpeedPresets.map { it.toFloat() }.sorted(), - key = { it }, - ) { + items(speedPresets, key = { it }) { InputChip( selected = speed == it, onClick = { onSpeedChange(it) }, @@ -113,32 +105,19 @@ fun PlaybackSpeedSheet( Icon( Icons.Default.Close, null, - modifier = Modifier - .clickable { - preferences.speedPresets().set( - playbackSpeedPresets.minus(it.toFixed(2).toString()), - ) - }, + modifier = Modifier.clickable { onRemoveSpeedPreset(it.toFixed(2)) }, ) }, ) } } - FilledTonalIconButton( - onClick = { - preferences.speedPresets().set(playbackSpeedPresets.plus(speed.toFixed(2).toString())) - }, - ) { + FilledTonalIconButton(onClick = { onAddSpeedPreset(speed.toFixed(2)) }) { Icon(Icons.Default.Add, null) } } - val pitchCorrection by audioPreferences.enablePitchCorrection().collectAsState() SwitchPreference( value = pitchCorrection, - onValueChange = { - audioPreferences.enablePitchCorrection().set(it) - MPVLib.setPropertyBoolean("audio-pitch-correction", it) - }, + onValueChange = onPitchCorrectionChange, content = { Column( modifier = Modifier.weight(1f), @@ -158,16 +137,11 @@ fun PlaybackSpeedSheet( ) { Button( modifier = Modifier.weight(1f), - onClick = { preferences.playerSpeed().set(speed) }, + onClick = { onMakeDefault(speed) }, ) { Text(text = stringResource(AYMR.strings.player_sheets_speed_make_default)) } - FilledIconButton( - onClick = { - preferences.playerSpeed().delete() - onSpeedChange(1f) - }, - ) { + FilledIconButton(onClick = onResetDefault) { Icon(imageVector = Icons.Default.RestartAlt, contentDescription = null) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/QualitySheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/QualitySheet.kt index f6c8ddecbd..8ea24fd091 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/QualitySheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/QualitySheet.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -42,12 +43,14 @@ import androidx.compose.ui.unit.dp import eu.kanade.presentation.player.components.PlayerSheet import eu.kanade.tachiyomi.animesource.model.Hoster import eu.kanade.tachiyomi.animesource.model.Video +import kotlinx.collections.immutable.ImmutableList import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource +@Stable sealed class HosterState(open val name: String) { data class Idle(override val name: String) : HosterState(name) data class Loading(override val name: String) : HosterState(name) @@ -74,8 +77,8 @@ fun HosterState.Ready.getChangedAt(index: Int, newVideo: Video, newState: Video. @Composable fun QualitySheet( isLoadingHosters: Boolean, - hosterState: List, - expandedState: List, + hosterState: ImmutableList, + expandedState: ImmutableList, selectedVideoIndex: Pair, onClickHoster: (Int) -> Unit, onClickVideo: (Int, Int) -> Unit, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt index 852b2b4451..18cbe5416b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/components/sheets/SubtitleTracksSheet.kt @@ -24,11 +24,14 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material.icons.filled.MoreTime import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -38,7 +41,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import eu.kanade.tachiyomi.ui.player.PlayerViewModel.VideoTrack +import androidx.compose.ui.unit.dp +import eu.kanade.tachiyomi.ui.player.TrackState +import eu.kanade.tachiyomi.ui.player.VideoTrack import kotlinx.collections.immutable.ImmutableList import tachiyomi.i18n.aniyomi.AYMR import tachiyomi.presentation.core.components.material.padding @@ -47,8 +52,7 @@ import tachiyomi.presentation.core.i18n.stringResource @Composable fun SubtitlesSheet( tracks: ImmutableList, - selectedTracks: ImmutableList, - onSelect: (Int) -> Unit, + onSelect: (VideoTrack) -> Unit, onAddSubtitle: () -> Unit, onOpenSubtitleSettings: () -> Unit, onOpenSubtitleDelay: () -> Unit, @@ -89,9 +93,9 @@ fun SubtitlesSheet( }, track = { track -> SubtitleTrackRow( - title = getTrackTitle(track), - selected = selectedTracks.indexOf(track.id), - onClick = { onSelect(track.id) }, + track = track, + selected = track.selection, + onClick = { onSelect(track) }, ) }, footer = { @@ -112,8 +116,8 @@ fun SubtitlesSheet( @Composable fun SubtitleTrackRow( - title: String, - selected: Int, // -1 unselected, otherwise return 0 and 1 for the selected indices + track: VideoTrack, + selected: Int, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -129,12 +133,16 @@ fun SubtitleTrackRow( onCheckedChange = { _ -> onClick() }, ) Text( - text = title, + text = getTrackTitle(track), fontStyle = if (selected > -1) FontStyle.Italic else FontStyle.Normal, fontWeight = if (selected > -1) FontWeight.ExtraBold else FontWeight.Normal, ) Spacer(modifier = Modifier.weight(1f)) - if (selected != -1) { + if (track is VideoTrack.External && track.state == TrackState.Loading) { + CircularProgressIndicator(modifier = Modifier.then(Modifier.size(24.dp))) + } else if (track is VideoTrack.External && track.state == TrackState.Error) { + Icon(Icons.Default.ErrorOutline, null, tint = MaterialTheme.colorScheme.error) + } else if (selected != -1) { Text( text = "#${selected + 1}", fontStyle = if (selected > -1) FontStyle.Italic else FontStyle.Normal, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/AudioManager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/AudioManager.kt new file mode 100644 index 0000000000..9c5b9e0384 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/AudioManager.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.ui.player.domain + +import android.content.Context +import android.media.AudioManager + +class AudioManager( + private val context: Context, +) { + private val audioManager by lazy { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } + + fun getVolume(): Int { + return audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + } + + fun getMaxVolume(): Int { + return audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + } + + fun setVolume(volume: Int) { + audioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, + volume, + 0, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/BrightnessManager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/BrightnessManager.kt new file mode 100644 index 0000000000..1d813b2859 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/BrightnessManager.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.ui.player.domain + +import android.content.Context +import android.provider.Settings +import eu.kanade.tachiyomi.ui.player.normalize + +class BrightnessManager( + private val context: Context, +) { + fun getCurrentBrightness(): Float { + return runCatching { + Settings.System.getFloat(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS) + .normalize(0f, 255f, 0f, 1f) + }.getOrElse { 0f } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/TrackSelect.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/TrackSelect.kt similarity index 79% rename from app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/TrackSelect.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/TrackSelect.kt index ec27e1478f..988272368e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/TrackSelect.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/domain/TrackSelect.kt @@ -1,7 +1,7 @@ -package eu.kanade.tachiyomi.ui.player.utils +package eu.kanade.tachiyomi.ui.player.domain import androidx.core.os.LocaleListCompat -import eu.kanade.tachiyomi.ui.player.PlayerViewModel.VideoTrack +import eu.kanade.tachiyomi.ui.player.VideoTrack import eu.kanade.tachiyomi.ui.player.settings.AudioPreferences import eu.kanade.tachiyomi.ui.player.settings.SubtitlePreferences import uy.kohesive.injekt.Injekt @@ -12,7 +12,6 @@ class TrackSelect( private val subtitlePreferences: SubtitlePreferences = Injekt.get(), private val audioPreferences: AudioPreferences = Injekt.get(), ) { - fun getPreferredTrackIndex(tracks: List, subtitle: Boolean = true): VideoTrack? { val prefLangs = if (subtitle) { subtitlePreferences.preferredSubLanguages().get() @@ -38,18 +37,18 @@ class TrackSelect( val chosenLocale = locales.firstOrNull { locale -> tracks.any { t -> containsLang(t, locale) } - } ?: return null + } val filtered = tracks.withIndex() .filterNot { (_, track) -> - blacklist.any { track.name.contains(it, true) } + blacklist.any { track.title.contains(it, true) } } .filter { (_, track) -> - containsLang(track, chosenLocale) + chosenLocale?.let { containsLang(track, it) } ?: true } return filtered.firstOrNull { (_, track) -> - whitelist.any { track.name.contains(it, true) } + whitelist.any { track.title.contains(it, true) } }?.value ?: filtered.getOrNull(0)?.value } @@ -57,9 +56,10 @@ class TrackSelect( val localName = locale.getDisplayName(locale) val englishName = locale.getDisplayName(Locale.ENGLISH).substringBefore(" (") val langRegex = Regex("""\b${locale.isO3Language}|${locale.language}\b""", RegexOption.IGNORE_CASE) + val trackTitle = track.title - return track.name.contains(localName, true) || - track.name.contains(englishName, true) || - track.language?.let { langRegex.find(it) != null } == true + return trackTitle.contains(localName, true) || + trackTitle.contains(englishName, true) || + track.lang.let { langRegex.find(it) != null } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/HosterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/HosterLoader.kt index 40d17e5249..cd56fbb831 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/HosterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/loader/HosterLoader.kt @@ -47,7 +47,7 @@ class HosterLoader { // Check for first video with non-empty url val firstValid: (Pair) -> Boolean = { (v, s) -> - v.videoUrl.isNotEmpty() && (s == Video.State.READY || s == Video.State.QUEUE) + (v.videoUrl.isNotEmpty() && s == Video.State.READY) || s == Video.State.QUEUE } val firstAvailableHosterIdx = availableHosters.indexOfFirst { (it.value as HosterState.Ready).let { hoster -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/DecoderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/DecoderPreferences.kt index 9313fe1cd6..9e7af3fd15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/DecoderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/DecoderPreferences.kt @@ -9,9 +9,14 @@ class DecoderPreferences( ) { fun tryHWDecoding() = preferenceStore.getBoolean("pref_try_hwdec", true) fun gpuNext() = preferenceStore.getBoolean("pref_gpu_next", false) - fun videoDebanding() = preferenceStore.getEnum("pref_video_debanding", Debanding.None) fun useYUV420P() = preferenceStore.getBoolean("use_yuv420p", true) + fun debanding() = preferenceStore.getEnum("pref_video_debanding", Debanding.None) + fun debandIterations() = preferenceStore.getInt("deband_iterations", 1) + fun debandThreshold() = preferenceStore.getInt("deband_threshold", 48) + fun debandRange() = preferenceStore.getInt("deband_range", 16) + fun debandGrain() = preferenceStore.getInt("deband_grain", 32) + // Non-preferences fun brightnessFilter() = preferenceStore.getInt("pref_player_filter_brightness") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt index f8395b97e1..ef1d0d2a25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt @@ -12,6 +12,10 @@ class PlayerPreferences( "pref_preserve_watching_position", false, ) + fun switchOnFailure() = preferenceStore.getBoolean( + "pref_player_switch_on_failure", + true, + ) fun progressPreference() = preferenceStore.getFloat("pref_progress_preference", 0.85F) fun defaultPlayerOrientationType() = preferenceStore.getEnum( "pref_default_player_orientation_type_key", diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt index af9e7a51b6..c16dc71faa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/SubtitlePreferences.kt @@ -61,4 +61,16 @@ enum class SubtitleJustification( Center("center", Icons.Default.FormatAlignCenter), Right("right", Icons.AutoMirrored.Default.FormatAlignRight), Auto("auto", Icons.Default.FormatAlignJustify), + ; + + companion object { + fun byValue(value: String): SubtitleJustification { + return when (value) { + "left" -> Left + "center" -> Center + "right" -> Right + else -> Auto + } + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/ChapterUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/ChapterUtils.kt index deff73e58e..f44ab717b5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/ChapterUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/utils/ChapterUtils.kt @@ -10,6 +10,8 @@ import kotlin.math.abs class ChapterUtils { companion object { + const val ANIYOMI_CHAPTER_IDENTIFIER = ";aniyomi=" + fun ChapterType.getStringRes(): StringResource? = when (this) { ChapterType.Opening -> AYMR.strings.player_chapter_type_opening ChapterType.Ending -> AYMR.strings.player_chapter_type_ending @@ -32,7 +34,7 @@ class ChapterUtils { } val startChapter = IndexedSegment( index = -2, // Index -2 is used to indicate that this is an external chapter - name = it.name, + name = it.name + ANIYOMI_CHAPTER_IDENTIFIER + it.type.ordinal, start = startTime.toFloat(), color = if (it.type == ChapterType.Other) Color.Unspecified else Color(0xFFD8BBDF), chapterType = it.type, diff --git a/app/src/main/res/drawable/expansion_card.xml b/app/src/main/res/drawable/expansion_card.xml new file mode 100644 index 0000000000..20dccfbc25 --- /dev/null +++ b/app/src/main/res/drawable/expansion_card.xml @@ -0,0 +1 @@ + diff --git a/app/src/main/res/drawable/reset_iso_24px.xml b/app/src/main/res/drawable/reset_iso_24px.xml new file mode 100644 index 0000000000..96029820b6 --- /dev/null +++ b/app/src/main/res/drawable/reset_iso_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/player_layout.xml b/app/src/main/res/layout/player_layout.xml deleted file mode 100644 index 22ce00fa64..0000000000 --- a/app/src/main/res/layout/player_layout.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt index 916ec38f42..da2cec5382 100644 --- a/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt +++ b/buildSrc/src/main/kotlin/mihon/buildlogic/ProjectExtensions.kt @@ -42,6 +42,9 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *, compilerOptions { jvmTarget.set(AndroidConfig.JvmTarget) freeCompilerArgs.addAll( + // AM --> + "-Xwhen-guards", + // <-- AM "-Xcontext-receivers", "-opt-in=kotlin.RequiresOptIn", ) diff --git a/domain/src/main/java/tachiyomi/domain/custombutton/model/CustomButton.kt b/domain/src/main/java/tachiyomi/domain/custombutton/model/CustomButton.kt index 650afee944..c0530e9c37 100644 --- a/domain/src/main/java/tachiyomi/domain/custombutton/model/CustomButton.kt +++ b/domain/src/main/java/tachiyomi/domain/custombutton/model/CustomButton.kt @@ -1,6 +1,9 @@ // AY --> package tachiyomi.domain.custombutton.model +import androidx.compose.runtime.Stable + +@Stable data class CustomButton( val id: Long, val name: String, @@ -12,17 +15,17 @@ data class CustomButton( ) { fun getButtonContent(primaryId: Long): String { val isPrimary = if (primaryId == id) "true" else "false" - return content.replace("${'$'}id", id.toString()).replace("${'$'}isPrimary", isPrimary) + return content.replace($$"$id", id.toString()).replace($$"$isPrimary", isPrimary) } fun getButtonLongPressContent(primaryId: Long): String { val isPrimary = if (primaryId == id) "true" else "false" - return longPressContent.replace("${'$'}id", id.toString()).replace("${'$'}isPrimary", isPrimary) + return longPressContent.replace($$"$id", id.toString()).replace($$"$isPrimary", isPrimary) } fun getButtonOnStartup(primaryId: Long): String { val isPrimary = if (primaryId == id) "true" else "false" - return onStartup.replace("${'$'}id", id.toString()).replace("${'$'}isPrimary", isPrimary) + return onStartup.replace($$"$id", id.toString()).replace($$"$isPrimary", isPrimary) } } // <-- AY diff --git a/gradle/aniyomi.versions.toml b/gradle/aniyomi.versions.toml index d4cffedd98..73d7a7e646 100644 --- a/gradle/aniyomi.versions.toml +++ b/gradle/aniyomi.versions.toml @@ -1,5 +1,5 @@ [versions] -aniyomi-mpv-lib = "1.18.n" +mpv-lib = "0.1.12" arthenica-smartexceptions = "0.2.1" constraint-layout = "1.1.0" ffmpeg-kit = "1.18" @@ -10,7 +10,7 @@ google-api-drive = "v3-rev197-1.25.0" google-api-oauth = "1.34.1" [libraries] -aniyomi-mpv = { module = "com.github.aniyomiorg:aniyomi-mpv-lib", version.ref = "aniyomi-mpv-lib" } +mpv-lib = { module = "io.github.secozzi:mpv-android-lib", version.ref = "mpv-lib" } arthenica-smartexceptions = { module = "com.arthenica:smart-exception-java", version.ref = "arthenica-smartexceptions" } compose-constraintlayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraint-layout" } ffmpeg-kit = { module = "com.github.jmir1:ffmpeg-kit", version.ref = "ffmpeg-kit" } diff --git a/i18n-animiru/src/commonMain/moko-resources/base/strings.xml b/i18n-animiru/src/commonMain/moko-resources/base/strings.xml index 06732059e5..69da977f28 100644 --- a/i18n-animiru/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-animiru/src/commonMain/moko-resources/base/strings.xml @@ -200,4 +200,18 @@ Sync on App Start Sync on App Resume Sync library + + + Switch video on loading failure + + + Video settings + Deband + None + CPU + GPU + Iterations + Threshold + Range + Grain diff --git a/i18n-aniyomi/src/commonMain/moko-resources/base/strings.xml b/i18n-aniyomi/src/commonMain/moko-resources/base/strings.xml index 0ebd09d88b..45880b774b 100644 --- a/i18n-aniyomi/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-aniyomi/src/commonMain/moko-resources/base/strings.xml @@ -256,6 +256,8 @@ #%d: %s (%s) #%d: %s #%d: %s + Audio #%d + Subtitle #%d Delay Palette diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/animesource/model/Video.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/animesource/model/Video.kt index dc5185ff78..7e9956cac5 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/animesource/model/Video.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/animesource/model/Video.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.animesource.model import android.net.Uri +import androidx.compose.runtime.Stable import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import okhttp3.Headers @@ -25,6 +26,7 @@ data class TimeStamp( val type: ChapterType = ChapterType.Other, ) +@Stable // mutability only concerns the downloader data class Video( var videoUrl: String = "", val videoTitle: String = "",