feat: home screen compose previews

This commit is contained in:
rushiiMachine 2025-03-28 15:41:44 -07:00
parent fdf3470973
commit bb876526d0
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
7 changed files with 237 additions and 47 deletions

View file

@ -0,0 +1,22 @@
package com.aliucord.manager.ui.previews.screens.home
import android.content.res.Configuration
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.components.ManagerTheme
import com.aliucord.manager.ui.screens.home.HomeScreenFailureContent
import com.aliucord.manager.ui.screens.home.components.HomeAppBar
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun HomeScreenFailedPreview() {
ManagerTheme {
Scaffold(
topBar = { HomeAppBar() },
) { padding ->
HomeScreenFailureContent(padding = padding)
}
}
}

View file

@ -0,0 +1,89 @@
package com.aliucord.manager.ui.previews.screens.home
import android.content.res.Configuration
import android.graphics.BitmapFactory
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.tooling.preview.*
import com.aliucord.manager.ui.components.ManagerTheme
import com.aliucord.manager.ui.screens.home.*
import com.aliucord.manager.ui.screens.home.components.HomeAppBar
import com.aliucord.manager.ui.util.DiscordVersion
import io.ktor.util.decodeBase64Bytes
import kotlinx.collections.immutable.persistentListOf
// This preview has animations that cannot be properly viewed from an IDE
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun HomeScreenLoadedPreview(
@PreviewParameter(TextBannerParametersProvider::class)
parameters: HomeScreenParameters,
) {
ManagerTheme {
Scaffold(
topBar = { HomeAppBar() },
) { padding ->
HomeScreenLoadedContent(
state = parameters.state,
supportedVersion = parameters.supportedVersion,
padding = padding,
onClickInstall = {},
onUpdate = {},
onOpenApp = {},
onOpenAppInfo = {},
onOpenPlugins = {},
)
}
}
}
private data class HomeScreenParameters(
val state: InstallsState.Fetched,
val supportedVersion: DiscordVersion,
)
private class TextBannerParametersProvider : PreviewParameterProvider<HomeScreenParameters> {
private val stableVersion = DiscordVersion.Existing(DiscordVersion.Type.STABLE, "126.21", 126021)
private val aliucordIconBytes =
"/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAKBueIx4ZKCMgoy0qqC+8P//8Nzc8P//////////////////////////////////////////////////////////2wBDAaq0tPDS8P//////////////////////////////////////////////////////////////////////////////wAARCAC9AL0DASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDAf/EACIQAQEAAgEFAAIDAAAAAAAAAAABAhEhAxIxQVETMiJhcf/EABcBAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAZEQEBAQEBAQAAAAAAAAAAAAAAARECEjH/2gAMAwEAAhEDEQA/AMsce7/Gkkngk1NOoxboAIAAAAAAAAAAAAAAAAAAm4ys7NXVbJyx2LKoAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZLfRcbJug4AABJugDtxs9OAAAAAAAAAAAAAA7jN0HccN81cxk9OityBZuaAVP459OyKBMifxwxw1dqAyBrYCpuEvhFmq1TnNwZsZgIyAAAAAAAANcJqM8ZutRrkAVoAAAAAAAAABnnNVLTObjNGKACAAAAAAL6c9rcxmsY6rcALdTYrlyk8kzlZeRGPTYThdzSlbC2TyMsruiW4vvimK+nedIkqwFaGN4rZnnP5DPSQEZAAAAAJ5BtPACug5n+tdAYi8sL6cmFqOeO9P2sk1NCtwYtk5Yb5gljNWH7Odt+NMce2IkjoCtiOp6WjqeIJfiAEYAAAACeYANgllnAroA5lbJxAdEzOe+FblDQAAAAcuUntyZW3iCaoAUT1PEUjOy8CX4gBGAAAAAACWzw0mf1mBLjYZS2eFTqfVa9O9TWv7Zu27u3EZtd7r9O/L64Brvdfrm79AHZ55asVY56mhZWjlykRc7Ui3pWWdqQGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHMbuOsZbLw0xy2LYoAQAAAAAAAAAAAAAAAAAARnlzqGWdnEQNSP/2Q=="
.decodeBase64Bytes()
private val aliucordIcon = BitmapFactory
.decodeByteArray(aliucordIconBytes, 0, aliucordIconBytes.size)
.asImageBitmap()
.let(::BitmapPainter)
override val values = sequenceOf(
HomeScreenParameters(
supportedVersion = stableVersion,
state = InstallsState.Fetched(
persistentListOf(
InstallData(
name = "Aliucord",
packageName = "com.aliucord",
version = stableVersion,
icon = aliucordIcon,
isUpToDate = true,
)
)
),
),
HomeScreenParameters(
supportedVersion = stableVersion,
state = InstallsState.Fetched(
persistentListOf(
InstallData(
name = "Discord",
packageName = "com.discord",
version = stableVersion,
icon = aliucordIcon,
isUpToDate = false,
)
)
),
),
)
}

