update tv ux, fix configuration-cache issues
This commit is contained in:
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = namespace
|
applicationId = namespace
|
||||||
minSdk = libs.versions.proj.min.get().toInt()
|
minSdk = libs.versions.proj.min.get().toInt()
|
||||||
targetSdk = libs.versions.proj.target.get().toInt()
|
targetSdk = libs.versions.proj.target.get().toInt()
|
||||||
versionCode = 39
|
versionCode = 40
|
||||||
versionName = "1.21"
|
versionName = "1.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ class SyncClientServerRecentlyWatchedRepository(
|
|||||||
private fun createSync(): RemoteSourceSynchronizer<RecentlyWatchedInfo, String> {
|
private fun createSync(): RemoteSourceSynchronizer<RecentlyWatchedInfo, String> {
|
||||||
return RemoteSourceSynchronizer(
|
return RemoteSourceSynchronizer(
|
||||||
isSync = { syncRepository.isSynchronized(AggregateType.RECENTLY_WATCHED) },
|
isSync = { syncRepository.isSynchronized(AggregateType.RECENTLY_WATCHED) },
|
||||||
setSync = { syncRepository.set(AggregateType.RECENTLY_WATCHED, true) },
|
setSync = { syncRepository.set(AggregateType.RECENTLY_WATCHED, it) },
|
||||||
authTokenCache = authTokenCache,
|
authTokenCache = authTokenCache,
|
||||||
queryRemote = { remote.getAllRecentlyWatched() },
|
queryRemote = { remote.getAllRecentlyWatched() },
|
||||||
queryLocal = { local.getAllRecentlyWatched() },
|
queryLocal = { local.getAllRecentlyWatched() },
|
||||||
|
|||||||
+2
-4
@@ -8,7 +8,6 @@ import kotlinx.serialization.json.encodeToJsonElement
|
|||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Named
|
import org.koin.core.annotation.Named
|
||||||
import ru.shadowsparky.domain.DispatcherProvider
|
|
||||||
import ru.shadowsparky.http.domain.TokenStorage
|
import ru.shadowsparky.http.domain.TokenStorage
|
||||||
import ru.shadowsparky.http.domain.hasAccount
|
import ru.shadowsparky.http.domain.hasAccount
|
||||||
import ru.shadowsparky.vbox.shared.data.RemoteSourceSynchronizer
|
import ru.shadowsparky.vbox.shared.data.RemoteSourceSynchronizer
|
||||||
@@ -30,8 +29,7 @@ class SyncClientSavedMovieRepository(
|
|||||||
private val syncRepository: SyncRepository,
|
private val syncRepository: SyncRepository,
|
||||||
private val authTokenCache: TokenStorage,
|
private val authTokenCache: TokenStorage,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
private val pendingOperationSynchronizer: PendingOperationSynchronizer,
|
private val pendingOperationSynchronizer: PendingOperationSynchronizer
|
||||||
private val dispatcherProvider: DispatcherProvider
|
|
||||||
) : SavedMovieRepository {
|
) : SavedMovieRepository {
|
||||||
|
|
||||||
override suspend fun save(details: VideoDetails) {
|
override suspend fun save(details: VideoDetails) {
|
||||||
@@ -81,7 +79,7 @@ class SyncClientSavedMovieRepository(
|
|||||||
private fun createSync(): RemoteSourceSynchronizer<VideoDetails, Long> {
|
private fun createSync(): RemoteSourceSynchronizer<VideoDetails, Long> {
|
||||||
return RemoteSourceSynchronizer(
|
return RemoteSourceSynchronizer(
|
||||||
isSync = { syncRepository.isSynchronized(AggregateType.SAVED_MOVIE) },
|
isSync = { syncRepository.isSynchronized(AggregateType.SAVED_MOVIE) },
|
||||||
setSync = { syncRepository.set(AggregateType.SAVED_MOVIE, true) },
|
setSync = { syncRepository.set(AggregateType.SAVED_MOVIE, it) },
|
||||||
authTokenCache = authTokenCache,
|
authTokenCache = authTokenCache,
|
||||||
queryRemote = { remote.getAll().first() },
|
queryRemote = { remote.getAll().first() },
|
||||||
queryLocal = { local.getAll().first() },
|
queryLocal = { local.getAll().first() },
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ class SyncClientServerSearchRepository(
|
|||||||
private fun createSync(): RemoteSourceSynchronizer<String, String> {
|
private fun createSync(): RemoteSourceSynchronizer<String, String> {
|
||||||
return RemoteSourceSynchronizer(
|
return RemoteSourceSynchronizer(
|
||||||
isSync = { syncRepository.isSynchronized(AggregateType.SEARCH) },
|
isSync = { syncRepository.isSynchronized(AggregateType.SEARCH) },
|
||||||
setSync = { syncRepository.set(AggregateType.SEARCH, true) },
|
setSync = { syncRepository.set(AggregateType.SEARCH, it) },
|
||||||
authTokenCache = authTokenCache,
|
authTokenCache = authTokenCache,
|
||||||
queryRemote = { remote.search("").first() },
|
queryRemote = { remote.search("").first() },
|
||||||
queryLocal = { local.search("").first() },
|
queryLocal = { local.search("").first() },
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ class SyncMovieTagRepository(
|
|||||||
private fun createSync(): RemoteSourceSynchronizer<MovieTagLink, String> {
|
private fun createSync(): RemoteSourceSynchronizer<MovieTagLink, String> {
|
||||||
return RemoteSourceSynchronizer(
|
return RemoteSourceSynchronizer(
|
||||||
isSync = { syncRepository.isSynchronized(AggregateType.MOVIE_TAG) },
|
isSync = { syncRepository.isSynchronized(AggregateType.MOVIE_TAG) },
|
||||||
setSync = { syncRepository.set(AggregateType.MOVIE_TAG, true) },
|
setSync = { syncRepository.set(AggregateType.MOVIE_TAG, it) },
|
||||||
authTokenCache = tokenStorage,
|
authTokenCache = tokenStorage,
|
||||||
queryRemote = {
|
queryRemote = {
|
||||||
val allTags = userTagRemote.userTags.first()
|
val allTags = userTagRemote.userTags.first()
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ class SyncUserTagRepository(
|
|||||||
private fun createSync(): RemoteSourceSynchronizer<TagInfo, String> {
|
private fun createSync(): RemoteSourceSynchronizer<TagInfo, String> {
|
||||||
return RemoteSourceSynchronizer(
|
return RemoteSourceSynchronizer(
|
||||||
isSync = { syncRepository.isSynchronized(AggregateType.USER_TAG) },
|
isSync = { syncRepository.isSynchronized(AggregateType.USER_TAG) },
|
||||||
setSync = { syncRepository.set(AggregateType.USER_TAG, true) },
|
setSync = { syncRepository.set(AggregateType.USER_TAG, it) },
|
||||||
authTokenCache = tokenStorage,
|
authTokenCache = tokenStorage,
|
||||||
queryRemote = { remote.userTags.first() },
|
queryRemote = { remote.userTags.first() },
|
||||||
queryLocal = { local.userTags.first() },
|
queryLocal = { local.userTags.first() },
|
||||||
|
|||||||
-11
@@ -10,17 +10,6 @@ import ru.shadowsparky.http.domain.TokenStorage
|
|||||||
import ru.shadowsparky.http.domain.hasAccount
|
import ru.shadowsparky.http.domain.hasAccount
|
||||||
import ru.shadowsparky.koin
|
import ru.shadowsparky.koin
|
||||||
|
|
||||||
suspend fun TokenStorage.use(
|
|
||||||
active: suspend () -> Unit,
|
|
||||||
fallback: suspend () -> Unit
|
|
||||||
) {
|
|
||||||
if (hasAccount()) {
|
|
||||||
runCatching { active() }.onFailure { fallback() }
|
|
||||||
} else {
|
|
||||||
fallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> TokenStorage.queryFlow(
|
fun <T> TokenStorage.queryFlow(
|
||||||
active: Flow<T>,
|
active: Flow<T>,
|
||||||
fallback: Flow<T>,
|
fallback: Flow<T>,
|
||||||
|
|||||||
+21
-15
@@ -7,26 +7,32 @@ import org.gradle.kotlin.dsl.configure
|
|||||||
import ru.shadowsparky.applyCoroutines
|
import ru.shadowsparky.applyCoroutines
|
||||||
import ru.shadowsparky.findLibs
|
import ru.shadowsparky.findLibs
|
||||||
import ru.shadowsparky.setupAndroid
|
import ru.shadowsparky.setupAndroid
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
class AndroidApplication : Plugin<Project> {
|
class AndroidApplication : Plugin<Project> {
|
||||||
override fun apply(target: Project) = with(target) {
|
override fun apply(target: Project) = with(target) {
|
||||||
val libs = findLibs()
|
val libs = findLibs()
|
||||||
pluginManager.apply(libs.findPlugin("android.application").get().get().pluginId)
|
pluginManager.apply(libs.findPlugin("android.application").get().get().pluginId)
|
||||||
|
val keystoreFileProvider = rootProject.layout.projectDirectory.file("keystore.properties")
|
||||||
|
val keystorePropertiesProvider =
|
||||||
|
providers.fileContents(keystoreFileProvider).asText.map { content ->
|
||||||
|
Properties().apply { content.reader().use { load(it) } }
|
||||||
|
}
|
||||||
extensions.configure<ApplicationExtension> {
|
extensions.configure<ApplicationExtension> {
|
||||||
val keystorePropertiesFile = File(rootDir, "keystore.properties")
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (keystorePropertiesFile.exists()) {
|
register("release") {
|
||||||
val keystoreProperties = Properties()
|
val propertiesFile = keystoreFileProvider.asFile
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
|
||||||
create("release") {
|
if (propertiesFile.exists()) {
|
||||||
storeFile = File(rootDir, "release.keystore")
|
val properties = keystorePropertiesProvider.get()
|
||||||
check(storeFile?.exists() == true) { "Unable to load keystore file!" }
|
|
||||||
storePassword = keystoreProperties["RELEASE_STORE_PASSWORD"].toString()
|
val keystoreFile = rootProject.layout.projectDirectory.file("release.keystore").asFile
|
||||||
keyAlias = keystoreProperties["RELEASE_KEY_ALIAS"].toString()
|
check(keystoreFile.exists()) { "Unable to load keystore file!" }
|
||||||
keyPassword = keystoreProperties["RELEASE_KEY_PASSWORD"].toString()
|
|
||||||
|
storeFile = keystoreFile
|
||||||
|
storePassword = properties.getProperty("RELEASE_STORE_PASSWORD")
|
||||||
|
keyAlias = properties.getProperty("RELEASE_KEY_ALIAS")
|
||||||
|
keyPassword = properties.getProperty("RELEASE_KEY_PASSWORD")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,15 +44,15 @@ class AndroidApplication : Plugin<Project> {
|
|||||||
getByName("release") {
|
getByName("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (signingConfigs.findByName("release")?.storeFile?.exists() == true) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
proguardFiles(file("proguard-rules.pro"))
|
proguardFiles(layout.projectDirectory.file("proguard-rules.pro"))
|
||||||
}
|
}
|
||||||
getByName("debug") {
|
getByName("debug") {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (signingConfigs.findByName("release")?.storeFile?.exists() == true) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,12 +2,12 @@ kotlin.code.style=official
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xms1g -Xmx3584m -XX:+UseParallelGC -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xms1g -Xmx3584m -XX:+UseParallelGC -Dfile.encoding=UTF-8
|
||||||
org.gradle.daemon.idletimeout=60000
|
|
||||||
|
|
||||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||||
org.jetbrains.compose.experimental.uikit.enabled=true
|
org.jetbrains.compose.experimental.uikit.enabled=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.r8.strictFullModeForKeepRules=true
|
android.r8.strictFullModeForKeepRules=true
|
||||||
|
android.injected.enableStableIds=true
|
||||||
|
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
|||||||
+3
-3
@@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class AndroidPlayerController(context: Context) : PlayerController {
|
class AndroidPlayerController(context: Context) : PlayerController {
|
||||||
@@ -114,9 +115,7 @@ class AndroidPlayerController(context: Context) : PlayerController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.flatten()
|
}.flatten()
|
||||||
|
|
||||||
audioTracks.value = list
|
audioTracks.value = list
|
||||||
|
|
||||||
if (!isTrackInitialized && list.isNotEmpty()) {
|
if (!isTrackInitialized && list.isNotEmpty()) {
|
||||||
isTrackInitialized = true
|
isTrackInitialized = true
|
||||||
list.firstOrNull()?.let { selectAudioTrack(it.id) }
|
list.firstOrNull()?.let { selectAudioTrack(it.id) }
|
||||||
@@ -151,6 +150,7 @@ class AndroidPlayerController(context: Context) : PlayerController {
|
|||||||
|
|
||||||
override suspend fun load(url: String, startPositionMs: Long): Unit =
|
override suspend fun load(url: String, startPositionMs: Long): Unit =
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
isTrackInitialized = false
|
||||||
bindListenerIfNeeded(true)
|
bindListenerIfNeeded(true)
|
||||||
val mediaItem = MediaItem.fromUri(url)
|
val mediaItem = MediaItem.fromUri(url)
|
||||||
exoPlayer.setMediaItem(mediaItem)
|
exoPlayer.setMediaItem(mediaItem)
|
||||||
@@ -199,7 +199,7 @@ class AndroidPlayerController(context: Context) : PlayerController {
|
|||||||
progressJob = scope.launch {
|
progressJob = scope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
updateState()
|
updateState()
|
||||||
delay(200)
|
delay(200.milliseconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-39
@@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import ru.shadowsparky.ui.components.isTv
|
||||||
import ru.shadowsparky.videoplayer.AudioTrackInfo
|
import ru.shadowsparky.videoplayer.AudioTrackInfo
|
||||||
import ru.shadowsparky.videoplayer.ControlsState
|
import ru.shadowsparky.videoplayer.ControlsState
|
||||||
import ru.shadowsparky.videoplayer.PlayerController
|
import ru.shadowsparky.videoplayer.PlayerController
|
||||||
@@ -74,6 +75,7 @@ fun DefaultControls(
|
|||||||
controller: PlayerController,
|
controller: PlayerController,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val isTv = isTv()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val audioTracksState by controller.audioTracks.collectAsState()
|
val audioTracksState by controller.audioTracks.collectAsState()
|
||||||
|
|
||||||
@@ -102,8 +104,8 @@ fun DefaultControls(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LockUiButton(focus, isUiLocked.value, audioTracksState.isNotEmpty()) {
|
if (!isTv) {
|
||||||
isUiLocked.value = it
|
LockUiButton(focus, isUiLocked.value) { isUiLocked.value = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isUiLocked.value) {
|
if (!isUiLocked.value) {
|
||||||
@@ -154,8 +156,8 @@ fun DefaultControls(
|
|||||||
Key.DirectionRight -> {
|
Key.DirectionRight -> {
|
||||||
scope.launch { controller.seek(false) }
|
scope.launch { controller.seek(false) }
|
||||||
}
|
}
|
||||||
Key.DirectionUp -> focus.next.requestFocus()
|
Key.DirectionUp -> focus.play.requestFocus()
|
||||||
Key.DirectionDown -> focus.lock.requestFocus()
|
Key.DirectionDown -> focus.audioTrack.requestFocus()
|
||||||
else -> return@onPreviewKeyEvent false
|
else -> return@onPreviewKeyEvent false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +173,6 @@ fun DefaultControls(
|
|||||||
private fun LockUiButton(
|
private fun LockUiButton(
|
||||||
focusContainer: FocusContainer,
|
focusContainer: FocusContainer,
|
||||||
state: Boolean,
|
state: Boolean,
|
||||||
hasAudioTrack: Boolean,
|
|
||||||
onChanged: (Boolean) -> Unit
|
onChanged: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -215,7 +216,7 @@ private fun AudioTracksButton(
|
|||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
modifier = Modifier.focusable()
|
modifier = Modifier.focusable()
|
||||||
) {
|
) {
|
||||||
for (i in 0 until tracks.size) {
|
for (i in tracks.indices) {
|
||||||
val (track, requester) = tracks[i]
|
val (track, requester) = tracks[i]
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@@ -280,6 +281,7 @@ private fun BoxScope.CenterControls(
|
|||||||
controller: PlayerController
|
controller: PlayerController
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
var canRequestFocus by remember { mutableStateOf(false) }
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
.focusGroup()
|
.focusGroup()
|
||||||
@@ -287,9 +289,7 @@ private fun BoxScope.CenterControls(
|
|||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isBuffering -> {
|
state.isBuffering -> CircularWavyProgressIndicator()
|
||||||
CircularWavyProgressIndicator()
|
|
||||||
}
|
|
||||||
state.isEnded -> {
|
state.isEnded -> {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -297,7 +297,7 @@ private fun BoxScope.CenterControls(
|
|||||||
controller.seekTo(0)
|
controller.seekTo(0)
|
||||||
controller.play()
|
controller.play()
|
||||||
}
|
}
|
||||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next)
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.replay_24px),
|
painter = painterResource(Res.drawable.replay_24px),
|
||||||
@@ -312,18 +312,6 @@ private fun BoxScope.CenterControls(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
scope.launch { controller.seek(true) }
|
|
||||||
}, modifier = Modifier.focusableWithBorder(focusContainer.prev)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "-$DEF_SEEK_TIME",
|
|
||||||
color = Color.White,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -343,25 +331,17 @@ private fun BoxScope.CenterControls(
|
|||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
scope.launch { controller.seek(false) }
|
|
||||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "+$DEF_SEEK_TIME",
|
|
||||||
color = Color.White,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
canRequestFocus = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(null) {
|
LaunchedEffect(state.videoUrl) { canRequestFocus = false }
|
||||||
withFrameNanos {}
|
LaunchedEffect(canRequestFocus) {
|
||||||
focusContainer.play.requestFocus()
|
if (canRequestFocus) {
|
||||||
|
withFrameNanos {}
|
||||||
|
focusContainer.play.requestFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,9 +379,7 @@ private fun formatDuration(millis: Long): String {
|
|||||||
private class FocusContainer(
|
private class FocusContainer(
|
||||||
val audioTrack: FocusRequester = FocusRequester(),
|
val audioTrack: FocusRequester = FocusRequester(),
|
||||||
val lock: FocusRequester = FocusRequester(),
|
val lock: FocusRequester = FocusRequester(),
|
||||||
val prev: FocusRequester = FocusRequester(),
|
|
||||||
val play: FocusRequester = FocusRequester(),
|
val play: FocusRequester = FocusRequester(),
|
||||||
val next: FocusRequester = FocusRequester(),
|
|
||||||
val slider: FocusRequester = FocusRequester()
|
val slider: FocusRequester = FocusRequester()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user