Skip to content
Snippets Groups Projects
Commit 95723f95 authored by Jonas Broeckmann's avatar Jonas Broeckmann
Browse files

Changed screen model management

parent d8ac6ae7
No related branches found
No related tags found
1 merge request!2Official API
package net.novagamestudios.kaffeekasse
import android.app.Activity
import android.app.Application
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.remember
import androidx.credentials.CredentialManager
import cafe.adriel.voyager.core.concurrent.ThreadSafeMap
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.platform.multiplatformName
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.runBlocking
import net.novagamestudios.common_utils.Logger
import net.novagamestudios.common_utils.compose.application
import net.novagamestudios.common_utils.compose.state.loadInitialBlocking
import net.novagamestudios.common_utils.error
import net.novagamestudios.common_utils.info
import net.novagamestudios.common_utils.toastShort
......@@ -42,67 +34,52 @@ import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
import net.novagamestudios.kaffeekasse.repositories.newSettingsStore
import net.novagamestudios.kaffeekasse.repositories.releases.GitLabReleases
import net.novagamestudios.kaffeekasse.ui.util.derived
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.util.context
class App : Application(), RepositoryProvider, CoroutineScope by MainScope() + CoroutineName(App::class.simpleName!!), Logger {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
}
override fun onCreate() {
super.onCreate()
instanceOrNull = this
info { "App instance available" }
runBlocking { loadInitialSettings() }
launch { tryAutoLoginDevice() }
launch { fetchNewerReleases() }
}
private suspend fun loadInitialSettings() {
try {
settingsRepository.loadInitialBlocking()
settingsRepository.loadInitial()
} catch (e: Throwable) {
error(e) { "Failed to load initial settings" }
toastShort("Failed to load settings: ${e.message}")
throw e
}
launch {
try {
loginRepository.tryAutoLoginDevice()
} catch (e: Throwable) {
warn(e) { "Failed to auto-login" }
toastShort("Failed to auto-login: ${e.message}")
}
}
launch {
try {
releases.fetchNewerReleases()
} catch (e: Throwable) {
warn(e) { "Failed to fetch newer releases" }
toastShort("Failed to check for updates: ${e.message}")
}
}
}
companion object : Logger {
var instanceOrNull: App? = null
private set
val instance get() = instanceOrNull ?: error("App instance not available")
val globalScreenModels = ThreadSafeMap<String, ScreenModel>()
@Composable
inline fun <reified T : ScreenModel> globalScreenModel(
tag: String? = null,
crossinline factory: @DisallowComposableCalls App.() -> T
): T = with(app()) {
val key = "${T::class.multiplatformName}:${tag ?: "default"}"
remember(key) {
globalScreenModels.getOrPut(key) { factory() } as T
}
private suspend fun tryAutoLoginDevice() {
try {
loginRepository.tryAutoLoginDevice()
} catch (e: Throwable) {
warn(e) { "Failed to auto-login device" }
toastShort("Failed to auto-login device: ${e.message}")
}
@Composable
inline fun <reified T : ScreenModel> navigatorScreenModel(
crossinline factory: @DisallowComposableCalls App.() -> T
): T = with(app()) {
LocalNavigator.currentOrThrow.rememberNavigatorScreenModel { factory() }
}
private suspend fun fetchNewerReleases() {
try {
releases.fetchNewerReleases()
} catch (e: Throwable) {
warn(e) { "Failed to fetch newer releases" }
toastShort("Failed to check for updates: ${e.message}")
}
}
@Composable
fun settings(): SettingsRepository = app().settingsRepository
companion object {
@Composable fun settings(): SettingsRepository = app().settingsRepository
val developerMode: Boolean @Composable get() = settings().derived { developerMode }.value
}
......@@ -161,19 +138,8 @@ class App : Application(), RepositoryProvider, CoroutineScope by MainScope() + C
)
}
val Activity.app get() = application as App
@Composable
fun app() = application<App>()
context (S)
@Composable
inline operator fun <S : Screen, reified T : ScreenModel> ScreenModelFactory<S, T>.getValue(
thisRef: Any?,
property: Any?
) = with(app()) {
rememberScreenModel {
create(context<S>())
}
}
......@@ -10,27 +10,46 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.novagamestudios.common_utils.LocalLogger
import net.novagamestudios.common_utils.Logger
import net.novagamestudios.common_utils.error
import net.novagamestudios.kaffeekasse.repositories.SettingsRepository
import net.novagamestudios.kaffeekasse.ui.App
import net.novagamestudios.kaffeekasse.ui.util.removeScrollableTabRowMinimumTabWidth
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
syncFullscreenWithSettings(app.settingsRepository)
removeScrollableTabRowMinimumTabWidth()
setContent {
CompositionLocalProvider(
LocalLogger provides app()
) {
CompositionLocalProvider(LocalLogger provides app()) {
App()
}
}
lifecycleScope.launch {
App.instance.settingsRepository.values
}
@SuppressLint("WrongConstant")
private fun syncFullscreenWithSettings(settingsRepository: SettingsRepository): Job {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowInsetsController.hide(WindowInsets.Type.systemBars())
}
}
fun showSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowInsetsController.show(WindowInsets.Type.systemBars())
}
}
return lifecycleScope.launch {
settingsRepository.values
.map { it.fullscreen }
.distinctUntilChanged()
.collect { fullscreen ->
......@@ -41,39 +60,5 @@ class MainActivity : ComponentActivity() {
}
}
}
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
@SuppressLint("WrongConstant")
private fun hideSystemUI() {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowInsetsController.hide(WindowInsets.Type.systemBars())
}
}
@SuppressLint("WrongConstant")
private fun showSystemUI() {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowInsetsController.show(WindowInsets.Type.systemBars())
}
}
private companion object : Logger {
// See https://issuetracker.google.com/issues/226665301
private fun removeScrollableTabRowMinimumTabWidth() {
try {
Class
.forName("androidx.compose.material3.TabRowKt")
.getDeclaredField("ScrollableTabRowMinimumTabWidth").apply {
isAccessible = true
}.set(this, 0f)
} catch (e: Exception) {
error(e) { "Failed to remove ScrollableTabRowMinimumTabWidth" }
}
}
}
}
......@@ -30,6 +30,8 @@ import net.novagamestudios.kaffeekasse.ui.navigation.AppScreenTransition
import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
import net.novagamestudios.kaffeekasse.ui.theme.KaffeekasseTheme
import net.novagamestudios.kaffeekasse.ui.util.navigation.requireWithKey
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.GlobalScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelProvider
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere
......@@ -39,12 +41,12 @@ class AppScreenModel private constructor(
val session by portal.session.collectAsStateHere()
companion object {
companion object : GlobalScreenModelFactory<App, AppScreenModel>, ScreenModelProvider<AppScreenModel> {
context (RepositoryProvider)
fun create() = AppScreenModel(
override fun create(app: App) = AppScreenModel(
portal = portalRepository
)
val model @Composable get() = App.globalScreenModel { create() }
@get:Composable override val model by this
}
}
......
package net.novagamestudios.kaffeekasse.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import cafe.adriel.voyager.core.concurrent.ThreadSafeMap
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.platform.multiplatformName
import cafe.adriel.voyager.core.screen.Screen
import net.novagamestudios.kaffeekasse.App
import net.novagamestudios.kaffeekasse.app
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.GlobalScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.util.context
val globalScreenModels = ThreadSafeMap<String, ScreenModel>()
@Composable
inline operator fun <reified T : ScreenModel> GlobalScreenModelFactory<App, T>.getValue(
thisRef: Any?,
property: Any?
) = with(app()) {
val key = "${T::class.multiplatformName}:default"
remember(key) { globalScreenModels.getOrPut(key) { create(app = this) } as T }
}
context (S)
@Composable
inline operator fun <S : Screen, reified T : ScreenModel> ScreenModelFactory<S, T>.getValue(
thisRef: Any?,
property: Any?
) = with(app()) {
rememberScreenModel { create(context<S>()) }
}
......@@ -58,6 +58,7 @@ import net.novagamestudios.common_utils.toastLong
import net.novagamestudios.kaffeekasse.App
import net.novagamestudios.kaffeekasse.App.Companion.settings
import net.novagamestudios.kaffeekasse.BuildConfig
import net.novagamestudios.kaffeekasse.getValue
import net.novagamestudios.kaffeekasse.model.app.AppRelease
import net.novagamestudios.kaffeekasse.model.app.AppVersion
import net.novagamestudios.kaffeekasse.model.date_time.format
......@@ -66,12 +67,15 @@ import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.UpdateController
import net.novagamestudios.kaffeekasse.repositories.releases.Releases
import net.novagamestudios.kaffeekasse.ui.util.openInBrowser
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.GlobalScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelProvider
import java.time.format.DateTimeFormatter
class UpdatesScreenModel private constructor(
private val releases: Releases,
private val updateController: UpdateController
private val updateController: UpdateController,
private val gitLabProjectUrl: String
) : ScreenModel {
var showAppInfo by mutableStateOf(false)
......@@ -126,17 +130,17 @@ class UpdatesScreenModel private constructor(
}
fun openInBrowser(context: Context) {
val url = App.instanceOrNull!!.gitLab.projectUrl
context.openInBrowser(url)
context.openInBrowser(gitLabProjectUrl)
}
companion object {
companion object : GlobalScreenModelFactory<App, UpdatesScreenModel>, ScreenModelProvider<UpdatesScreenModel> {
context (RepositoryProvider)
fun create() = UpdatesScreenModel(
override fun create(app: App) = UpdatesScreenModel(
releases = releases,
updateController = updateController
updateController = updateController,
gitLabProjectUrl = app.gitLab.projectUrl
)
val model @Composable get() = App.globalScreenModel { create() }
@get:Composable override val model by this
}
}
......
......@@ -21,6 +21,8 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import io.ktor.http.Url
import net.novagamestudios.common_utils.LoggerForFun
import net.novagamestudios.common_utils.error
import net.novagamestudios.common_utils.toastShort
......@@ -90,3 +92,19 @@ fun Context.openInBrowser(url: Url) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))
startActivity(browserIntent)
}
/**
* See [https://issuetracker.google.com/issues/226665301](https://issuetracker.google.com/issues/226665301)
*/
fun removeScrollableTabRowMinimumTabWidth() = with(LoggerForFun()) {
try {
Class
.forName("androidx.compose.material3.TabRowKt")
.getDeclaredField("ScrollableTabRowMinimumTabWidth")
.apply { isAccessible = true }
.set(this, 0f)
} catch (e: Exception) {
error(e) { "Failed to remove ScrollableTabRowMinimumTabWidth" }
}
}
package net.novagamestudios.kaffeekasse.ui.util.screenmodel
import android.app.Application
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.screen.Screen
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
......@@ -7,4 +8,9 @@ import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
fun interface ScreenModelFactory<in S : Screen, out T : ScreenModel> {
context (RepositoryProvider)
fun create(screen: S): T
}
\ No newline at end of file
}
fun interface GlobalScreenModelFactory<in A : Application, out T : ScreenModel> {
context (RepositoryProvider)
fun create(app: A): T
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment