update tv ux, fix configuration-cache issues
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ class SyncClientServerRecentlyWatchedRepository(
|
||||
private fun createSync(): RemoteSourceSynchronizer<RecentlyWatchedInfo, String> {
|
||||
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() },
|
||||
|
||||
+2
-4
@@ -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<VideoDetails, Long> {
|
||||
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() },
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ class SyncClientServerSearchRepository(
|
||||
private fun createSync(): RemoteSourceSynchronizer<String, String> {
|
||||
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() },
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ class SyncMovieTagRepository(
|
||||
private fun createSync(): RemoteSourceSynchronizer<MovieTagLink, String> {
|
||||
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()
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class SyncUserTagRepository(
|
||||
private fun createSync(): RemoteSourceSynchronizer<TagInfo, String> {
|
||||
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() },
|
||||
|
||||
-11
@@ -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 <T> TokenStorage.queryFlow(
|
||||
active: Flow<T>,
|
||||
fallback: Flow<T>,
|
||||
|
||||
+21
-15
@@ -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<Project> {
|
||||
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<ApplicationExtension> {
|
||||
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<Project> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-39
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user