diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5940a1c3..6323ea30 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -91,10 +91,11 @@ android {
jvmTarget = "11"
freeCompilerArgs += listOf(
"-Xcontext-receivers",
- "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
- "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
-// "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${reportsDir}",
+ "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
+ "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
+ "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
+ "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${reportsDir}",
)
}
@@ -106,6 +107,10 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
+
+ lint {
+ disable += "ModifierParameter"
+ }
}
dependencies {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b4cf3355..48268971 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,20 +4,14 @@
+
-
-
-
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/ProjectHeader.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/ProjectHeader.kt
new file mode 100644
index 00000000..3f5721ba
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/ProjectHeader.kt
@@ -0,0 +1,70 @@
+package com.aliucord.manager.ui.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.aliucord.manager.BuildConfig
+import com.aliucord.manager.R
+
+@Composable
+fun ProjectHeader(modifier: Modifier = Modifier) {
+ val uriHandler = LocalUriHandler.current
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = modifier,
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_aliucord_logo),
+ contentDescription = null,
+ modifier = Modifier
+ .size(88.dp)
+ .padding(bottom = 8.dp),
+ )
+
+ Text(
+ text = stringResource(R.string.aliucord),
+ style = MaterialTheme.typography.titleMedium.copy(fontSize = 26.sp)
+ )
+
+ Text(
+ text = stringResource(R.string.app_description),
+ style = MaterialTheme.typography.titleSmall.copy(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = .6f)
+ )
+ )
+
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ TextButton(onClick = { uriHandler.openUri("https://github.com/Aliucord") }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_account_github_white_24dp),
+ contentDescription = null,
+ modifier = Modifier.padding(end = ButtonDefaults.IconSpacing),
+ )
+ Text(text = stringResource(R.string.github))
+ }
+
+ TextButton(onClick = { uriHandler.openUri("https://discord.gg/${BuildConfig.SUPPORT_SERVER}") }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_discord),
+ contentDescription = stringResource(R.string.support_server),
+ modifier = Modifier
+ .padding(end = ButtonDefaults.IconSpacing)
+ .size(22.dp),
+ )
+ Text(text = stringResource(R.string.discord))
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/SegmentedButton.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/SegmentedButton.kt
new file mode 100644
index 00000000..bb4b2385
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/SegmentedButton.kt
@@ -0,0 +1,45 @@
+package com.aliucord.manager.ui.components
+
+import androidx.compose.foundation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun RowScope.SegmentedButton(
+ icon: Painter,
+ iconColor: Color = MaterialTheme.colorScheme.primary,
+ iconDescription: String? = null,
+ text: String,
+ textColor: Color = MaterialTheme.colorScheme.primary,
+ onClick: () -> Unit,
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
+ modifier = Modifier
+ .clickable(onClick = onClick)
+ .background(MaterialTheme.colorScheme.surfaceColorAtElevation(LocalAbsoluteTonalElevation.current + 2.dp))
+ .weight(1f)
+ .padding(12.dp)
+ ) {
+ Icon(
+ painter = icon,
+ contentDescription = iconDescription,
+ tint = iconColor,
+ )
+
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelLarge,
+ color = textColor,
+ maxLines = 1,
+ modifier = Modifier.basicMarquee(),
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/home/InfoCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/home/InfoCard.kt
deleted file mode 100644
index e4201829..00000000
--- a/app/src/main/kotlin/com/aliucord/manager/ui/components/home/InfoCard.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-package com.aliucord.manager.ui.components.home
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.*
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.aliucord.manager.R
-import com.aliucord.manager.ui.util.DiscordVersion
-
-@Composable
-fun InfoCard(
- packageName: String,
- supportedVersion: DiscordVersion,
- currentVersion: DiscordVersion,
- onDownloadClick: () -> Unit,
- onUninstallClick: () -> Unit,
- onLaunchClick: () -> Unit,
-) {
- ElevatedCard {
- Column(
- modifier = Modifier
- .padding(20.dp)
- .fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text(
- text = "${stringResource(R.string.aliucord)} ($packageName)",
- style = MaterialTheme.typography.titleLarge.copy(fontSize = 23.sp),
- color = MaterialTheme.colorScheme.primary
- )
-
- Text(
- buildAnnotatedString {
- append(stringResource(R.string.version_supported))
- withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
- append(" ")
- if (supportedVersion is DiscordVersion.Existing) {
- append(supportedVersion.name)
- append(" - ")
- }
- append(supportedVersion.toDisplayName())
- }
-
- append('\n')
- append(stringResource(R.string.version_installed))
- withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
- append(" ")
- if (currentVersion is DiscordVersion.Existing) {
- append(currentVersion.name)
- append(" - ")
- }
- append(currentVersion.toDisplayName())
- }
- },
- style = MaterialTheme.typography.bodyMedium
- )
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 10.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- val (icon, description) = when {
- currentVersion !is DiscordVersion.Existing ->
- R.drawable.ic_download to R.string.action_install
-
- currentVersion < supportedVersion ->
- R.drawable.ic_refresh to R.string.action_reinstall
-
- else ->
- R.drawable.ic_update to R.string.action_update
- }
-
- FilledTonalIconButton(
- modifier = Modifier
- .weight(1f)
- .heightIn(min = 50.dp),
- onClick = onDownloadClick,
- shape = ShapeDefaults.Large,
- enabled = supportedVersion !is DiscordVersion.Error
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(5.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- painter = painterResource(icon),
- contentDescription = stringResource(description)
- )
- if (currentVersion is DiscordVersion.None) {
- Text(
- stringResource(description),
- style = MaterialTheme.typography.labelLarge
- )
- }
- }
- }
-
- if (currentVersion is DiscordVersion.Existing) {
- FilledTonalIconButton(
- modifier = Modifier
- .weight(1f)
- .heightIn(min = 50.dp),
- onClick = onUninstallClick,
- shape = ShapeDefaults.Large
- ) {
- Icon(
- imageVector = Icons.Default.Delete,
- contentDescription = stringResource(R.string.action_uninstall)
- )
- }
-
- FilledTonalIconButton(
- modifier = Modifier
- .weight(1f)
- .heightIn(min = 50.dp),
- onClick = onLaunchClick,
- shape = ShapeDefaults.Large
- ) {
- Icon(
- painter = painterResource(R.drawable.ic_launch),
- contentDescription = stringResource(R.string.action_launch)
- )
- }
- }
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt
index 567fdbbc..7d84cd3b 100644
--- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/about/AboutScreen.kt
@@ -19,7 +19,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -47,6 +46,7 @@ class AboutScreen : Screen {
}
) { paddingValues ->
LazyColumn(
+ horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = paddingValues
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top),
modifier = Modifier
@@ -68,7 +68,7 @@ class AboutScreen : Screen {
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
- .padding(start = 16.dp, top = 12.dp, bottom = 16.dp)
+ .padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
)
}
@@ -101,61 +101,6 @@ class AboutScreen : Screen {
}
}
-@Composable
-private fun ProjectHeader(modifier: Modifier = Modifier) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = modifier
- .fillMaxWidth()
- .padding(vertical = 16.dp)
- ) {
- AsyncImage(
- model = "https://github.com/Aliucord.png",
- contentDescription = stringResource(R.string.aliucord),
- modifier = Modifier.size(71.dp)
- )
-
- Text(
- text = stringResource(R.string.aliucord),
- style = MaterialTheme.typography.titleMedium.copy(
- fontSize = 26.sp
- )
- )
-
- Text(
- text = stringResource(R.string.app_description),
- style = MaterialTheme.typography.titleSmall.copy(
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- )
- )
-
- Row(
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxWidth()
- ) {
- val uriHandler = LocalUriHandler.current
-
- TextButton(onClick = { uriHandler.openUri("https://github.com/Aliucord") }) {
- Icon(
- painter = painterResource(R.drawable.ic_account_github_white_24dp),
- contentDescription = stringResource(R.string.github)
- )
- Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
- Text(text = stringResource(id = R.string.github))
- }
-
- TextButton(onClick = { uriHandler.openUri("https://aliucord.com") }) {
- Icon(
- painter = painterResource(R.drawable.ic_link),
- contentDescription = stringResource(R.string.website)
- )
- Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
- Text(text = stringResource(id = R.string.website))
- }
- }
- }
-}
-
@Composable
private fun MainContributors(modifier: Modifier = Modifier) {
Row(
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt
index 003a4283..a3f1827f 100644
--- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt
@@ -1,110 +1,125 @@
package com.aliucord.manager.ui.screens.home
import android.app.Application
+import android.content.Intent
import android.content.pm.PackageManager
+import android.provider.Settings
import android.util.Log
import androidx.compose.runtime.*
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+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 com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.domain.repository.GithubRepository
-import com.aliucord.manager.installer.util.uninstallApk
-import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.network.utils.fold
import com.aliucord.manager.ui.util.DiscordVersion
-import com.aliucord.manager.util.getPackageVersion
+import com.aliucord.manager.util.launchBlock
import com.aliucord.manager.util.showToast
-import kotlinx.coroutines.*
+import kotlinx.collections.immutable.toImmutableList
class HomeModel(
private val application: Application,
private val github: GithubRepository,
- val preferences: PreferencesManager,
) : ScreenModel {
var supportedVersion by mutableStateOf(DiscordVersion.None)
private set
- var installedVersion by mutableStateOf(DiscordVersion.None)
+ var installations by mutableStateOf(InstallsState.Fetching)
private set
init {
- screenModelScope.launch(Dispatchers.IO) {
- _fetchInstalledVersion()
- _fetchSupportedVersion()
- }
+ fetchInstallations()
+ fetchSupportedVersion()
}
- private suspend fun _fetchInstalledVersion() {
- try {
- val (versionName, versionCode) = application.getPackageVersion(preferences.packageName)
-
- withContext(Dispatchers.Main) {
- installedVersion = DiscordVersion.Existing(
- type = DiscordVersion.parseVersionType(versionCode),
- name = versionName.split("-")[0].trim(),
- code = versionCode,
- )
- }
- } catch (t: PackageManager.NameNotFoundException) {
- withContext(Dispatchers.Main) {
- installedVersion = DiscordVersion.None
- }
- } catch (t: Throwable) {
- Log.e(BuildConfig.TAG, Log.getStackTraceString(t))
+ fun launchApp(packageName: String) {
+ val launchIntent = application.packageManager
+ .getLaunchIntentForPackage(packageName)
- withContext(Dispatchers.Main) {
- installedVersion = DiscordVersion.Error
- }
+ if (launchIntent != null) {
+ application.startActivity(launchIntent)
+ } else {
+ application.showToast(R.string.launch_aliucord_fail)
}
}
- private suspend fun _fetchSupportedVersion() {
- val version = github.getDataJson()
+ fun openAppInfo(packageName: String) {
+ val launchIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .setData("package:$packageName".toUri())
- withContext(Dispatchers.Main) {
- version.fold(
- success = {
- val versionCode = it.versionCode.toIntOrNull() ?: return@fold
+ application.startActivity(launchIntent)
+ }
- supportedVersion = DiscordVersion.Existing(
- type = DiscordVersion.parseVersionType(versionCode),
- name = it.versionName.split("-")[0].trim(),
- code = versionCode,
- )
- },
- fail = {
- Log.e(BuildConfig.TAG, Log.getStackTraceString(it))
- supportedVersion = DiscordVersion.Error
+ private fun fetchInstallations() = screenModelScope.launchBlock {
+ try {
+ val packageManager = application.packageManager
+
+ val installedPackages = packageManager
+ .getInstalledPackages(PackageManager.GET_META_DATA)
+ .takeIf { it.isNotEmpty() }
+ ?: throw IllegalStateException("Failed to fetch installed packages (returned none)")
+
+ val aliucordPackages = installedPackages
+ .asSequence()
+ .filter {
+ val isAliucordPkg = it.packageName == "com.aliucord"
+ val hasAliucordMeta = it.applicationInfo.metaData?.containsKey("isAliucord") == true
+ isAliucordPkg || hasAliucordMeta
}
- )
- }
- }
- fun fetchInstalledVersion() {
- screenModelScope.launch(Dispatchers.IO) {
- _fetchInstalledVersion()
- }
- }
+ val aliucordInstallations = aliucordPackages
+ .map {
+ // `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor`
+ @Suppress("DEPRECATION")
+ val versionCode = it.versionCode
- fun fetchSupportedVersion() {
- screenModelScope.launch(Dispatchers.IO) {
- _fetchSupportedVersion()
- }
- }
+ val baseVersion = it.applicationInfo.metaData?.getInt("aliucordBaseVersion")
+ val isBaseUpdated = /* TODO: remote data json instead */ baseVersion == 0
- fun launchAliucord() {
- val launchIntent = application.packageManager
- .getLaunchIntentForPackage(preferences.packageName)
+ InstallData(
+ name = packageManager.getApplicationLabel(it.applicationInfo).toString(),
+ packageName = it.packageName,
+ baseUpdated = isBaseUpdated,
+ icon = packageManager
+ .getApplicationIcon(it.applicationInfo)
+ .toBitmap()
+ .asImageBitmap()
+ .let(::BitmapPainter),
+ version = DiscordVersion.Existing(
+ type = DiscordVersion.parseVersionType(versionCode),
+ name = it.versionName.split("-")[0].trim(),
+ code = versionCode,
+ ),
+ )
+ }
- if (launchIntent != null) {
- application.startActivity(launchIntent)
- } else {
- application.showToast(R.string.launch_aliucord_fail)
+ installations = InstallsState.Fetched(data = aliucordInstallations.toImmutableList())
+ } catch (t: Throwable) {
+ Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t)
+ installations = InstallsState.Error
}
}
- fun uninstallAliucord() {
- application.uninstallApk(preferences.packageName)
+ private fun fetchSupportedVersion() = screenModelScope.launchBlock {
+ github.getDataJson().fold(
+ success = {
+ val versionCode = it.versionCode.toIntOrNull() ?: return@fold
+
+ supportedVersion = DiscordVersion.Existing(
+ type = DiscordVersion.parseVersionType(versionCode),
+ name = it.versionName.split("-")[0].trim(),
+ code = versionCode,
+ )
+ },
+ fail = {
+ Log.e(BuildConfig.TAG, Log.getStackTraceString(it))
+ supportedVersion = DiscordVersion.Error
+ }
+ )
}
}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt
index eca80bc9..d0142a10 100644
--- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt
@@ -6,28 +6,33 @@
package com.aliucord.manager.ui.screens.home
+import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Info
-import androidx.compose.material3.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
-import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
+import com.aliucord.manager.ui.components.ProjectHeader
import com.aliucord.manager.ui.components.dialogs.InstallerDialog
-import com.aliucord.manager.ui.components.home.InfoCard
-import com.aliucord.manager.ui.screens.about.AboutScreen
+import com.aliucord.manager.ui.screens.home.components.*
import com.aliucord.manager.ui.screens.install.InstallScreen
import com.aliucord.manager.ui.screens.plugins.PluginsScreen
-import com.aliucord.manager.ui.screens.settings.SettingsScreen
+import com.aliucord.manager.ui.util.DiscordVersion
+import com.aliucord.manager.ui.util.paddings.PaddingValuesSides
+import com.aliucord.manager.ui.util.paddings.exclude
class HomeScreen : Screen {
override val key = "Home"
@@ -38,11 +43,6 @@ class HomeScreen : Screen {
val model = getScreenModel()
var showInstallerDialog by remember { mutableStateOf(false) }
-
- LaunchedEffect(model.preferences.packageName) {
- model.fetchInstalledVersion()
- }
-
if (showInstallerDialog) {
InstallerDialog(
onDismiss = { showInstallerDialog = false },
@@ -56,66 +56,72 @@ class HomeScreen : Screen {
Scaffold(
topBar = { HomeAppBar() },
) { paddingValues ->
- Column(
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ contentPadding = paddingValues
+ .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top),
modifier = Modifier
.fillMaxSize()
- .padding(paddingValues)
- .padding(horizontal = 16.dp)
- .padding(top = 8.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ .padding(paddingValues.exclude(PaddingValuesSides.Bottom))
+ .padding(top = 16.dp, start = 16.dp, end = 16.dp),
) {
- InfoCard(
- packageName = model.preferences.packageName,
- supportedVersion = model.supportedVersion,
- currentVersion = model.installedVersion,
- onDownloadClick = { showInstallerDialog = true },
- onLaunchClick = model::launchAliucord,
- onUninstallClick = model::uninstallAliucord
- )
- }
- }
- }
-}
-
-@Composable
-private fun HomeAppBar() {
- TopAppBar(
- title = { Text(stringResource(R.string.navigation_home)) },
- actions = {
- val uriHandler = LocalUriHandler.current
- val navigator = LocalNavigator.currentOrThrow
+ item(key = "PROJECT_HEADER") {
+ ProjectHeader()
+ }
- IconButton(
- onClick = {
- uriHandler.openUri("https://discord.gg/${BuildConfig.SUPPORT_SERVER}")
+ item(key = "ADD_INSTALL_BUTTON") {
+ InstallButton(
+ // TODO: install options screen to configure pkg name
+ enabled = (model.installations as? InstallsState.Fetched)?.data?.isEmpty() ?: false,
+ onClick = { showInstallerDialog = true },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 10.dp)
+ )
}
- ) {
- Icon(
- painter = painterResource(R.drawable.ic_discord),
- contentDescription = stringResource(R.string.support_server)
- )
- }
- IconButton(onClick = { navigator.push(AboutScreen()) }) {
- Icon(
- imageVector = Icons.Default.Info,
- contentDescription = stringResource(R.string.navigation_about)
- )
- }
+ item(key = "SUPPORTED_VERSION") {
+ AnimatedVisibility(
+ enter = fadeIn() + slideInVertically { it * -2 },
+ exit = fadeOut() + slideOutVertically { it * -2 },
+ visible = model.supportedVersion !is DiscordVersion.None,
+ ) {
+ VersionDisplay(
+ version = model.supportedVersion,
+ prefix = {
+ withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
+ append(stringResource(R.string.version_supported))
+ append(" ")
+ }
+ },
+ modifier = Modifier
+ .alpha(.5f)
+ .padding(bottom = 22.dp),
+ )
+ }
+ }
- IconButton(onClick = { navigator.push(PluginsScreen()) }) {
- Icon(
- painter = painterResource(R.drawable.ic_extension),
- contentDescription = stringResource(R.string.navigation_about)
- )
- }
+ val installations = (model.installations as? InstallsState.Fetched)?.data
+ ?: return@LazyColumn
- IconButton(onClick = { navigator.push(SettingsScreen()) }) {
- Icon(
- painter = painterResource(R.drawable.ic_settings),
- contentDescription = stringResource(R.string.navigation_settings)
- )
+ items(installations, key = { it.packageName }) {
+ AnimatedVisibility(
+ enter = fadeIn() + slideInHorizontally { it * -2 },
+ exit = fadeOut() + slideOutHorizontally { it * 2 },
+ visible = model.supportedVersion !is DiscordVersion.None,
+ ) {
+ InstalledItemCard(
+ data = it,
+ onUpdate = { showInstallerDialog = true }, // TODO: prefilled install options screen
+ onOpenApp = { model.launchApp(it.packageName) },
+ onOpenInfo = { model.openAppInfo(it.packageName) },
+ onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
}
}
- )
+ }
}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt
new file mode 100644
index 00000000..2fd30896
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt
@@ -0,0 +1,14 @@
+package com.aliucord.manager.ui.screens.home
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import com.aliucord.manager.ui.util.DiscordVersion
+
+@Immutable
+data class InstallData(
+ val name: String,
+ val packageName: String,
+ val version: DiscordVersion,
+ val icon: BitmapPainter,
+ val baseUpdated: Boolean,
+)
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallsState.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallsState.kt
new file mode 100644
index 00000000..3441f3d7
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallsState.kt
@@ -0,0 +1,12 @@
+package com.aliucord.manager.ui.screens.home
+
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
+sealed interface InstallsState {
+ data object None : InstallsState
+ data object Error : InstallsState
+ data object Fetching : InstallsState
+ data class Fetched(val data: ImmutableList) : InstallsState
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/HomeAppBar.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/HomeAppBar.kt
new file mode 100644
index 00000000..e5f9b00b
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/HomeAppBar.kt
@@ -0,0 +1,37 @@
+package com.aliucord.manager.ui.screens.home.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material3.*
+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
+
+@Composable
+fun HomeAppBar() {
+ TopAppBar(
+ title = {},
+ actions = {
+ val navigator = LocalNavigator.currentOrThrow
+
+ IconButton(onClick = { navigator.push(AboutScreen()) }) {
+ Icon(
+ imageVector = Icons.Default.Info,
+ contentDescription = stringResource(R.string.navigation_about)
+ )
+ }
+
+ IconButton(onClick = { navigator.push(SettingsScreen()) }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_settings),
+ contentDescription = stringResource(R.string.navigation_settings)
+ )
+ }
+ }
+ )
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt
new file mode 100644
index 00000000..eb8ed1c9
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt
@@ -0,0 +1,40 @@
+package com.aliucord.manager.ui.screens.home.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.aliucord.manager.R
+
+@Composable
+fun InstallButton(
+ enabled: Boolean = true,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ FilledTonalIconButton(
+ shape = MaterialTheme.shapes.medium,
+ enabled = enabled,
+ onClick = onClick,
+ modifier = modifier,
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_add),
+ contentDescription = null,
+ )
+ Text(
+ text = stringResource(R.string.action_add_install),
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt
new file mode 100644
index 00000000..417a2b5e
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt
@@ -0,0 +1,158 @@
+package com.aliucord.manager.ui.screens.home.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.*
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.*
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.aliucord.manager.R
+import com.aliucord.manager.ui.components.SegmentedButton
+import com.aliucord.manager.ui.screens.home.InstallData
+
+@Composable
+fun InstalledItemCard(
+ data: InstallData,
+ onUpdate: () -> Unit,
+ onOpenApp: () -> Unit,
+ onOpenInfo: () -> Unit,
+ onOpenPlugins: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ElevatedCard(
+ shape = MaterialTheme.shapes.medium,
+ elevation = CardDefaults.elevatedCardElevation(
+ defaultElevation = 3.dp,
+ ),
+ modifier = modifier
+ .width(IntrinsicSize.Max)
+ .shadow(
+ clip = false,
+ elevation = 2.dp,
+ shape = MaterialTheme.shapes.medium,
+ )
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ modifier = Modifier.padding(20.dp),
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Image(
+ painter = data.icon,
+ contentDescription = null,
+ modifier = Modifier
+ .size(34.dp)
+ .clip(CircleShape),
+ )
+
+ Column {
+ Text(
+ text = "\"${data.name}\"",
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .94f),
+ )
+
+ Text(
+ text = data.packageName,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier
+ .padding(start = 1.dp)
+ .offset(y = (-2).dp)
+ .alpha(.7f)
+ .basicMarquee(),
+ )
+ }
+
+ Spacer(Modifier.weight(1f, fill = true))
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ horizontalAlignment = Alignment.End,
+ modifier = Modifier
+ .alpha(.6f)
+ .padding(end = 4.dp),
+ ) {
+ VersionDisplay(
+ version = data.version,
+ prefix = { append("v") },
+ )
+
+ // TODO: display install core commit version
+ // Text(
+ // text = data.commit,
+ // style = MaterialTheme.typography.labelLarge,
+ // )
+ }
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(2.dp),
+ modifier = Modifier.clip(MaterialTheme.shapes.large),
+ ) {
+ SegmentedButton(
+ icon = painterResource(R.drawable.ic_extension),
+ text = stringResource(R.string.plugins_title),
+ onClick = onOpenPlugins,
+ )
+ SegmentedButton(
+ icon = painterResource(R.drawable.ic_info),
+ text = stringResource(R.string.action_open_info),
+ onClick = onOpenInfo,
+ )
+
+ if (data.baseUpdated) {
+ SegmentedButton(
+ icon = painterResource(R.drawable.ic_launch),
+ text = stringResource(R.string.action_launch),
+ onClick = onOpenApp,
+ )
+ } else {
+ val warningColor = Color(0xFFFFBB33)
+
+ SegmentedButton(
+ icon = painterResource(R.drawable.ic_update),
+ text = stringResource(R.string.action_update),
+ iconColor = warningColor,
+ textColor = warningColor,
+ onClick = onUpdate,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun LabelTextItem(
+ label: String,
+ value: String,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ text = buildAnnotatedString {
+ withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
+ append("• ")
+ append(label)
+ }
+ append(" ")
+ append(value)
+ },
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .9f),
+ modifier = modifier,
+ )
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/VersionDisplay.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/VersionDisplay.kt
new file mode 100644
index 00000000..b532f08f
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/VersionDisplay.kt
@@ -0,0 +1,30 @@
+package com.aliucord.manager.ui.screens.home.components
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.*
+import com.aliucord.manager.ui.util.DiscordVersion
+
+@Composable
+fun VersionDisplay(
+ version: DiscordVersion,
+ prefix: (@Composable AnnotatedString.Builder.() -> Unit)? = null,
+ style: TextStyle = MaterialTheme.typography.labelLarge,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ text = buildAnnotatedString {
+ prefix?.invoke(this)
+
+ if (version is DiscordVersion.Existing) {
+ append(version.name)
+ append(" - ")
+ }
+ append(version.toDisplayName())
+ },
+ style = style,
+ modifier = modifier,
+ )
+}
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt b/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt
index 4b0ac5f0..51d6f02d 100644
--- a/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/util/DiscordVersion.kt
@@ -1,12 +1,14 @@
package com.aliucord.manager.ui.util
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
+@Immutable
sealed interface DiscordVersion : Comparable {
- object Error : DiscordVersion
- object None : DiscordVersion
+ data object Error : DiscordVersion
+ data object None : DiscordVersion
data class Existing(
val type: Type,
diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Coroutines.kt b/app/src/main/kotlin/com/aliucord/manager/util/Coroutines.kt
new file mode 100644
index 00000000..d7d9092b
--- /dev/null
+++ b/app/src/main/kotlin/com/aliucord/manager/util/Coroutines.kt
@@ -0,0 +1,22 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.aliucord.manager.util
+
+import kotlinx.coroutines.*
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Launch a Job for a block without returning anything
+ */
+inline fun CoroutineScope.launchBlock(
+ context: CoroutineContext = Dispatchers.Main,
+ noinline block: suspend CoroutineScope.() -> Unit,
+) {
+ launch(context, block = block)
+}
+
+/**
+ * Wrapper util to run a block with the main thread context
+ */
+suspend inline fun mainThread(noinline block: CoroutineScope.() -> Unit) =
+ withContext(Dispatchers.Main, block)
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 00000000..bf12e7ff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_aliucord_logo.xml b/app/src/main/res/drawable/ic_aliucord_logo.xml
new file mode 100644
index 00000000..ce5b8adf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_aliucord_logo.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 00000000..aa33ecba
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7fdb02c6..8f26dd71 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,6 +3,7 @@
A modification for the Discord Android App
Aliucord
+ Discord
GitHub
Support Server
Commits
@@ -15,6 +16,7 @@
Confirm
Dismiss
Install
+ Add Installation
Reinstall
Update
Clear
@@ -29,6 +31,7 @@
Copied!
Cleared cache!
Exit anyways
+ Open info
Grant Permissions
In order for Aliucord Manager to function, file permissions are required. Since shared data is stored in ~/Aliucord, permissions are required in order to access it.
@@ -78,6 +81,9 @@
Failed to load
+ Package:
+ Discord version:
+
Supported version:
Installed version:
None