diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1092986c..7004e93e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 16 - versionName = "1.1-beta34" + versionName = "1.1-beta35" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt b/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt index ad0c76ac..ae1ff91e 100644 --- a/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt +++ b/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt @@ -1,9 +1,9 @@ package com.skyd.anivu.base import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -15,7 +15,7 @@ import com.skyd.anivu.ui.theme.AniVuTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -open class BaseComposeActivity : ComponentActivity() { +open class BaseComposeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/skyd/anivu/model/preference/player/MpvInputConfigPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/player/MpvInputConfigPreference.kt new file mode 100644 index 00000000..d8fbd7d3 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/player/MpvInputConfigPreference.kt @@ -0,0 +1,28 @@ +package com.skyd.anivu.model.preference.player + +import com.skyd.anivu.config.Const +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.File + +object MpvInputConfigPreference { + private var value: String? = null + + fun put(scope: CoroutineScope, value: String) { + this.value = value + scope.launch(Dispatchers.IO) { + File(Const.MPV_CONFIG_DIR, "input.conf") + .apply { if (!exists()) createNewFile() } + .writeText(value) + } + } + + fun getValue(): String = value ?: runBlocking(Dispatchers.IO) { + value = File(Const.MPV_CONFIG_DIR, "input.conf") + .apply { if (!exists()) createNewFile() } + .readText() + value.orEmpty() + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt index 1542c873..28fd7e66 100644 --- a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt +++ b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle +import android.view.KeyEvent import android.view.WindowManager import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.DisposableEffect @@ -16,6 +17,7 @@ import androidx.core.util.Consumer import com.skyd.anivu.base.BaseComposeActivity import com.skyd.anivu.ext.savePictureToMediaStore import com.skyd.anivu.ui.component.showToast +import com.skyd.anivu.ui.mpv.MPVView import com.skyd.anivu.ui.mpv.PlayerView import com.skyd.anivu.ui.mpv.copyAssetsForMpv import java.io.File @@ -26,7 +28,7 @@ class PlayActivity : BaseComposeActivity() { const val VIDEO_URI_KEY = "videoUri" } - + private var player: MPVView? = null private lateinit var picture: File private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() @@ -63,7 +65,8 @@ class PlayActivity : BaseComposeActivity() { onSaveScreenshot = { picture = it saveScreenshot() - } + }, + onPlayerChanged = { player = it } ) } } @@ -82,4 +85,11 @@ class PlayActivity : BaseComposeActivity() { requestPermissionLauncher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) } } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (player?.onKey(event) == true) { + return true + } + return super.dispatchKeyEvent(event) + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt b/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt index 71141bd7..9e2de1ab 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt @@ -32,6 +32,7 @@ fun TextFieldDialog( errorText: String = "", dismissText: String = stringResource(R.string.cancel), confirmText: String = stringResource(R.string.ok), + enableConfirm: (String) -> Boolean = { it.isNotBlank() }, onValueChange: (String) -> Unit = {}, onDismissRequest: () -> Unit = {}, onConfirm: (String) -> Unit = {}, @@ -66,7 +67,7 @@ fun TextFieldDialog( }, confirmButton = { TextButton( - enabled = value.isNotBlank(), + enabled = enableConfirm(value), onClick = { focusManager.clearFocus() onConfirm(value) @@ -74,7 +75,7 @@ fun TextFieldDialog( ) { Text( text = confirmText, - color = if (value.isNotBlank()) { + color = if (enableConfirm(value)) { Color.Unspecified } else { MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt index e89f3c02..705f9f72 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt @@ -132,7 +132,7 @@ fun FeedScreen() { } val windowWidth = with(density) { currentWindowSize().width.toDp() } - val feedListWidth by remember(windowWidth) { mutableStateOf(windowWidth * 0.31f) } + val feedListWidth by remember(windowWidth) { mutableStateOf(windowWidth * 0.335f) } ListDetailPaneScaffold( modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only( diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/playerconfig/advanced/PlayerConfigAdvancedFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/playerconfig/advanced/PlayerConfigAdvancedFragment.kt index fb8ac818..3a156ea7 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/playerconfig/advanced/PlayerConfigAdvancedFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/playerconfig/advanced/PlayerConfigAdvancedFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Keyboard import androidx.compose.material.icons.outlined.PlayCircle import androidx.compose.material.icons.rounded.DeveloperBoard import androidx.compose.material3.Scaffold @@ -27,6 +28,7 @@ import com.skyd.anivu.R import com.skyd.anivu.base.BaseComposeFragment import com.skyd.anivu.model.preference.player.HardwareDecodePreference import com.skyd.anivu.model.preference.player.MpvConfigPreference +import com.skyd.anivu.model.preference.player.MpvInputConfigPreference import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle import com.skyd.anivu.ui.component.BaseSettingsItem @@ -52,6 +54,8 @@ fun PlayerConfigAdvancedScreen() { val scope = rememberCoroutineScope() var mpvConfEditDialogValue by rememberSaveable { mutableStateOf("") } var openMpvConfEditDialog by rememberSaveable { mutableStateOf(false) } + var mpvInputConfEditDialogValue by rememberSaveable { mutableStateOf("") } + var openMpvInputConfEditDialog by rememberSaveable { mutableStateOf(false) } Scaffold( topBar = { @@ -94,6 +98,17 @@ fun PlayerConfigAdvancedScreen() { } ) } + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.Keyboard), + text = stringResource(id = R.string.player_config_advanced_screen_mpv_input_config), + descriptionText = null, + onClick = { + mpvInputConfEditDialogValue = MpvInputConfigPreference.getValue() + openMpvInputConfEditDialog = true + } + ) + } } TextFieldDialog( @@ -107,7 +122,23 @@ fun PlayerConfigAdvancedScreen() { ) openMpvConfEditDialog = false }, + enableConfirm = { true }, onDismissRequest = { openMpvConfEditDialog = false }, ) + + TextFieldDialog( + visible = openMpvInputConfEditDialog, + value = mpvInputConfEditDialogValue, + onValueChange = { mpvInputConfEditDialogValue = it }, + onConfirm = { + MpvInputConfigPreference.put( + scope = scope, + value = it, + ) + openMpvInputConfEditDialog = false + }, + enableConfirm = { true }, + onDismissRequest = { openMpvInputConfEditDialog = false }, + ) } } diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/KeyMapping.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/KeyMapping.kt new file mode 100644 index 00000000..909d7d31 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/KeyMapping.kt @@ -0,0 +1,70 @@ +package com.skyd.anivu.ui.mpv + +import android.util.SparseArray +import android.view.KeyEvent + +internal object KeyMapping { + val map: SparseArray = SparseArray() + + init { + // cf. https://github.com/mpv-player/mpv/blob/master/input/keycodes.h + map.put(KeyEvent.KEYCODE_SPACE, "SPACE") + map.put(KeyEvent.KEYCODE_ENTER, "ENTER") + map.put(KeyEvent.KEYCODE_TAB, "TAB") + map.put(KeyEvent.KEYCODE_DEL, "BS") + map.put(KeyEvent.KEYCODE_FORWARD_DEL, "DEL") + map.put(KeyEvent.KEYCODE_INSERT, "INS") + map.put(KeyEvent.KEYCODE_MOVE_HOME, "HOME") + map.put(KeyEvent.KEYCODE_MOVE_END, "END") + map.put(KeyEvent.KEYCODE_PAGE_UP, "PGUP") + map.put(KeyEvent.KEYCODE_PAGE_DOWN, "PGDWN") + map.put(KeyEvent.KEYCODE_ESCAPE, "ESC") + map.put(KeyEvent.KEYCODE_SYSRQ, "PRINT") + + map.put(KeyEvent.KEYCODE_DPAD_RIGHT, "RIGHT") + map.put(KeyEvent.KEYCODE_DPAD_LEFT, "LEFT") + map.put(KeyEvent.KEYCODE_DPAD_DOWN, "DOWN") + map.put(KeyEvent.KEYCODE_DPAD_UP, "UP") + + // not bound, let the OS handle these: + map.put(KeyEvent.KEYCODE_MEDIA_PLAY, "PLAYONLY") + map.put(KeyEvent.KEYCODE_MEDIA_PAUSE, "PAUSEONLY") + map.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, "PLAYPAUSE") + map.put(KeyEvent.KEYCODE_MEDIA_STOP, "STOP") + map.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, "FORWARD") + map.put(KeyEvent.KEYCODE_MEDIA_REWIND, "REWIND") + map.put(KeyEvent.KEYCODE_MEDIA_NEXT, "NEXT") + map.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS, "PREV") + map.put(KeyEvent.KEYCODE_MEDIA_RECORD, "RECORD") + map.put(KeyEvent.KEYCODE_CHANNEL_UP, "CHANNEL_UP") + map.put(KeyEvent.KEYCODE_CHANNEL_DOWN, "CHANNEL_DOWN") + map.put(KeyEvent.KEYCODE_ZOOM_IN, "ZOOMIN") + map.put(KeyEvent.KEYCODE_ZOOM_OUT, "ZOOMOUT") + + map.put(KeyEvent.KEYCODE_F1, "F1") + map.put(KeyEvent.KEYCODE_F2, "F2") + map.put(KeyEvent.KEYCODE_F3, "F3") + map.put(KeyEvent.KEYCODE_F4, "F4") + map.put(KeyEvent.KEYCODE_F5, "F5") + map.put(KeyEvent.KEYCODE_F6, "F6") + map.put(KeyEvent.KEYCODE_F7, "F7") + map.put(KeyEvent.KEYCODE_F8, "F8") + map.put(KeyEvent.KEYCODE_F9, "F9") + map.put(KeyEvent.KEYCODE_F10, "F10") + map.put(KeyEvent.KEYCODE_F11, "F11") + map.put(KeyEvent.KEYCODE_F12, "F12") + + map.put(KeyEvent.KEYCODE_NUMPAD_0, "KP0") + map.put(KeyEvent.KEYCODE_NUMPAD_1, "KP1") + map.put(KeyEvent.KEYCODE_NUMPAD_2, "KP2") + map.put(KeyEvent.KEYCODE_NUMPAD_3, "KP3") + map.put(KeyEvent.KEYCODE_NUMPAD_4, "KP4") + map.put(KeyEvent.KEYCODE_NUMPAD_5, "KP5") + map.put(KeyEvent.KEYCODE_NUMPAD_6, "KP6") + map.put(KeyEvent.KEYCODE_NUMPAD_7, "KP7") + map.put(KeyEvent.KEYCODE_NUMPAD_8, "KP8") + map.put(KeyEvent.KEYCODE_NUMPAD_9, "KP9") + map.put(KeyEvent.KEYCODE_NUMPAD_DOT, "KP_DEC") + map.put(KeyEvent.KEYCODE_NUMPAD_ENTER, "KP_ENTER") + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt index 42beaaf1..f3281422 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt @@ -4,6 +4,8 @@ import android.content.Context import android.os.Build import android.util.AttributeSet import android.util.Log +import android.view.KeyCharacterMap +import android.view.KeyEvent import android.view.SurfaceHolder import android.view.SurfaceView import android.view.WindowManager @@ -47,7 +49,7 @@ class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, att vo: String = "gpu", ) { if (initialized) return - synchronized(MPVView::class) { + synchronized(this) { if (initialized) return initialized = true } @@ -118,6 +120,45 @@ class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, att MPVLib.destroy() } + fun onKey(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_MULTIPLE) + return false + if (KeyEvent.isModifierKey(event.keyCode)) + return false + + var mapped = KeyMapping.map.get(event.keyCode) + if (mapped == null) { + // Fallback to produced glyph + if (!event.isPrintingKey) { + if (event.repeatCount == 0) { + Log.d(TAG, "Unmapped non-printable key ${event.keyCode}") + } + return false + } + + val ch = event.unicodeChar + if (ch.and(KeyCharacterMap.COMBINING_ACCENT) != 0) { + return false // dead key + } + mapped = ch.toChar().toString() + } + + if (event.repeatCount > 0) + return true // eat event but ignore it, mpv has its own key repeat + + val mod: MutableList = mutableListOf() + event.isShiftPressed && mod.add("shift") + event.isCtrlPressed && mod.add("ctrl") + event.isAltPressed && mod.add("alt") + event.isMetaPressed && mod.add("meta") + + val action = if (event.action == KeyEvent.ACTION_DOWN) "keydown" else "keyup" + mod.add(mapped) + MPVLib.command(arrayOf(action, mod.joinToString("+"))) + + return true + } + private fun observeProperties() { // This observes all properties needed by MPVView, MPVActivity or other classes data class Property(val name: String, val format: Int = MPV_FORMAT_NONE) diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt index 443c2057..9ede1a55 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt @@ -28,24 +28,24 @@ internal fun Uri.resolveUri(context: Context): String? { return filepath } -private fun Uri.openContentFd(context: Context): String? { - val resolver = context.contentResolver - val fd = try { - resolver.openFileDescriptor(this, "r")!!.detachFd() - } catch (e: Exception) { - Log.e("openContentFd", "Failed to open content fd: $e") - return null - } - // See if we skip the indirection and read the real file directly - val path = findRealPath(fd) - if (path != null) { - Log.v("openContentFd", "Found real file path: $path") - ParcelFileDescriptor.adoptFd(fd).close() // we don't need that anymore - return path +private fun Uri.openContentFd(context: Context): String? = + context.contentResolver.openFileDescriptor(this, "r")!!.use { fileDescriptor -> + val fd = try { + fileDescriptor.detachFd() + } catch (e: Exception) { + Log.e("openContentFd", "Failed to open content fd: $e") + return@use null + } + // See if we skip the indirection and read the real file directly + val path = findRealPath(fd) + if (path != null) { + Log.v("openContentFd", "Found real file path: $path") + ParcelFileDescriptor.adoptFd(fd).close() // we don't need that anymore + return@use path + } + // Else, pass the fd to mpv + return@use "fd://${fd}" } - // Else, pass the fd to mpv - return "fd://${fd}" -} fun findRealPath(fd: Int): String? { var ins: InputStream? = null diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt index 78e32cb1..b207fb9c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.doOnAttach +import androidx.core.view.doOnDetach import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner @@ -120,6 +122,7 @@ fun PlayerView( configDir: String = Const.MPV_CONFIG_DIR.path, cacheDir: String = Const.MPV_CACHE_DIR.path, fontDir: String = Const.MPV_FONT_DIR.path, + onPlayerChanged: (MPVView?) -> Unit, ) { rememberSystemUiController().apply { isSystemBarsVisible = false @@ -297,6 +300,8 @@ fun PlayerView( } .collect() } + doOnAttach { onPlayerChanged(this) } + doOnDetach { onPlayerChanged(null) } } }, ) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a8ba1054..88043ac7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -203,6 +203,7 @@ 高级 高级播放器设置 编辑 mpv.conf + 编辑 input.conf 字幕轨道 已选 截图 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf69f7ea..682170d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,6 +211,7 @@ Advanced Advanced config Edit mpv.conf + Edit input.conf Subtitle track Selected Screenshot