mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
address review: signal now playing with an event flow, not an int
This commit is contained in:
@@ -9,13 +9,14 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.subtracks.ui.NowPlayingLauncher
|
||||||
import com.subtracks.ui.SubtracksRoot
|
import com.subtracks.ui.SubtracksRoot
|
||||||
import com.subtracks.ui.theme.SubtracksTheme
|
import com.subtracks.ui.theme.SubtracksTheme
|
||||||
|
import org.koin.core.context.GlobalContext
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val nowPlayingRequest = mutableIntStateOf(0)
|
private val nowPlayingLauncher: NowPlayingLauncher by lazy { GlobalContext.get().get() }
|
||||||
|
|
||||||
private val requestNotifications =
|
private val requestNotifications =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
|
||||||
@@ -25,11 +26,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
requestNotificationPermission()
|
requestNotificationPermission()
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
if (shouldRecordNowPlayingLaunch(intent?.action, restoringState = savedInstanceState != null)) {
|
if (shouldRecordNowPlayingLaunch(intent?.action, restoringState = savedInstanceState != null)) {
|
||||||
nowPlayingRequest.intValue++
|
nowPlayingLauncher.request()
|
||||||
}
|
}
|
||||||
setContent {
|
setContent {
|
||||||
SubtracksTheme {
|
SubtracksTheme {
|
||||||
SubtracksRoot(nowPlayingRequest = nowPlayingRequest.intValue)
|
SubtracksRoot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +38,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
if (intent.action == ACTION_OPEN_NOW_PLAYING) nowPlayingRequest.intValue++
|
if (intent.action == ACTION_OPEN_NOW_PLAYING) nowPlayingLauncher.request()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestNotificationPermission() {
|
private fun requestNotificationPermission() {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.subtracks.playback.MediaSessionConnection
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.playback.PlayerConnection
|
import com.subtracks.playback.PlayerConnection
|
||||||
import com.subtracks.playback.Scrobbler
|
import com.subtracks.playback.Scrobbler
|
||||||
|
import com.subtracks.ui.NowPlayingLauncher
|
||||||
import com.subtracks.ui.RootViewModel
|
import com.subtracks.ui.RootViewModel
|
||||||
import com.subtracks.ui.downloads.DownloadsViewModel
|
import com.subtracks.ui.downloads.DownloadsViewModel
|
||||||
import com.subtracks.ui.home.HomeListViewModel
|
import com.subtracks.ui.home.HomeListViewModel
|
||||||
@@ -108,6 +109,7 @@ fun appModule(
|
|||||||
it.attach(playback.state, playback.positionMs)
|
it.attach(playback.state, playback.positionMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
single { NowPlayingLauncher() }
|
||||||
viewModel { RootViewModel(get()) }
|
viewModel { RootViewModel(get()) }
|
||||||
viewModel { LibraryViewModel(get(), get(), get(), get(), get(), get()) }
|
viewModel { LibraryViewModel(get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { HomeViewModel(get(), get(), get(), get()) }
|
viewModel { HomeViewModel(get(), get(), get(), get()) }
|
||||||
|
|||||||
@@ -181,10 +181,7 @@ internal fun focusPops(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SubtracksRoot(
|
fun SubtracksRoot(root: RootViewModel = koinViewModel()) {
|
||||||
root: RootViewModel = koinViewModel(),
|
|
||||||
nowPlayingRequest: Int = 0,
|
|
||||||
) {
|
|
||||||
when (val hasSource = root.hasSource.collectAsStateWithLifecycle().value) {
|
when (val hasSource = root.hasSource.collectAsStateWithLifecycle().value) {
|
||||||
null -> {
|
null -> {
|
||||||
LoadingState()
|
LoadingState()
|
||||||
@@ -198,14 +195,14 @@ fun SubtracksRoot(
|
|||||||
val playbackController = koinInject<PlaybackController>()
|
val playbackController = koinInject<PlaybackController>()
|
||||||
LaunchedEffect(Unit) { playbackController.connect() }
|
LaunchedEffect(Unit) { playbackController.connect() }
|
||||||
val playbackReady by playbackController.ready.collectAsStateWithLifecycle()
|
val playbackReady by playbackController.ready.collectAsStateWithLifecycle()
|
||||||
if (playbackReady) MainNavigation(nowPlayingRequest) else LoadingState()
|
if (playbackReady) MainNavigation() else LoadingState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainNavigation(nowPlayingRequest: Int) {
|
private fun MainNavigation() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val currentRoute =
|
val currentRoute =
|
||||||
navController
|
navController
|
||||||
@@ -215,13 +212,13 @@ private fun MainNavigation(nowPlayingRequest: Int) {
|
|||||||
?.route
|
?.route
|
||||||
val tabBarVisible = currentRoute == Routes.LIBRARY
|
val tabBarVisible = currentRoute == Routes.LIBRARY
|
||||||
val playbackController = koinInject<PlaybackController>()
|
val playbackController = koinInject<PlaybackController>()
|
||||||
|
val nowPlayingLauncher = koinInject<NowPlayingLauncher>()
|
||||||
val libraryRepository = koinInject<LibraryRepository>()
|
val libraryRepository = koinInject<LibraryRepository>()
|
||||||
val downloadRepository = koinInject<DownloadRepository>()
|
val downloadRepository = koinInject<DownloadRepository>()
|
||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
val downloads by downloadRepository.states().collectAsStateWithLifecycle()
|
val downloads by downloadRepository.states().collectAsStateWithLifecycle()
|
||||||
var showingQueue by rememberSaveable { mutableStateOf(false) }
|
var showingQueue by rememberSaveable { mutableStateOf(false) }
|
||||||
var nowPlayingOpen by rememberSaveable { mutableStateOf(false) }
|
var nowPlayingOpen by rememberSaveable { mutableStateOf(false) }
|
||||||
var handledNowPlayingRequest by remember { mutableStateOf(0) }
|
|
||||||
var nowPlayingProgress by remember { mutableFloatStateOf(0f) }
|
var nowPlayingProgress by remember { mutableFloatStateOf(0f) }
|
||||||
var miniPlayerTopPx by remember { mutableFloatStateOf(0f) }
|
var miniPlayerTopPx by remember { mutableFloatStateOf(0f) }
|
||||||
var nowPlayingFadeOut by remember { mutableStateOf(false) }
|
var nowPlayingFadeOut by remember { mutableStateOf(false) }
|
||||||
@@ -306,9 +303,9 @@ private fun MainNavigation(nowPlayingRequest: Int) {
|
|||||||
nowPlayingProgress = 0f
|
nowPlayingProgress = 0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(nowPlayingRequest, playerVisible) {
|
LaunchedEffect(playerVisible) {
|
||||||
if (playerVisible && nowPlayingRequest > handledNowPlayingRequest) {
|
if (!playerVisible) return@LaunchedEffect
|
||||||
handledNowPlayingRequest = nowPlayingRequest
|
nowPlayingLauncher.openNowPlaying.collect {
|
||||||
if (navController.currentDestination?.route in DETAIL_ROUTES) {
|
if (navController.currentDestination?.route in DETAIL_ROUTES) {
|
||||||
navController.popBackStack(Routes.LIBRARY, inclusive = false)
|
navController.popBackStack(Routes.LIBRARY, inclusive = false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.subtracks.ui
|
||||||
|
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
|
||||||
|
class NowPlayingLauncher {
|
||||||
|
private val requests = Channel<Unit>(Channel.CONFLATED)
|
||||||
|
|
||||||
|
val openNowPlaying: Flow<Unit> = requests.receiveAsFlow()
|
||||||
|
|
||||||
|
fun request() {
|
||||||
|
requests.trySend(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.subtracks.ui
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class NowPlayingLauncherTest {
|
||||||
|
@Test
|
||||||
|
fun aRequestMadeBeforeCollectionIsDelivered() =
|
||||||
|
runTest {
|
||||||
|
val launcher = NowPlayingLauncher()
|
||||||
|
launcher.request()
|
||||||
|
assertEquals(Unit, launcher.openNowPlaying.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun repeatedRequestsCollapseToTheLatest() =
|
||||||
|
runTest {
|
||||||
|
val launcher = NowPlayingLauncher()
|
||||||
|
repeat(3) { launcher.request() }
|
||||||
|
assertEquals(Unit, launcher.openNowPlaying.first())
|
||||||
|
assertNull(withTimeoutOrNull(100) { launcher.openNowPlaying.first() })
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user