diff --git a/apps/vbox/client/androidApp/build.gradle.kts b/apps/vbox/client/androidApp/build.gradle.kts index 9789983..75e07a1 100644 --- a/apps/vbox/client/androidApp/build.gradle.kts +++ b/apps/vbox/client/androidApp/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = namespace minSdk = libs.versions.proj.min.get().toInt() targetSdk = libs.versions.proj.target.get().toInt() - versionCode = 39 - versionName = "1.21" + versionCode = 40 + versionName = "1.22" } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt index 4a33de5..30b985f 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/recent/SyncClientServerRecentlyWatchedRepository.kt @@ -44,7 +44,7 @@ class SyncClientServerRecentlyWatchedRepository( private fun createSync(): RemoteSourceSynchronizer { return RemoteSourceSynchronizer( isSync = { syncRepository.isSynchronized(AggregateType.RECENTLY_WATCHED) }, - setSync = { syncRepository.set(AggregateType.RECENTLY_WATCHED, true) }, + setSync = { syncRepository.set(AggregateType.RECENTLY_WATCHED, it) }, authTokenCache = authTokenCache, queryRemote = { remote.getAllRecentlyWatched() }, queryLocal = { local.getAllRecentlyWatched() }, diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt index 0f59caa..56fbc34 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/saved/SyncClientSavedMovieRepository.kt @@ -8,7 +8,6 @@ import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put import org.koin.core.annotation.Factory import org.koin.core.annotation.Named -import ru.shadowsparky.domain.DispatcherProvider import ru.shadowsparky.http.domain.TokenStorage import ru.shadowsparky.http.domain.hasAccount import ru.shadowsparky.vbox.shared.data.RemoteSourceSynchronizer @@ -30,8 +29,7 @@ class SyncClientSavedMovieRepository( private val syncRepository: SyncRepository, private val authTokenCache: TokenStorage, private val json: Json, - private val pendingOperationSynchronizer: PendingOperationSynchronizer, - private val dispatcherProvider: DispatcherProvider + private val pendingOperationSynchronizer: PendingOperationSynchronizer ) : SavedMovieRepository { override suspend fun save(details: VideoDetails) { @@ -81,7 +79,7 @@ class SyncClientSavedMovieRepository( private fun createSync(): RemoteSourceSynchronizer { return RemoteSourceSynchronizer( isSync = { syncRepository.isSynchronized(AggregateType.SAVED_MOVIE) }, - setSync = { syncRepository.set(AggregateType.SAVED_MOVIE, true) }, + setSync = { syncRepository.set(AggregateType.SAVED_MOVIE, it) }, authTokenCache = authTokenCache, queryRemote = { remote.getAll().first() }, queryLocal = { local.getAll().first() }, diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/search/SyncClientServerSearchRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/search/SyncClientServerSearchRepository.kt index a626616..322b662 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/search/SyncClientServerSearchRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/search/SyncClientServerSearchRepository.kt @@ -42,7 +42,7 @@ class SyncClientServerSearchRepository( private fun createSync(): RemoteSourceSynchronizer { return RemoteSourceSynchronizer( isSync = { syncRepository.isSynchronized(AggregateType.SEARCH) }, - setSync = { syncRepository.set(AggregateType.SEARCH, true) }, + setSync = { syncRepository.set(AggregateType.SEARCH, it) }, authTokenCache = authTokenCache, queryRemote = { remote.search("").first() }, queryLocal = { local.search("").first() }, diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt index e9c7f7b..3be49c9 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncMovieTagRepository.kt @@ -43,7 +43,7 @@ class SyncMovieTagRepository( private fun createSync(): RemoteSourceSynchronizer { return RemoteSourceSynchronizer( isSync = { syncRepository.isSynchronized(AggregateType.MOVIE_TAG) }, - setSync = { syncRepository.set(AggregateType.MOVIE_TAG, true) }, + setSync = { syncRepository.set(AggregateType.MOVIE_TAG, it) }, authTokenCache = tokenStorage, queryRemote = { val allTags = userTagRemote.userTags.first() diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt index 007f9b4..d5edacc 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/data/tag/SyncUserTagRepository.kt @@ -40,7 +40,7 @@ class SyncUserTagRepository( private fun createSync(): RemoteSourceSynchronizer { return RemoteSourceSynchronizer( isSync = { syncRepository.isSynchronized(AggregateType.USER_TAG) }, - setSync = { syncRepository.set(AggregateType.USER_TAG, true) }, + setSync = { syncRepository.set(AggregateType.USER_TAG, it) }, authTokenCache = tokenStorage, queryRemote = { remote.userTags.first() }, queryLocal = { local.userTags.first() }, diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/AuthTokenCacheExt.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/AuthTokenCacheExt.kt index 50c741a..d4a8d47 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/AuthTokenCacheExt.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/domain/AuthTokenCacheExt.kt @@ -10,17 +10,6 @@ import ru.shadowsparky.http.domain.TokenStorage import ru.shadowsparky.http.domain.hasAccount import ru.shadowsparky.koin -suspend fun TokenStorage.use( - active: suspend () -> Unit, - fallback: suspend () -> Unit -) { - if (hasAccount()) { - runCatching { active() }.onFailure { fallback() } - } else { - fallback() - } -} - fun TokenStorage.queryFlow( active: Flow, fallback: Flow, diff --git a/build-logic/convention/src/main/kotlin/ru/shadowsparky/plugin/AndroidApplication.kt b/build-logic/convention/src/main/kotlin/ru/shadowsparky/plugin/AndroidApplication.kt index 4399a15..5ae997b 100644 --- a/build-logic/convention/src/main/kotlin/ru/shadowsparky/plugin/AndroidApplication.kt +++ b/build-logic/convention/src/main/kotlin/ru/shadowsparky/plugin/AndroidApplication.kt @@ -7,26 +7,32 @@ import org.gradle.kotlin.dsl.configure import ru.shadowsparky.applyCoroutines import ru.shadowsparky.findLibs import ru.shadowsparky.setupAndroid -import java.io.File -import java.io.FileInputStream import java.util.Properties class AndroidApplication : Plugin { override fun apply(target: Project) = with(target) { val libs = findLibs() 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 { - val keystorePropertiesFile = File(rootDir, "keystore.properties") signingConfigs { - if (keystorePropertiesFile.exists()) { - val keystoreProperties = Properties() - keystoreProperties.load(FileInputStream(keystorePropertiesFile)) - create("release") { - storeFile = File(rootDir, "release.keystore") - check(storeFile?.exists() == true) { "Unable to load keystore file!" } - storePassword = keystoreProperties["RELEASE_STORE_PASSWORD"].toString() - keyAlias = keystoreProperties["RELEASE_KEY_ALIAS"].toString() - keyPassword = keystoreProperties["RELEASE_KEY_PASSWORD"].toString() + register("release") { + val propertiesFile = keystoreFileProvider.asFile + + if (propertiesFile.exists()) { + val properties = keystorePropertiesProvider.get() + + val keystoreFile = rootProject.layout.projectDirectory.file("release.keystore").asFile + check(keystoreFile.exists()) { "Unable to load keystore file!" } + + 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 { getByName("release") { isMinifyEnabled = true isShrinkResources = true - if (keystorePropertiesFile.exists()) { + if (signingConfigs.findByName("release")?.storeFile?.exists() == true) { signingConfig = signingConfigs.getByName("release") } - proguardFiles(file("proguard-rules.pro")) + proguardFiles(layout.projectDirectory.file("proguard-rules.pro")) } getByName("debug") { isMinifyEnabled = false applicationIdSuffix = ".debug" - if (keystorePropertiesFile.exists()) { + if (signingConfigs.findByName("release")?.storeFile?.exists() == true) { signingConfig = signingConfigs.getByName("release") } } diff --git a/gradle.properties b/gradle.properties index f059a69..2e46d06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,12 +2,12 @@ kotlin.code.style=official android.useAndroidX=true 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.uikit.enabled=true android.nonTransitiveRClass=false android.r8.strictFullModeForKeepRules=true +android.injected.enableStableIds=true org.gradle.caching=true org.gradle.parallel=true diff --git a/libs/video-player/src/androidMain/kotlin/ru/shadowsparky/videoplayer/AndroidPlayerController.kt b/libs/video-player/src/androidMain/kotlin/ru/shadowsparky/videoplayer/AndroidPlayerController.kt index 16526b5..7dd8450 100644 --- a/libs/video-player/src/androidMain/kotlin/ru/shadowsparky/videoplayer/AndroidPlayerController.kt +++ b/libs/video-player/src/androidMain/kotlin/ru/shadowsparky/videoplayer/AndroidPlayerController.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.getString import org.koin.core.annotation.Factory +import kotlin.time.Duration.Companion.milliseconds @Factory class AndroidPlayerController(context: Context) : PlayerController { @@ -114,9 +115,7 @@ class AndroidPlayerController(context: Context) : PlayerController { ) } }.flatten() - audioTracks.value = list - if (!isTrackInitialized && list.isNotEmpty()) { isTrackInitialized = true list.firstOrNull()?.let { selectAudioTrack(it.id) } @@ -151,6 +150,7 @@ class AndroidPlayerController(context: Context) : PlayerController { override suspend fun load(url: String, startPositionMs: Long): Unit = withContext(Dispatchers.Main) { + isTrackInitialized = false bindListenerIfNeeded(true) val mediaItem = MediaItem.fromUri(url) exoPlayer.setMediaItem(mediaItem) @@ -199,7 +199,7 @@ class AndroidPlayerController(context: Context) : PlayerController { progressJob = scope.launch { while (isActive) { updateState() - delay(200) + delay(200.milliseconds) } } } diff --git a/libs/video-player/src/commonMain/kotlin/ru/shadowsparky/videoplayer/ui/DefaultControls.kt b/libs/video-player/src/commonMain/kotlin/ru/shadowsparky/videoplayer/ui/DefaultControls.kt index 30ff75b..c49ce6f 100644 --- a/libs/video-player/src/commonMain/kotlin/ru/shadowsparky/videoplayer/ui/DefaultControls.kt +++ b/libs/video-player/src/commonMain/kotlin/ru/shadowsparky/videoplayer/ui/DefaultControls.kt @@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource +import ru.shadowsparky.ui.components.isTv import ru.shadowsparky.videoplayer.AudioTrackInfo import ru.shadowsparky.videoplayer.ControlsState import ru.shadowsparky.videoplayer.PlayerController @@ -74,6 +75,7 @@ fun DefaultControls( controller: PlayerController, modifier: Modifier = Modifier ) { + val isTv = isTv() val scope = rememberCoroutineScope() val audioTracksState by controller.audioTracks.collectAsState() @@ -102,8 +104,8 @@ fun DefaultControls( } ) } - LockUiButton(focus, isUiLocked.value, audioTracksState.isNotEmpty()) { - isUiLocked.value = it + if (!isTv) { + LockUiButton(focus, isUiLocked.value) { isUiLocked.value = it } } } if (!isUiLocked.value) { @@ -154,8 +156,8 @@ fun DefaultControls( Key.DirectionRight -> { scope.launch { controller.seek(false) } } - Key.DirectionUp -> focus.next.requestFocus() - Key.DirectionDown -> focus.lock.requestFocus() + Key.DirectionUp -> focus.play.requestFocus() + Key.DirectionDown -> focus.audioTrack.requestFocus() else -> return@onPreviewKeyEvent false } } @@ -171,7 +173,6 @@ fun DefaultControls( private fun LockUiButton( focusContainer: FocusContainer, state: Boolean, - hasAudioTrack: Boolean, onChanged: (Boolean) -> Unit ) { IconButton( @@ -215,7 +216,7 @@ private fun AudioTracksButton( onDismissRequest = { expanded = false }, modifier = Modifier.focusable() ) { - for (i in 0 until tracks.size) { + for (i in tracks.indices) { val (track, requester) = tracks[i] DropdownMenuItem( text = { @@ -280,6 +281,7 @@ private fun BoxScope.CenterControls( controller: PlayerController ) { val scope = rememberCoroutineScope() + var canRequestFocus by remember { mutableStateOf(false) } Box( modifier = Modifier.align(Alignment.Center) .focusGroup() @@ -287,9 +289,7 @@ private fun BoxScope.CenterControls( .padding(horizontal = 16.dp, vertical = 8.dp) ) { when { - state.isBuffering -> { - CircularWavyProgressIndicator() - } + state.isBuffering -> CircularWavyProgressIndicator() state.isEnded -> { IconButton( onClick = { @@ -297,7 +297,7 @@ private fun BoxScope.CenterControls( controller.seekTo(0) controller.play() } - }, modifier = Modifier.focusableWithBorder(focusContainer.next) + } ) { Icon( painter = painterResource(Res.drawable.replay_24px), @@ -312,18 +312,6 @@ private fun BoxScope.CenterControls( verticalAlignment = Alignment.CenterVertically, 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( onClick = { scope.launch { @@ -343,25 +331,17 @@ private fun BoxScope.CenterControls( 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) { - withFrameNanos {} - focusContainer.play.requestFocus() + LaunchedEffect(state.videoUrl) { canRequestFocus = false } + LaunchedEffect(canRequestFocus) { + if (canRequestFocus) { + withFrameNanos {} + focusContainer.play.requestFocus() + } } } @@ -399,9 +379,7 @@ private fun formatDuration(millis: Long): String { private class FocusContainer( val audioTrack: FocusRequester = FocusRequester(), val lock: FocusRequester = FocusRequester(), - val prev: FocusRequester = FocusRequester(), val play: FocusRequester = FocusRequester(), - val next: FocusRequester = FocusRequester(), val slider: FocusRequester = FocusRequester() )