diff --git a/app/src/main/java/live/mehiz/mpvkt/database/Migrations.kt b/app/src/main/java/live/mehiz/mpvkt/database/Migrations.kt new file mode 100644 index 0000000..d94ce4b --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/database/Migrations.kt @@ -0,0 +1,17 @@ +package live.mehiz.mpvkt.database + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val Migrations: Array = arrayOf( + MIGRATION1to2 +) + +private object MIGRATION1to2 : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + listOf("subDelay", "secondarySubDelay", "audioDelay").forEach { + db.execSQL("ALTER TABLE PlaybackStateEntity ADD COLUMN $it INTEGER NOT NULL DEFAULT 0") + } + db.execSQL("ALTER TABLE PlaybackStateEntity ADD COLUMN subSpeed REAL NOT NULL DEFAULT 0") + } +} diff --git a/app/src/main/java/live/mehiz/mpvkt/database/MpvKtDatabase.kt b/app/src/main/java/live/mehiz/mpvkt/database/MpvKtDatabase.kt index 0a02eb6..151ff64 100644 --- a/app/src/main/java/live/mehiz/mpvkt/database/MpvKtDatabase.kt +++ b/app/src/main/java/live/mehiz/mpvkt/database/MpvKtDatabase.kt @@ -5,7 +5,7 @@ import androidx.room.RoomDatabase import live.mehiz.mpvkt.database.dao.PlaybackStateDao import live.mehiz.mpvkt.database.entities.PlaybackStateEntity -@Database(entities = [PlaybackStateEntity::class], version = 1) +@Database(entities = [PlaybackStateEntity::class], version = 2) abstract class MpvKtDatabase : RoomDatabase() { abstract fun videoDataDao(): PlaybackStateDao } diff --git a/app/src/main/java/live/mehiz/mpvkt/database/entities/PlaybackStateEntity.kt b/app/src/main/java/live/mehiz/mpvkt/database/entities/PlaybackStateEntity.kt index 2917fc4..4d5e847 100644 --- a/app/src/main/java/live/mehiz/mpvkt/database/entities/PlaybackStateEntity.kt +++ b/app/src/main/java/live/mehiz/mpvkt/database/entities/PlaybackStateEntity.kt @@ -8,6 +8,10 @@ data class PlaybackStateEntity( @PrimaryKey val mediaTitle: String, val lastPosition: Int, // in seconds val sid: Int, + val subDelay: Int, + val subSpeed: Double, val secondarySid: Int, + val secondarySubDelay: Int, val aid: Int, + val audioDelay: Int, ) diff --git a/app/src/main/java/live/mehiz/mpvkt/di/DatabaseModule.kt b/app/src/main/java/live/mehiz/mpvkt/di/DatabaseModule.kt index ed8f2a2..e0c6d20 100644 --- a/app/src/main/java/live/mehiz/mpvkt/di/DatabaseModule.kt +++ b/app/src/main/java/live/mehiz/mpvkt/di/DatabaseModule.kt @@ -1,6 +1,7 @@ package live.mehiz.mpvkt.di import androidx.room.Room +import live.mehiz.mpvkt.database.Migrations import live.mehiz.mpvkt.database.MpvKtDatabase import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -9,6 +10,7 @@ val DatabaseModule = module { single { Room .databaseBuilder(androidContext(), MpvKtDatabase::class.java, "mpvKt.db") + .addMigrations(migrations = Migrations) .build() } } diff --git a/app/src/main/java/live/mehiz/mpvkt/preferences/AudioPreferences.kt b/app/src/main/java/live/mehiz/mpvkt/preferences/AudioPreferences.kt index 979f6f8..c09c70b 100644 --- a/app/src/main/java/live/mehiz/mpvkt/preferences/AudioPreferences.kt +++ b/app/src/main/java/live/mehiz/mpvkt/preferences/AudioPreferences.kt @@ -4,4 +4,6 @@ import live.mehiz.mpvkt.preferences.preference.PreferenceStore class AudioPreferences(preferenceStore: PreferenceStore) { val preferredLanguages = preferenceStore.getString("audio_preferred_languages") + val defaultAudioDelay = preferenceStore.getInt("audio_delay_default") + val audioPitchCorrection = preferenceStore.getBoolean("audio_pitch_correction", true) } diff --git a/app/src/main/java/live/mehiz/mpvkt/preferences/SubtitlesPreferences.kt b/app/src/main/java/live/mehiz/mpvkt/preferences/SubtitlesPreferences.kt index 7b8a36e..306a4cb 100644 --- a/app/src/main/java/live/mehiz/mpvkt/preferences/SubtitlesPreferences.kt +++ b/app/src/main/java/live/mehiz/mpvkt/preferences/SubtitlesPreferences.kt @@ -30,6 +30,10 @@ class SubtitlesPreferences(preferenceStore: PreferenceStore) { val justification = preferenceStore.getEnum("sub_justify", SubtitleJustification.Auto) val overrideAssSubs = preferenceStore.getBoolean("sub_override_ass") + + val defaultSubDelay = preferenceStore.getInt("sub_default_delay") + val defaultSubSpeed = preferenceStore.getFloat("sub_default_speed") + val defaultSecondarySubDelay = preferenceStore.getInt("sub_default_secondary_delay") } enum class SubtitleJustification( diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/ExpandableCard.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/ExpandableCard.kt index f273632..4247671 100644 --- a/app/src/main/java/live/mehiz/mpvkt/presentation/ExpandableCard.kt +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/ExpandableCard.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding @@ -65,8 +66,8 @@ fun ExpandableCard( Icon(Icons.Default.ArrowDropDown, null) } } - if (isExpanded) { - content() + Box(Modifier.animateContentSize()) { + if (isExpanded) content() } } } diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/OutlinedNumericChooser.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/OutlinedNumericChooser.kt new file mode 100644 index 0000000..2ba4187 --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/OutlinedNumericChooser.kt @@ -0,0 +1,132 @@ +package live.mehiz.mpvkt.presentation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material.icons.filled.RemoveCircle +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import live.mehiz.mpvkt.R + +@Composable +fun OutlinedNumericChooser( + value: Int, + onChange: (Int) -> Unit, + max: Int, + step: Int, + modifier: Modifier = Modifier, + min: Int = 0, + suffix: (@Composable () -> Unit)? = null, + label: (@Composable () -> Unit)? = null, +) { + assert(max > min) { "min can't be larger than max ($min > $max)" } + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RepeatingIconButton(onClick = { onChange(value - step) }) { + Icon(Icons.Filled.RemoveCircle, null) + } + var valueString by remember { mutableStateOf("$value") } + LaunchedEffect(value) { + if (valueString.isBlank()) return@LaunchedEffect + valueString = value.toString() + } + OutlinedTextField( + label = label, + value = valueString, + onValueChange = { newValue -> + if (newValue.isBlank()) { + valueString = newValue + onChange(0) + } + runCatching { + val intValue = newValue.toInt() + onChange(intValue) + valueString = newValue + } + }, + isError = value > max || value < min, + supportingText = { + if (value > max) Text(stringResource(R.string.numeric_chooser_value_too_big)) + if (value < min) Text(stringResource(R.string.numeric_chooser_value_too_small)) + }, + suffix = suffix, + modifier = Modifier.weight(1f), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + RepeatingIconButton(onClick = { onChange(value + step) }) { + Icon(Icons.Filled.AddCircle, null) + } + } +} + +@Composable +fun OutlinedNumericChooser( + value: Float, + onChange: (Float) -> Unit, + max: Float, + step: Float, + modifier: Modifier = Modifier, + min: Float = 0f, + suffix: (@Composable () -> Unit)? = null, + label: (@Composable () -> Unit)? = null, +) { + assert(max > min) { "min can't be larger than max ($min > $max)" } + Row( + modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RepeatingIconButton(onClick = { onChange(value - step) }) { + Icon(Icons.Filled.RemoveCircle, null) + } + var valueString by remember { mutableStateOf("$value") } + LaunchedEffect(value) { + if (valueString.isBlank()) return@LaunchedEffect + valueString = value.toString().dropLastWhile { it == '0' }.dropLastWhile { it == '.' } + } + OutlinedTextField( + value = valueString, + label = label, + onValueChange = { newValue -> + if (newValue.isBlank()) { + valueString = newValue + onChange(0f) + } + runCatching { + if (newValue.startsWith('.')) return@runCatching + val floatValue = newValue.toFloat() + onChange(floatValue) + valueString = newValue + } + }, + isError = value > max || value < min, + supportingText = { + if (value > max) Text(stringResource(R.string.numeric_chooser_value_too_big)) + if (value < min) Text(stringResource(R.string.numeric_chooser_value_too_small)) + }, + modifier = Modifier.weight(1f), + suffix = suffix, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + RepeatingIconButton(onClick = { onChange(value + step) }) { + Icon(Icons.Filled.AddCircle, null) + } + } +} diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/RepeatingIconButton.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/RepeatingIconButton.kt new file mode 100644 index 0000000..013c74d --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/RepeatingIconButton.kt @@ -0,0 +1,60 @@ +package live.mehiz.mpvkt.presentation + +import android.view.MotionEvent +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInteropFilter +import kotlinx.coroutines.delay + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun RepeatingIconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + maxDelayMillis: Long = 750, + minDelayMillis: Long = 5, + delayDecayFactor: Float = .25f, + content: @Composable () -> Unit, +) { + val currentClickListener by rememberUpdatedState(onClick) + var pressed by remember { mutableStateOf(false) } + + IconButton( + modifier = modifier.pointerInteropFilter { + pressed = when (it.action) { + MotionEvent.ACTION_DOWN -> true + + else -> false + } + + true + }, + onClick = {}, + enabled = enabled, + interactionSource = interactionSource, + content = content, + ) + + LaunchedEffect(pressed, enabled) { + var currentDelayMillis = maxDelayMillis + + while (enabled && pressed) { + currentClickListener() + delay(currentDelayMillis) + currentDelayMillis = + (currentDelayMillis - (currentDelayMillis * delayDecayFactor)) + .toLong().coerceAtLeast(minDelayMillis) + } + } +} diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/SliderItem.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/SliderItem.kt index f75c850..3bb7f6d 100644 --- a/app/src/main/java/live/mehiz/mpvkt/presentation/SliderItem.kt +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/SliderItem.kt @@ -71,6 +71,55 @@ fun SliderItem( } } +@Composable +fun SliderItem( + label: String, + value: Float, + valueText: String, + onChange: (Float) -> Unit, + max: Float, + steps: Int, + modifier: Modifier = Modifier, + min: Float = 0f, + icon: @Composable () -> Unit = {}, +) { + val haptic = LocalHapticFeedback.current + + Row( + modifier = modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + icon() + Column(modifier = Modifier.weight(0.5f)) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + ) + Text(valueText) + } + + Slider( + value = value, + onValueChange = { + val newValue = it + if (newValue != value) { + onChange(newValue) + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + }, + modifier = Modifier.weight(1.5f), + valueRange = min..max, + steps = steps, + ) + } +} + @Composable fun VerticalSliderItem( label: String, diff --git a/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt b/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt index 12b600e..ed09e17 100644 --- a/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt +++ b/app/src/main/java/live/mehiz/mpvkt/presentation/crash/CrashActivity.kt @@ -150,8 +150,8 @@ class CrashActivity : ComponentActivity() { } OutlinedButton( onClick = { + finish() startActivity(Intent(this@CrashActivity, MainActivity::class.java)) - finishAndRemoveTask() }, modifier = Modifier.fillMaxWidth(), ) { diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt index 890cff5..6c8fd26 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt @@ -203,6 +203,8 @@ class PlayerActivity : AppCompatActivity() { private fun setupAudio() { MPVLib.setPropertyString("alang", audioPreferences.preferredLanguages.get()) + MPVLib.setPropertyDouble("audio-delay", audioPreferences.defaultAudioDelay.get() / 1000.0) + MPVLib.setPropertyBoolean("audio-pitch-correction", audioPreferences.audioPitchCorrection.get()) val request = AudioFocusRequestCompat .Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) @@ -236,6 +238,10 @@ class PlayerActivity : AppCompatActivity() { MPVLib.setPropertyString("sub-back-color", subtitlesPreferences.backgroundColor.get().toColorHexString()) MPVLib.setPropertyInt("sub-pos", subtitlesPreferences.position.get()) + + MPVLib.setPropertyDouble("sub-delay", subtitlesPreferences.defaultSubDelay.get() / 1000.0) + MPVLib.setPropertyDouble("sub-speed", subtitlesPreferences.defaultSubSpeed.get().toDouble()) + MPVLib.setPropertyDouble("secondary-sub-delay", subtitlesPreferences.defaultSecondarySubDelay.get() / 1000.0) } private fun copyMPVConfigFiles() { @@ -443,21 +449,35 @@ class PlayerActivity : AppCompatActivity() { PlaybackStateEntity( fileName, if (playerPreferences.savePositionOnQuit.get()) player.timePos ?: 0 else 0, - player.sid, - player.secondarySid, - player.aid, + sid = player.sid, + subDelay = (MPVLib.getPropertyDouble("sub-delay") * 1000).toInt(), + subSpeed = MPVLib.getPropertyDouble("sub-speed"), + secondarySid = player.secondarySid, + secondarySubDelay = (MPVLib.getPropertyDouble("sub-delay") * 1000).toInt(), + aid = player.aid, + audioDelay = (MPVLib.getPropertyDouble("audio-delay") * 1000).toInt(), ), ) } private suspend fun loadVideoPlaybackState(mediaTitle: String) { val state = mpvKtDatabase.videoDataDao().getVideoDataByTitle(mediaTitle) + val getDelay: (Int, Int?) -> Double = { preferenceDelay, stateDelay -> + (stateDelay ?: preferenceDelay) / 1000.0 + } + val subDelay = getDelay(subtitlesPreferences.defaultSubDelay.get(), state?.subDelay) + val secondarySubDelay = getDelay(subtitlesPreferences.defaultSecondarySubDelay.get(), state?.secondarySubDelay) + val audioDelay = getDelay(audioPreferences.defaultAudioDelay.get(), state?.audioDelay) state?.let { player.timePos = if (playerPreferences.savePositionOnQuit.get()) it.lastPosition else 0 player.sid = it.sid player.secondarySid = it.secondarySid player.aid = it.aid + player.subDelay = subDelay + player.secondarySubDelay = secondarySubDelay + MPVLib.setPropertyDouble("audio-delay", audioDelay) } + MPVLib.setPropertyDouble("sub-speed", state?.subSpeed ?: subtitlesPreferences.defaultSubSpeed.get().toDouble()) } private fun endPlayback(reason: EndPlaybackReason) { diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerEnums.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerEnums.kt index c346e32..eb5487c 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerEnums.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerEnums.kt @@ -40,9 +40,11 @@ enum class Debanding { enum class Sheets { None, - SubtitlesSheet, + SubtitleTracks, SubtitleSettings, - AudioSheet, + SubtitleDelay, + AudioTracks, + AudioDelay, Chapters, Decoders, More, diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerSheets.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerSheets.kt index 3dbad21..2c9bf38 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerSheets.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerSheets.kt @@ -10,10 +10,12 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.update import live.mehiz.mpvkt.ui.player.PlayerViewModel import live.mehiz.mpvkt.ui.player.Sheets +import live.mehiz.mpvkt.ui.player.controls.components.sheets.AudioDelaySheet import live.mehiz.mpvkt.ui.player.controls.components.sheets.AudioTracksSheet import live.mehiz.mpvkt.ui.player.controls.components.sheets.ChaptersSheet import live.mehiz.mpvkt.ui.player.controls.components.sheets.DecodersSheet import live.mehiz.mpvkt.ui.player.controls.components.sheets.MoreSheet +import live.mehiz.mpvkt.ui.player.controls.components.sheets.subtitles.SubtitleDelaySheet import live.mehiz.mpvkt.ui.player.controls.components.sheets.subtitles.SubtitleSettingsSheet import live.mehiz.mpvkt.ui.player.controls.components.sheets.subtitles.SubtitlesSheet import org.koin.compose.koinInject @@ -30,7 +32,7 @@ fun PlayerSheets(modifier: Modifier = Modifier) { when (sheetShown) { Sheets.None -> {} - Sheets.SubtitlesSheet -> { + Sheets.SubtitleTracks -> { val subtitlesPicker = rememberLauncherForActivityResult( ActivityResultContracts.OpenDocument(), ) { @@ -43,11 +45,12 @@ fun PlayerSheets(modifier: Modifier = Modifier) { onSelect = { viewModel.selectSub(it) }, onAddSubtitle = { subtitlesPicker.launch(arrayOf("*/*")) }, onOpenSubtitleSettings = { viewModel.sheetShown.update { Sheets.SubtitleSettings } }, + onOpenSubtitleDelay = { viewModel.sheetShown.update { Sheets.SubtitleDelay } }, onDismissRequest = onDismissRequest ) } - Sheets.AudioSheet -> { + Sheets.AudioTracks -> { val audioPicker = rememberLauncherForActivityResult( ActivityResultContracts.OpenDocument(), ) { @@ -59,6 +62,7 @@ fun PlayerSheets(modifier: Modifier = Modifier) { selectedAudio, { viewModel.selectAudio(it) }, { audioPicker.launch(arrayOf("*/*")) }, + onOpenDelaySheet = { viewModel.sheetShown.update { Sheets.AudioDelay } }, onDismissRequest ) } @@ -96,5 +100,15 @@ fun PlayerSheets(modifier: Modifier = Modifier) { modifier = Modifier.then(modifier) ) } + + Sheets.SubtitleDelay -> { + viewModel.hideControls() + SubtitleDelaySheet() + } + + Sheets.AudioDelay -> { + viewModel.hideControls() + AudioDelaySheet() + } } } diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/TopRightPlayerControls.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/TopRightPlayerControls.kt index 34fe9c8..1b1fe06 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/TopRightPlayerControls.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/TopRightPlayerControls.kt @@ -36,11 +36,11 @@ fun TopRightPlayerControls(modifier: Modifier = Modifier) { } ControlsButton( Icons.Default.Subtitles, - onClick = { viewModel.sheetShown.update { Sheets.SubtitlesSheet } }, + onClick = { viewModel.sheetShown.update { Sheets.SubtitleTracks } }, ) ControlsButton( Icons.Default.Audiotrack, - onClick = { viewModel.sheetShown.update { Sheets.AudioSheet } }, + onClick = { viewModel.sheetShown.update { Sheets.AudioTracks } }, ) ControlsButton( Icons.Default.MoreVert, diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/Seekbar.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/Seekbar.kt index 0ab1564..78d488b 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/Seekbar.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/Seekbar.kt @@ -15,6 +15,7 @@ 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.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -49,6 +50,7 @@ fun SeekbarWithTimers( value = position, timersInverted.first, onClick = positionTimerOnClick, + modifier = Modifier.weight(.1f) ) Seeker( value = position, @@ -57,7 +59,7 @@ fun SeekbarWithTimers( onValueChangeFinished = onValueChangeFinished, readAheadValue = readAheadValue, segments = chapters?.map { it.toSegment() } ?: emptyList(), - modifier = Modifier.weight(0.9f), + modifier = Modifier.weight(0.8f), colors = SeekerDefaults.seekerColors( progressColor = MaterialTheme.colorScheme.primary, thumbColor = MaterialTheme.colorScheme.primary, @@ -69,6 +71,7 @@ fun SeekbarWithTimers( value = if (timersInverted.second) position - duration else duration, isInverted = timersInverted.second, onClick = durationTimerOnCLick, + modifier = Modifier.weight(.1f) ) } } @@ -92,6 +95,7 @@ fun VideoTimer( .wrapContentHeight(Alignment.CenterVertically), text = Utils.prettyTime(value.toInt(), isInverted), color = Color.White, + fontFamily = FontFamily.Monospace, textAlign = TextAlign.Center, ) } diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioDelaySheet.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioDelaySheet.kt new file mode 100644 index 0000000..2ddfd57 --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioDelaySheet.kt @@ -0,0 +1,93 @@ +package live.mehiz.mpvkt.ui.player.controls.components.sheets + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +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.size +import androidx.compose.material.icons.Icons +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.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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import `is`.xyz.mpv.MPVLib +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.update +import live.mehiz.mpvkt.R +import live.mehiz.mpvkt.preferences.AudioPreferences +import live.mehiz.mpvkt.ui.player.PlayerViewModel +import live.mehiz.mpvkt.ui.player.Sheets +import live.mehiz.mpvkt.ui.player.controls.components.sheets.subtitles.DelayCard +import org.koin.compose.koinInject + +@Composable +fun AudioDelaySheet( + modifier: Modifier = Modifier, +) { + val preferences = koinInject() + val viewModel = koinInject() + + BackHandler { viewModel.sheetShown.update { Sheets.None } } + + ConstraintLayout( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + ) { + 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.defaultAudioDelay.set(delay) }, + onReset = { delay = 0 }, + title = { AudioDelayCardTitle(onClose = { viewModel.sheetShown.update { Sheets.None } }) }, + modifier = Modifier.constrainAs(delayControlCard) { + linkTo(parent.top, parent.bottom, bias = 0.8f) + end.linkTo(parent.end) + }, + ) + } +} + +@Composable +fun AudioDelayCardTitle( + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(R.string.player_sheets_audio_delay_card_title), + style = MaterialTheme.typography.headlineMedium + ) + IconButton(onClose) { + Icon( + Icons.Default.Close, + null, + modifier = Modifier.size(32.dp), + ) + } + } +} diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioTracksSheet.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioTracksSheet.kt index b8be1cf..e8475ba 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioTracksSheet.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/AudioTracksSheet.kt @@ -5,6 +5,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreTime +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,13 +28,24 @@ fun AudioTracksSheet( selectedId: Int, onSelect: (Int) -> Unit, onAddAudioTrack: () -> Unit, + onOpenDelaySheet: () -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier ) { GenericTracksSheet( tracks, onDismissRequest = onDismissRequest, - header = { AddTrackRow(stringResource(R.string.player_sheets_add_ext_audio), onAddAudioTrack) }, + header = { + AddTrackRow( + stringResource(R.string.player_sheets_add_ext_audio), + onAddAudioTrack, + actions = { + IconButton(onClick = onOpenDelaySheet) { + Icon(Icons.Default.MoreTime, null) + } + } + ) + }, track = { AudioTrackRow( title = getTrackTitle(it), diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/LongPressSheet.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/LongPressSheet.kt deleted file mode 100644 index 7e488cb..0000000 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/LongPressSheet.kt +++ /dev/null @@ -1 +0,0 @@ -package live.mehiz.mpvkt.ui.player.controls.components.sheets diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleDelaySheet.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleDelaySheet.kt new file mode 100644 index 0000000..1d611fb --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleDelaySheet.kt @@ -0,0 +1,261 @@ +package live.mehiz.mpvkt.ui.player.controls.components.sheets.subtitles + +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +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.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +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.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import `is`.xyz.mpv.MPVLib +import kotlinx.coroutines.flow.update +import live.mehiz.mpvkt.R +import live.mehiz.mpvkt.preferences.SubtitlesPreferences +import live.mehiz.mpvkt.presentation.OutlinedNumericChooser +import live.mehiz.mpvkt.ui.player.PlayerViewModel +import live.mehiz.mpvkt.ui.player.Sheets +import org.koin.compose.koinInject +import kotlin.math.round + +@Composable +fun SubtitleDelaySheet( + modifier: Modifier = Modifier +) { + val preferences = koinInject() + val viewModel = koinInject() + + BackHandler { viewModel.sheetShown.update { Sheets.None } } + ConstraintLayout( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + ) { + val delayControlCard = createRef() + + var affectedSubtitle by remember { mutableStateOf(SubtitleDelayType.Primary) } + var delay by remember { mutableIntStateOf((MPVLib.getPropertyDouble("sub-delay") * 1000).toInt()) } + var speed by remember { mutableFloatStateOf(MPVLib.getPropertyDouble("sub-speed").toFloat()) } + LaunchedEffect(speed) { + MPVLib.setPropertyDouble("sub-speed", speed.toDouble()) + } + LaunchedEffect(delay) { + val finalDelay = 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) + } + } + } + SubtitleDelayCard( + delay = delay, + onDelayChange = { delay = it }, + speed = speed, + onSpeedChange = { speed = round(it * 10) / 10f }, + affectedSubtitle = affectedSubtitle, + onTypeChange = { affectedSubtitle = it }, + onApply = { + preferences.defaultSubDelay.set(delay) + preferences.defaultSubSpeed.set(speed) + }, + onReset = { + delay = 0 + speed = 1f + }, + onClose = { viewModel.sheetShown.update { Sheets.None } }, + modifier = Modifier.constrainAs(delayControlCard) { + linkTo(parent.top, parent.bottom, bias = 0.8f) + end.linkTo(parent.end) + }, + ) + } +} + +@Composable +fun SubtitleDelayCard( + delay: Int, + onDelayChange: (Int) -> Unit, + speed: Float, + onSpeedChange: (Float) -> Unit, + affectedSubtitle: SubtitleDelayType, + onTypeChange: (SubtitleDelayType) -> Unit, + onApply: () -> Unit, + onReset: () -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + DelayCard( + delay = delay, + onDelayChange = onDelayChange, + onApply = onApply, + onReset = onReset, + title = { + SubtitleDelayTitle( + affectedSubtitle = affectedSubtitle, + onClose = onClose, + onTypeChange = onTypeChange, + ) + }, + extraSettings = { + when (affectedSubtitle) { + SubtitleDelayType.Primary -> { + OutlinedNumericChooser( + label = { Text(stringResource(R.string.player_sheets_sub_delay_card_speed)) }, + value = speed, + onChange = onSpeedChange, + max = 10f, + step = .1f, + min = .1f + ) + } + else -> {} + } + }, + modifier = modifier, + ) +} + +enum class SubtitleDelayType( + @StringRes val title: Int, +) { + Primary( + R.string.player_sheets_sub_delay_subtitle_type_primary + ), Secondary(R.string.player_sheets_sub_delay_subtitle_type_secondary), Both( + R.string.player_sheets_sub_delay_subtitle_type_primary_and_secondary, + ), +} + +@Composable +fun DelayCard( + delay: Int, + onDelayChange: (Int) -> Unit, + onApply: () -> Unit, + onReset: () -> Unit, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + extraSettings: @Composable ColumnScope.() -> Unit = {}, +) { + Card( + modifier = modifier + .widthIn(max = CARDS_MAX_WIDTH) + .animateContentSize(), + colors = SubtitleSettingsCardColors(), + ) { + Column( + Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + title() + OutlinedNumericChooser( + label = { Text(stringResource(R.string.player_sheets_sub_delay_card_delay)) }, + value = delay, + onChange = onDelayChange, + step = 50, + min = Int.MIN_VALUE, + max = Int.MAX_VALUE, + suffix = { Text(stringResource(R.string.generic_unit_ms)) } + ) + Column( + modifier = Modifier.animateContentSize() + ) { extraSettings() } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = onApply, + modifier = Modifier.weight(1f), + ) { + Text(stringResource(R.string.player_sheets_delay_set_as_default)) + } + FilledIconButton(onClick = onReset) { + Icon(Icons.Default.Refresh, null) + } + } + } + } +} + +@Composable +fun SubtitleDelayTitle( + affectedSubtitle: SubtitleDelayType, + onClose: () -> Unit, + onTypeChange: (SubtitleDelayType) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier.fillMaxWidth(), + ) { + Text( + "Subtitle Delay", + style = MaterialTheme.typography.headlineMedium, + ) + var showDropDownMenu by remember { mutableStateOf(false) } + Row(modifier = Modifier.clickable { showDropDownMenu = true }) { + Text( + stringResource(affectedSubtitle.title), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + Icon(Icons.Default.ArrowDropDown, null) + DropdownMenu( + expanded = showDropDownMenu, + onDismissRequest = { showDropDownMenu = false }, + ) { + SubtitleDelayType.entries.forEach { + DropdownMenuItem( + text = { Text(stringResource(it.title)) }, + onClick = { + onTypeChange(it) + showDropDownMenu = false + }, + ) + } + } + } + Spacer(Modifier.weight(1f)) + IconButton(onClose) { + Icon( + Icons.Default.Close, + null, + modifier = Modifier.size(32.dp), + ) + } + } +} diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleSettingsSheet.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleSettingsSheet.kt index cfa89cf..69a3785 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleSettingsSheet.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleSettingsSheet.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -55,11 +56,11 @@ fun SubtitleSettingsSheet( ConstraintLayout(modifier = modifier.fillMaxSize()) { val vposSlider = createRef() val subSettingsCards = createRef() - val cards: @Composable (Int) -> Unit = { - when (it) { - 0 -> SubtitleSettingsTypographyCard() - 1 -> SubtitleSettingsColorsCard() - 2 -> SubtitlesMiscellaneousCard() + val cards: @Composable (Int, Modifier) -> Unit = { value, cardsModifier -> + when (value) { + 0 -> SubtitleSettingsTypographyCard(cardsModifier) + 1 -> SubtitleSettingsColorsCard(cardsModifier) + 2 -> SubtitlesMiscellaneousCard(cardsModifier) else -> {} } } @@ -78,7 +79,7 @@ fun SubtitleSettingsSheet( start.linkTo(parent.start) }, ) { page -> - cards(page) + cards(page, Modifier.fillMaxWidth()) } } else { LazyColumn( @@ -90,7 +91,7 @@ fun SubtitleSettingsSheet( }, ) { item { Spacer(Modifier.height(16.dp)) } - items(3) { cards(it) } + items(3) { cards(it, Modifier) } item { Spacer(Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitlesSheet.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleTracksSheet.kt similarity index 91% rename from app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitlesSheet.kt rename to app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleTracksSheet.kt index 77b90d0..7f3bd39 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitlesSheet.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/sheets/subtitles/SubtitleTracksSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreTime import androidx.compose.material.icons.filled.Palette import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon @@ -34,8 +35,9 @@ fun SubtitlesSheet( onSelect: (Int) -> Unit, onAddSubtitle: () -> Unit, onOpenSubtitleSettings: () -> Unit, + onOpenSubtitleDelay: () -> Unit, onDismissRequest: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { GenericTracksSheet( tracks, @@ -45,12 +47,13 @@ fun SubtitlesSheet( stringResource(R.string.player_sheets_add_ext_sub), onAddSubtitle, actions = { - IconButton( - onClick = onOpenSubtitleSettings - ) { + IconButton(onClick = onOpenSubtitleSettings) { Icon(Icons.Default.Palette, null) } - } + IconButton(onClick = onOpenSubtitleDelay) { + Icon(Icons.Default.MoreTime, null) + } + }, ) }, track = { track -> diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/preferences/AudioPreferencesScreen.kt b/app/src/main/java/live/mehiz/mpvkt/ui/preferences/AudioPreferencesScreen.kt index 6e773ef..895b373 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/preferences/AudioPreferencesScreen.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/preferences/AudioPreferencesScreen.kt @@ -23,6 +23,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import live.mehiz.mpvkt.R import live.mehiz.mpvkt.preferences.AudioPreferences import me.zhanghai.compose.preference.ProvidePreferenceLocals +import me.zhanghai.compose.preference.switchPreference import me.zhanghai.compose.preference.textFieldPreference import org.koin.compose.koinInject @@ -69,6 +70,12 @@ object AudioPreferencesScreen : Screen { } }, ) + switchPreference( + key = preferences.audioPitchCorrection.key(), + defaultValue = preferences.audioPitchCorrection.defaultValue(), + title = { Text(stringResource(R.string.pref_audio_pitch_correction_title)) }, + summary = { Text(stringResource(R.string.pref_audio_pitch_correction_summary)) }, + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1dbb932..c49266a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Reset Share + ms Appearance Theme @@ -54,6 +55,8 @@ Fonts directory Audio + Enable audio pitch correction + Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds. Advanced pick mpv configuration storage location @@ -82,6 +85,19 @@ Border size Colors Miscellaneous + Subtitle delay + Delay + Speed + Affected subtitle + Primary + Secondary + Both + Set as default + Audio delay + + Value too big + Value too small + Default statistics page Page %d %d seconds