View file

@ -0,0 +1,24 @@
package com.aliucord.manager.ui.previews.screens.home
import android.content.res.Configuration
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.components.ManagerTheme
import com.aliucord.manager.ui.screens.home.HomeScreenLoadingContent
import com.aliucord.manager.ui.screens.home.components.HomeAppBar
// This preview cannot be properly viewed from an IDE
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun HomeScreenLoadingPreview() {
ManagerTheme {
Scaffold(
topBar = { HomeAppBar() },
) { padding ->
HomeScreenLoadingContent(padding = padding)
}
}
}

View file

@ -0,0 +1,25 @@
package com.aliucord.manager.ui.previews.screens.home
import android.content.res.Configuration
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.components.ManagerTheme
import com.aliucord.manager.ui.screens.home.HomeScreenNoneContent
import com.aliucord.manager.ui.screens.home.components.HomeAppBar
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun HomeScreenNonePreview() {
ManagerTheme {
Scaffold(
topBar = { HomeAppBar() },
) { padding ->
HomeScreenNoneContent(
padding = padding,
onClickInstall = {},
)
}
}
}

View file

@ -13,7 +13,6 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.network.models.BuildInfo
@ -43,7 +42,7 @@ class HomeModel(
var supportedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None)
private set
var installations by mutableStateOf<InstallsState>(InstallsState.Fetching)
var state by mutableStateOf<InstallsState>(InstallsState.Fetching)
private set
private var remoteDataJson: BuildInfo? = null
@ -58,7 +57,7 @@ class HomeModel(
}
}
fun launchApp(packageName: String) {
fun openApp(packageName: String) {
val launchIntent = application.packageManager
.getLaunchIntentForPackage(packageName)
@ -78,7 +77,7 @@ class HomeModel(
}
fun fetchInstallations() = screenModelScope.launchBlock {
installations = InstallsState.Fetching
state = InstallsState.Fetching
try {
val packageManager = application.packageManager
@ -120,39 +119,40 @@ class HomeModel(
)
}.toImmutableList()
installations = if (aliucordInstallations.isNotEmpty()) {
state = if (aliucordInstallations.isNotEmpty()) {
InstallsState.Fetched(data = aliucordInstallations)
} else {
InstallsState.None
}
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t)
installations = InstallsState.Error
state = InstallsState.Error
}
}
// Launches the patching screen with prefilled options according to the existing APK
fun updateAliucord(data: InstallData, navigator: Navigator) = screenModelScope.launchBlock {
/**
* Creates a [PatchOptionsScreen] that can be navigated to,
* with prefilled options from an existing installation.
*/
fun createPrefilledPatchOptsScreen(packageName: String): PatchOptionsScreen {
val metadata = try {
val applicationInfo = application.packageManager.getApplicationInfo(data.packageName, 0)
val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0)
val metadataFile = ZipReader(applicationInfo.publicSourceDir)
.use { it.openEntry("aliucord.json")?.read() }
@OptIn(ExperimentalSerializationApi::class)
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to parse Aliucord install metadata from package ${data.packageName}", t)
Log.w(BuildConfig.TAG, "Failed to parse Aliucord install metadata from package ${packageName}", t)
null
}
val patchOptions = metadata?.options
?: PatchOptions.Default.copy(packageName = data.packageName)
?: PatchOptions.Default.copy(packageName = packageName)
navigator.push(
PatchOptionsScreen(
prefilledOptions = patchOptions,
supportedVersion = supportedVersion,
)
return PatchOptionsScreen(
prefilledOptions = patchOptions,
supportedVersion = supportedVersion,
)
}

View file

@ -8,11 +8,13 @@ package com.aliucord.manager.ui.screens.home
import android.os.Parcelable
import androidx.compose.animation.*
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -32,6 +34,7 @@ import com.aliucord.manager.ui.screens.plugins.PluginsScreen
import com.aliucord.manager.ui.util.DiscordVersion
import com.aliucord.manager.ui.util.paddings.PaddingValuesSides
import com.aliucord.manager.ui.util.paddings.exclude
import kotlinx.coroutines.launch
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@ -43,7 +46,9 @@ class HomeScreen : Screen, Parcelable {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val model = getScreenModel<HomeModel>()
val onClickInstall = remember { { navigator.push(PatchOptionsScreen(supportedVersion = model.supportedVersion)) } }
// Refresh installations list when the screen changes or activity resumes
LifecycleResumeEffect(Unit) {
@ -55,29 +60,37 @@ class HomeScreen : Screen, Parcelable {
Scaffold(
topBar = { HomeAppBar() },
) { padding ->
when (model.installations) {
is InstallsState.Fetched -> LoadedContent(
model = model,
when (val state = model.state) {
is InstallsState.Fetched -> HomeScreenLoadedContent(
state = state,
supportedVersion = model.supportedVersion,
padding = padding,
onClickInstall = { navigator.push(PatchOptionsScreen(supportedVersion = model.supportedVersion)) },
onClickInstall = onClickInstall,
onUpdate = {
scope.launch {
navigator.push(model.createPrefilledPatchOptsScreen(it))
}
},
onOpenApp = model::openApp,
onOpenAppInfo = model::openAppInfo,
onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
)
InstallsState.Fetching -> LoadingContent(padding = padding)
InstallsState.Fetching -> HomeScreenLoadingContent(padding = padding)
InstallsState.None -> NoInstallsContent(
onClickInstall = { navigator.push(PatchOptionsScreen(supportedVersion = model.supportedVersion)) },
modifier = Modifier
.padding(padding.exclude(PaddingValuesSides.Bottom)),
InstallsState.None -> HomeScreenNoneContent(
padding = padding,
onClickInstall = onClickInstall,
)
InstallsState.Error -> FailureContent(padding = padding)
InstallsState.Error -> HomeScreenFailureContent(padding = padding)
}
}
}
}
@Composable
private fun LoadingContent(padding: PaddingValues) {
fun HomeScreenLoadingContent(padding: PaddingValues) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
@ -86,14 +99,32 @@ private fun LoadingContent(padding: PaddingValues) {
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
ProjectHeader()
AnimatedVisibility(
visibleState = remember { MutableTransitionState(false) }.apply { targetState = true },
enter = fadeIn(animationSpec = tween(durationMillis = 800)),
exit = ExitTransition.None,
) {
Box(
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() },
modifier = Modifier
.fillMaxSize(),
)
}
}
}
@Composable
private fun LoadedContent(
model: HomeModel,
fun HomeScreenLoadedContent(
state: InstallsState.Fetched,
supportedVersion: DiscordVersion,
padding: PaddingValues,
onClickInstall: () -> Unit,
onUpdate: (packageName: String) -> Unit,
onOpenApp: (packageName: String) -> Unit,
onOpenAppInfo: (packageName: String) -> Unit,
onOpenPlugins: (packageName: String) -> Unit,
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(6.dp),
@ -122,28 +153,26 @@ private fun LoadedContent(
item(key = "SUPPORTED_VERSION") {
AnimatedVersionDisplay(
version = model.supportedVersion,
version = supportedVersion,
modifier = Modifier.padding(bottom = 30.dp),
)
}
val installations = (model.installations as? InstallsState.Fetched)?.data
val installations = (state as? InstallsState.Fetched)?.data
?: return@LazyColumn
items(installations, key = { it.packageName }) {
AnimatedVisibility(
enter = fadeIn() + slideInHorizontally { it * -2 },
exit = fadeOut() + slideOutHorizontally { it * 2 },
visible = model.supportedVersion !is DiscordVersion.None,
visible = supportedVersion !is DiscordVersion.None,
) {
val navigator = LocalNavigator.currentOrThrow
InstalledItemCard(
data = it,
onUpdate = { model.updateAliucord(it, navigator) },
onOpenApp = { model.launchApp(it.packageName) },
onOpenInfo = { model.openAppInfo(it.packageName) },
onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
onUpdate = { onUpdate(it.packageName) },
onOpenApp = { onOpenApp(it.packageName) },
onOpenInfo = { onOpenAppInfo(it.packageName) },
onOpenPlugins = { onOpenPlugins(it.packageName) },
modifier = Modifier.fillMaxWidth()
)
}
@ -152,7 +181,8 @@ private fun LoadedContent(
}
@Composable
private fun NoInstallsContent(
fun HomeScreenNoneContent(
padding: PaddingValues,
onClickInstall: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -160,8 +190,9 @@ private fun NoInstallsContent(
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxSize()
.padding(16.dp),
.padding(padding)
.padding(16.dp)
.fillMaxSize(),
) {
ProjectHeader()
@ -198,7 +229,7 @@ private fun NoInstallsContent(
}
@Composable
fun FailureContent(
fun HomeScreenFailureContent(
padding: PaddingValues,
) {
Column(

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.aliucord.manager.R
import com.aliucord.manager.ui.screens.about.AboutScreen
import com.aliucord.manager.ui.screens.settings.SettingsScreen
@ -15,16 +14,16 @@ fun HomeAppBar() {
TopAppBar(
title = {},
actions = {
val navigator = LocalNavigator.currentOrThrow
val navigator = LocalNavigator.current
IconButton(onClick = { navigator.push(AboutScreen()) }) {
IconButton(onClick = { navigator?.push(AboutScreen()) }) {
Icon(
painter = painterResource(R.drawable.ic_info),
contentDescription = stringResource(R.string.navigation_about)
)
}
IconButton(onClick = { navigator.push(SettingsScreen()) }) {
IconButton(onClick = { navigator?.push(SettingsScreen()) }) {
Icon(
painter = painterResource(R.drawable.ic_settings),
contentDescription = stringResource(R.string.navigation_settings)