feat: new home page (#65)

This commit is contained in:
rushii 2024-02-07 10:49:27 -08:00 • committed by GitHub
parent d4c575f496
commit 15464fabe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 646 additions and 350 deletions

View file

@ -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 {

View file

@ -4,20 +4,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.REQUEST_DELETE_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.DELETE_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />

View file

@ -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))
}
}
}
}

View file

@ -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(),
)
}
}

View file

@ -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)
)
}
}
}
}
}
}

View file

@ -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(

View file

@ -1,101 +1,44 @@
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>(DiscordVersion.None)
private set
var installedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None)
var installations by mutableStateOf<InstallsState>(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))
withContext(Dispatchers.Main) {
installedVersion = DiscordVersion.Error
}
}
}
private suspend fun _fetchSupportedVersion() {
val version = github.getDataJson()
withContext(Dispatchers.Main) {
version.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
}
)
}
}
fun fetchInstalledVersion() {
screenModelScope.launch(Dispatchers.IO) {
_fetchInstalledVersion()
}
}
fun fetchSupportedVersion() {
screenModelScope.launch(Dispatchers.IO) {
_fetchSupportedVersion()
}
}
fun launchAliucord() {
fun launchApp(packageName: String) {
val launchIntent = application.packageManager
.getLaunchIntentForPackage(preferences.packageName)
.getLaunchIntentForPackage(packageName)
if (launchIntent != null) {
application.startActivity(launchIntent)
@ -104,7 +47,79 @@ class HomeModel(
}
}
fun uninstallAliucord() {
application.uninstallApk(preferences.packageName)
fun openAppInfo(packageName: String) {
val launchIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData("package:$packageName".toUri())
application.startActivity(launchIntent)
}
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
}
val aliucordInstallations = aliucordPackages
.map {
// `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor`
@Suppress("DEPRECATION")
val versionCode = it.versionCode
val baseVersion = it.applicationInfo.metaData?.getInt("aliucordBaseVersion")
val isBaseUpdated = /* TODO: remote data json instead */ baseVersion == 0
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,
),
)
}
installations = InstallsState.Fetched(data = aliucordInstallations.toImmutableList())
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t)
installations = InstallsState.Error
}
}
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
}
)
}
}

View file

@ -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<HomeModel>()
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
)
item(key = "PROJECT_HEADER") {
ProjectHeader()
}
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)
)
}
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),
)
}
}
val installations = (model.installations 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,
) {
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()
)
}
}
}
}
}
}
@Composable
private fun HomeAppBar() {
TopAppBar(
title = { Text(stringResource(R.string.navigation_home)) },
actions = {
val uriHandler = LocalUriHandler.current
val navigator = LocalNavigator.currentOrThrow
IconButton(
onClick = {
uriHandler.openUri("https://discord.gg/${BuildConfig.SUPPORT_SERVER}")
}
) {
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)
)
}
IconButton(onClick = { navigator.push(PluginsScreen()) }) {
Icon(
painter = painterResource(R.drawable.ic_extension),
contentDescription = stringResource(R.string.navigation_about)
)
}
IconButton(onClick = { navigator.push(SettingsScreen()) }) {
Icon(
painter = painterResource(R.drawable.ic_settings),
contentDescription = stringResource(R.string.navigation_settings)
)
}
}
)
}

View file

@ -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,
)

View file

@ -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<InstallData>) : InstallsState
}

View file

@ -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)
)
}
}
)
}

View file

@ -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,
)
}
}
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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<DiscordVersion> {
object Error : DiscordVersion
object None : DiscordVersion
data object Error : DiscordVersion
data object None : DiscordVersion
data class Existing(
val type: Type,

View file

@ -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)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M440,520L240,520q-17,0 -28.5,-11.5T200,480q0,-17 11.5,-28.5T240,440h200v-200q0,-17 11.5,-28.5T480,200q17,0 28.5,11.5T520,240v200h200q17,0 28.5,11.5T760,480q0,17 -11.5,28.5T720,520L520,520v200q0,17 -11.5,28.5T480,760q-17,0 -28.5,-11.5T440,720v-200Z" />
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#33FF88"
android:pathData="M15.672,16.224c-1.333,3.194 -6.808,2.414 -6.808,-0.36l0,-7.961c0,-2.87 5.403,-3.53 7.048,-0.648l4.995,-1.921c-1.945,-4.119 -5.536,-5.319 -8.513,-5.319c-4.851,0 -9.642,2.678 -9.642,7.901l0,7.961c0,5.259 4.791,7.901 9.546,7.901c3.038,0 6.7,-1.441 8.718,-5.187L15.672,16.224z" />
<path
android:fillColor="#00C853"
android:fillType="evenOdd"
android:pathData="M9.08,0.59l5.644,0L23.477,24.005l-6.652,0l-1.597,-4.695l-6.544,0L7.17,24.005l-6.652,0l3.638,-9.954l5.92,0l3.674,0l-1.693,-6.844l-0.168,0l-1.057,4.011l-4.167,-3.999L9.08,0.59z" />
<path
android:fillColor="#33FF88"
android:pathData="M21.015,18.578l-5.331,-2.353c-0.612,1.813 -1.561,2.39 -3.71,3.074l4.479,3.566C18.217,22.072 19.874,20.691 21.015,18.578z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M440,680h80v-240h-80v240ZM480,360q17,0 28.5,-11.5T520,320q0,-17 -11.5,-28.5T480,280q-17,0 -28.5,11.5T440,320q0,17 11.5,28.5T480,360ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z" />
</vector>

View file

@ -3,6 +3,7 @@
<string name="app_description">A modification for the Discord Android App</string>
<string name="aliucord">Aliucord</string>
<string name="discord">Discord</string>
<string name="github">GitHub</string>
<string name="support_server">Support Server</string>
<string name="commits">Commits</string>
@ -15,6 +16,7 @@
<string name="action_confirm">Confirm</string>
<string name="action_dismiss">Dismiss</string>
<string name="action_install">Install</string>
<string name="action_add_install">Add Installation</string>
<string name="action_reinstall">Reinstall</string>
<string name="action_update">Update</string>
<string name="action_clear">Clear</string>
@ -29,6 +31,7 @@
<string name="action_copied">Copied!</string>
<string name="action_cleared_cache">Cleared cache!</string>
<string name="action_exit_anyways">Exit anyways</string>
<string name="action_open_info">Open info</string>
<string name="permissions_grant_title">Grant Permissions</string>
<string name="permissions_grant_body">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.</string>
@ -78,6 +81,9 @@
<string name="network_load_fail">Failed to load</string>
<string name="label_pkg_name">Package:</string>
<string name="label_version_discord">Discord version:</string>
<string name="version_supported">Supported version:</string>
<string name="version_installed">Installed version:</string>
<string name="version_none">None</string>