Fixed installer

This commit is contained in:
zt64 2022-03-05 15:45:46 -05:00
parent 5e1a71d4d4
commit 739196fa52
41 changed files with 747 additions and 616 deletions

View file

@ -3,6 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
val pagingVersion = "3.1.0"
val accompanistVersion = "0.23.0"
val composeVersion = "1.1.0"
val ktorVersion = "1.6.7"
@ -10,6 +11,7 @@ val ktorVersion = "1.6.7"
plugins {
id("com.android.application")
id("kotlin-android")
id("com.google.devtools.ksp") version "1.6.10-1.0.4"
}
android {
@ -49,32 +51,47 @@ android {
composeOptions.kotlinCompilerExtensionVersion = composeVersion
}
kotlin {
sourceSets {
debug {
kotlin.srcDir("build/generated/ksp/debug/kotlin")
}
release {
kotlin.srcDir("build/generated/ksp/release/kotlin")
}
}
}
dependencies {
implementation(fileTree("./libs"))
// core dependencies
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.core:core-ktx:1.7.0")
implementation("com.github.aliucord:libzip:1.0.1")
// implementation("com.github.aliucord:libzip:1.0.1")
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
// compose dependencies
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.compose.ui:ui:${composeVersion}")
implementation("androidx.compose.material3:material3:1.0.0-alpha06")
implementation("androidx.compose.ui:ui-tooling:${composeVersion}")
implementation("androidx.compose.material:material:${composeVersion}")
implementation("androidx.compose.ui:ui-tooling-preview:${composeVersion}")
implementation("androidx.navigation:navigation-compose:2.4.1")
implementation("androidx.compose.material3:material3:1.0.0-alpha06")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
// accompanist dependencies
implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}")
implementation("com.google.accompanist:accompanist-permissions:${accompanistVersion}")
// compose destinations
implementation("io.github.raamcosta.compose-destinations:core:1.3.2-beta")
ksp("io.github.raamcosta.compose-destinations:ksp:1.3.2-beta")
// other dependencies
implementation("com.google.code.gson:gson:2.8.9")
implementation("io.coil-kt:coil-compose:1.4.0")
implementation("org.bouncycastle:bcpkix-jdk15on:1.65")
// ktor
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-gson:$ktorVersion")
implementation("org.bouncycastle:bcpkix-jdk15on:1.65")
}

BIN
app/libs/libzip-release.aar Normal file

Binary file not shown.

View file

@ -17,4 +17,5 @@
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
-keep class com.aliucord.manager.models.** { private final *; }
-keep class com.aliucord.manager.models.** { private final *; }
-keep class com.aliucord.libzip.Zip { private final *; }

View file

@ -26,7 +26,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AliucordManager"
android:theme="@style/Theme.AliucordManager.SplashScreen"
tools:ignore="AllowBackup">
<provider
android:name="androidx.core.content.FileProvider"

View file

@ -1,6 +0,0 @@
package com.aliucord.manager
import java.security.PrivateKey
import java.security.cert.X509Certificate
class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey)

View file

@ -5,111 +5,67 @@
package com.aliucord.manager
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.view.animation.AccelerateInterpolator
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.aliucord.manager.preferences.Preferences
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.preferences.sharedPreferences
import com.aliucord.manager.preferences.themePref
import com.aliucord.manager.ui.Screen
import com.aliucord.manager.ui.components.AppBar
import com.aliucord.manager.ui.components.GrantPermission
import com.aliucord.manager.ui.screens.*
import com.aliucord.manager.ui.components.ManagerScaffold
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.theme.Theme
import com.aliucord.manager.utils.Github
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionRequired
import com.google.accompanist.permissions.rememberPermissionState
@ExperimentalMaterialApi
@ExperimentalFoundationApi
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = getPreferences(Context.MODE_PRIVATE)
installSplashScreen().setOnExitAnimationListener { provider ->
provider.view.animate().apply {
interpolator = AccelerateInterpolator()
duration = 200L
Github.checkForUpdates()
alpha(0f)
withEndAction(provider::remove)
start()
}
}
sharedPreferences = getPreferences(Context.MODE_PRIVATE)
setContent {
ManagerTheme(
isBlack = Preferences.useBlack.get(),
isBlack = Prefs.useBlack.get(),
isDarkTheme = run {
val theme = Theme.from(themePref.get())
val theme = Theme.from(Prefs.theme.get())
theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK
}
) {
MainActivityLayout()
}
}
}
}
val context = LocalContext.current
@ExperimentalMaterialApi
@ExperimentalMaterial3Api
@ExperimentalFoundationApi
@ExperimentalPermissionsApi
@Composable
private fun MainActivityLayout() {
val storagePermissionState = rememberPermissionState(Manifest.permission.READ_EXTERNAL_STORAGE)
val navController = rememberNavController()
val requestAllFilesAccess = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
val context = LocalContext.current
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
SideEffect {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData("package:com.aliucord.manager".toUri())
SideEffect {
@SuppressLint("NewApi")
if (requestAllFilesAccess && !Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData("package:com.aliucord.manager".toUri())
context.startActivity(intent)
}
}
context.startActivity(intent)
} else if (storagePermissionState.shouldShowRationale) storagePermissionState.launchPermissionRequest()
}
Scaffold(
topBar = { AppBar(navController) }
) { paddingValues ->
PermissionRequired(
permissionState = storagePermissionState,
permissionNotAvailableContent = { GrantPermission(storagePermissionState) },
permissionNotGrantedContent = { GrantPermission(storagePermissionState) }
) {
NavHost(
navController,
modifier = Modifier.padding(paddingValues).padding(12.dp),
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) { HomeScreen(navController) }
composable(Screen.Commits.route) { CommitsScreen() }
composable(Screen.About.route) { AboutScreen() }
composable(Screen.Settings.route) { SettingsScreen() }
composable(Screen.Store.route) { StoreScreen() }
composable(Screen.Installer.route) { InstallerScreen(navController) }
ManagerScaffold()
}
}
}

View file

@ -1,22 +1,23 @@
package com.aliucord.manager.utils
package com.aliucord.manager.models
import android.util.Log
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.models.Manifest
import com.aliucord.manager.utils.gson
import com.aliucord.manager.utils.pluginsDir
import java.io.File
import java.io.InputStreamReader
import java.util.zip.ZipException
import java.util.zip.ZipFile
class Plugin(val file: File) {
data class Plugin(val file: File) {
val manifest: Manifest
init {
ZipFile(file).use { zipFile ->
val entry = zipFile.getEntry("manifest.json")
?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.")
zipFile.getInputStream(entry).use { zis ->
manifest = gson.fromJson(InputStreamReader(zis), Manifest::class.java)
manifest = zipFile.getInputStream(entry).use { zis ->
gson.fromJson(zis.reader(), Manifest::class.java)
}
}
}
@ -31,14 +32,16 @@ class Plugin(val file: File) {
val files = pluginsDir.listFiles() ?: return emptyList()
return files.mapNotNull { file ->
if (!file.name.endsWith(".zip")) {
if (file.extension == "zip") {
try {
Plugin(file)
} catch (th: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", th)
null
}
} else {
Log.w(BuildConfig.TAG, "Found non zip ${file.name} in plugins folder.")
null
} else try {
Plugin(file)
} catch (th: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", th)
null
}
}.sortedBy { it.manifest.name }
}

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.models
package com.aliucord.manager.models.github
import com.google.gson.annotations.SerializedName

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.models
package com.aliucord.manager.models.github
import com.google.gson.annotations.SerializedName

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.models
package com.aliucord.manager.models.github
data class Version(
val versionCode: String,

View file

@ -10,7 +10,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.core.content.edit
import kotlin.reflect.KProperty
var sharedPreferences: SharedPreferences? = null
lateinit var sharedPreferences: SharedPreferences
class Preference<T>(
private val key: String,
@ -18,7 +18,7 @@ class Preference<T>(
getter: SharedPreferences.(key: String, defaultValue: T) -> T?,
private val setter: SharedPreferences.Editor.(key: String, newValue: T) -> Unit
) {
val value = mutableStateOf(sharedPreferences!!.getter(key, defaultValue) ?: defaultValue)
private val value = mutableStateOf(sharedPreferences.getter(key, defaultValue) ?: defaultValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value.value
@ -31,7 +31,7 @@ class Preference<T>(
value.value = newValue
sharedPreferences!!.edit { setter(key, newValue) }
sharedPreferences.edit { setter(key, newValue) }
}
}

View file

@ -5,15 +5,14 @@
package com.aliucord.manager.preferences
import android.os.Environment
import com.aliucord.manager.utils.aliucordDir
var themePref = intPreference("theme")
val replaceBg = boolPreference("replace_bg", true)
val devModePreference = boolPreference("dev_mode", false)
val debuggablePreference = boolPreference("debuggable")
val useDexFromStoragePreference = boolPreference("use_dex_from_storage")
val dexLocationPreference = stringPreference("dex_location", Environment.getExternalStorageDirectory().absolutePath + "/Aliucord/Injector.dex")
object Preferences {
object Prefs {
val theme = intPreference("theme")
val useBlack = boolPreference("use_black")
val replaceBg = boolPreference("replace_bg", true)
val devMode = boolPreference("dev_mode", false)
val debuggable = boolPreference("debuggable")
val useDexFromStorage = boolPreference("use_dex_from_storage")
val dexLocation = stringPreference("dex_location", "$aliucordDir/Injector.dex")
}

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2021 Juby210
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import com.aliucord.manager.R
@ExperimentalFoundationApi
sealed class Screen(val route: String, @StringRes val displayName: Int) {
companion object {
val screens by lazy { listOf(Home, About, Settings, Commits, Installer, Store) }
}
object Home : Screen("home", R.string.app_name)
object About : Screen("about", R.string.about)
object Settings : Screen("settings", R.string.settings)
object Commits : Screen("commits", R.string.commits)
object Installer : Screen("installer", R.string.installer)
object Store : Screen("store", R.string.store)
}

View file

@ -1,6 +1,6 @@
package com.aliucord.manager.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
@ -18,34 +18,43 @@ import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.ui.Screen
import com.aliucord.manager.ui.screens.destinations.*
import com.aliucord.manager.ui.screens.navDestination
@get:StringRes
private val Destination.title
get() = when (this) {
HomeScreenDestination -> R.string.home
InstallerScreenDestination -> R.string.installer
CommitsScreenDestination -> R.string.commits
StoreScreenDestination -> R.string.store
SettingsScreenDestination -> R.string.settings
AboutScreenDestination -> R.string.about
}
@ExperimentalFoundationApi
@Composable
fun AppBar(navController: NavController) {
val route = navController.currentBackStackEntryAsState().value?.destination?.route ?: Screen.Home.route
val destination = navController.currentBackStackEntryAsState().value?.navDestination ?: HomeScreenDestination
var isMenuExpanded by remember { mutableStateOf(false) }
MediumTopAppBar(
navigationIcon = {
if (route != Screen.Home.route) {
if (destination != HomeScreenDestination) {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, stringResource(R.string.back), tint = MaterialTheme.colorScheme.onBackground)
}
}
},
title = {
Text(stringResource(Screen.screens.first { screen ->
screen.route == route
}.displayName))
Text(stringResource(destination.title))
},
actions = {
val localUriHandler = LocalUriHandler.current
IconButton(
onClick = {
navController.navigate(Screen.Store.route) {
popUpTo(Screen.Home.route) { saveState = true }
navController.navigate(StoreScreenDestination.route) {
popUpTo(HomeScreenDestination.route) { saveState = true }
}
},
) {
@ -89,21 +98,17 @@ fun AppBar(navController: NavController) {
DropdownMenuItem(
onClick = {
isMenuExpanded = false
navController.navigate(Screen.About.route) {
popUpTo(Screen.Home.route) { saveState = true }
}
navController.navigate(AboutScreenDestination.route)
},
text = { Text(stringResource(Screen.About.displayName)) }
text = { Text(stringResource(AboutScreenDestination.title)) }
)
DropdownMenuItem(
onClick = {
isMenuExpanded = false
navController.navigate(Screen.Settings.route) {
popUpTo(Screen.Home.route) { saveState = true }
}
navController.navigate(SettingsScreenDestination.route)
},
text = { Text(stringResource(Screen.Settings.displayName)) }
text = { Text(stringResource(SettingsScreenDestination.title)) }
)
}
}

View file

@ -0,0 +1,53 @@
package com.aliucord.manager.ui.components
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.material.ListItem as M2ListItem
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ListItem(
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
secondaryText: @Composable (() -> Unit)? = null,
singleLineSecondaryText: Boolean = true,
overlineText: @Composable (() -> Unit)? = null,
trailing: @Composable (() -> Unit)? = null,
text: @Composable () -> Unit
) = M2ListItem(
modifier = modifier,
icon = icon,
overlineText = if (overlineText != null) {{
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
LocalTextStyle provides MaterialTheme.typography.labelMedium
) {
overlineText()
}
}} else null,
text = {
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
LocalTextStyle provides MaterialTheme.typography.bodyLarge
) {
text()
}
},
secondaryText = if (secondaryText != null) {
{
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
LocalTextStyle provides MaterialTheme.typography.bodyMedium
) {
secondaryText()
}
}
} else null,
trailing = trailing,
singleLineSecondaryText = singleLineSecondaryText,
)

View file

@ -0,0 +1,48 @@
package com.aliucord.manager.ui.components
import android.Manifest
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import com.aliucord.manager.ui.screens.NavGraphs
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionRequired
import com.google.accompanist.permissions.rememberPermissionState
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.rememberNavHostEngine
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalPermissionsApi::class,
ExperimentalAnimationApi::class
)
@Composable
fun ManagerScaffold() {
val navController = rememberNavController()
Scaffold(
topBar = { AppBar(navController) }
) { paddingValues ->
val storagePermissionState = rememberPermissionState(Manifest.permission.READ_EXTERNAL_STORAGE)
PermissionRequired(
permissionState = storagePermissionState,
permissionNotAvailableContent = { GrantPermission(storagePermissionState) },
permissionNotGrantedContent = { GrantPermission(storagePermissionState) }
) {
DestinationsNavHost(
modifier = Modifier
.padding(paddingValues)
.padding(horizontal = 12.dp, vertical = 14.dp),
navGraph = NavGraphs.root,
navController = navController,
engine = rememberNavHostEngine()
)
}
}
}

View file

@ -23,9 +23,9 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.aliucord.manager.R
import com.aliucord.manager.models.Plugin
import com.aliucord.manager.ui.components.plugins.Changelog
import com.aliucord.manager.ui.components.plugins.PluginCard
import com.aliucord.manager.utils.Plugin
@Composable
fun PluginsList() = Column(
@ -46,7 +46,7 @@ fun PluginsList() = Column(
trailingIcon = { Icon(Icons.Default.Search, stringResource(R.string.search_plugins)) },
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search),
keyboardActions = KeyboardActions { focusManager.clearFocus() },
shape = RoundedCornerShape(20.dp),
shape = RoundedCornerShape(16.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = MaterialTheme.colorScheme.onSecondaryContainer,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
@ -65,8 +65,6 @@ fun PluginsList() = Column(
|| authors.any { a -> a.name.contains(search, true) }
}
}) { plugin ->
// TODO - make this get pluginEnabled from Aliucord preferences somehow
var isChecked by remember { mutableStateOf(true) }
var showDeleteDialog by remember { mutableStateOf(false) }
var showChangelogDialog by remember { mutableStateOf(false) }

View file

@ -6,24 +6,26 @@ import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import coil.transform.CircleCropTransformation
import com.aliucord.manager.R
@Composable
fun ContributorEntry(name: String) {
val uriHandler = LocalUriHandler.current
Image(
modifier = Modifier
.size(48.dp)
.clickable { uriHandler.openUri("https://github.com/$name") },
painter = rememberImagePainter(
data = "https://github.com/$name.png",
builder = {
transformations(CircleCropTransformation())
}
),
contentDescription = "Contributor",
modifier = Modifier
.size(48.dp)
.clickable { uriHandler.openUri("https://github.com/$name") }
contentDescription = stringResource(R.string.contributor)
)
}

View file

@ -15,15 +15,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.models.GithubUser
import com.aliucord.manager.models.github.GithubUser
import com.aliucord.manager.utils.Github
import com.aliucord.manager.utils.httpClient
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@ExperimentalFoundationApi
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun ContributorsCard() = ElevatedCard(
modifier = Modifier.wrapContentHeight().fillMaxWidth()
@ -35,7 +34,7 @@ fun ContributorsCard() = ElevatedCard(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
stringResource(R.string.contributors),
text = stringResource(R.string.contributors),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)

View file

@ -21,12 +21,14 @@ fun TeamCard() = ElevatedCard(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
stringResource(R.string.team),
text = stringResource(R.string.team),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
UserEntry("Juby210", "Fox")
UserEntry("Vendicated", "the ven")
UserEntry("mwittrien", "your memory gon")

View file

@ -1,7 +1,10 @@
package com.aliucord.manager.ui.components.about
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -12,9 +15,7 @@ import coil.compose.rememberImagePainter
import coil.transform.CircleCropTransformation
@Composable
fun UserEntry(name: String, roles: String) = Column(
modifier = Modifier.padding(vertical = 8.dp)
) {
fun UserEntry(name: String, roles: String) = Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)

View file

@ -1,72 +1,94 @@
package com.aliucord.manager.ui.components.installer
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
import com.aliucord.manager.ui.Screen
@OptIn(ExperimentalFoundationApi::class)
enum class DownloadMethod { DOWNLOAD, SELECT }
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun InstallerDialog(
visible: MutableState<Boolean>,
navController: NavController
) = AlertDialog(
title = {
Row (
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(painterResource(R.drawable.ic_discord), "Discord")
Spacer(Modifier.width(12.dp))
Text("Select Installation Method")
}
},
confirmButton = {
Column(
modifier = Modifier.wrapContentWidth().padding(32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val modifier = Modifier.fillMaxWidth()
onDismissRequest: () -> Unit,
onConfirm: (DownloadMethod) -> Unit
) {
var selectedMethod by remember { mutableStateOf(DownloadMethod.DOWNLOAD) }
AlertDialog(
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_download_24dp),
contentDescription = stringResource(R.string.download_method)
)
},
title = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.download_method))
}
},
text = {
Column {
mapOf(
DownloadMethod.DOWNLOAD to stringResource(R.string.download_apk),
DownloadMethod.SELECT to stringResource(R.string.select_apk)
).forEach { (method, name) ->
Column {
Row(
modifier = Modifier.clickable {
selectedMethod = method
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = name,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = selectedMethod == method,
onClick = { selectedMethod = method }
)
}
}
}
}
},
confirmButton = {
Button(
modifier = modifier,
onClick = {
navController.currentBackStackEntry?.arguments
navController.navigate(Screen.Installer.route)
onConfirm(selectedMethod)
onDismissRequest()
}
) {
Text("Download")
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
Button(
modifier = modifier,
onClick = {
navController.navigate(Screen.Installer.route)
}
onClick = onDismissRequest,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text("From installed app")
Text(stringResource(R.string.dismiss))
}
Button(
modifier = modifier,
onClick = {
navController.navigate(Screen.Installer.route)
}
) {
Text("From storage")
}
}
},
onDismissRequest = { visible.value = false }
)
},
onDismissRequest = onDismissRequest
)
}

View file

@ -19,20 +19,19 @@ import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation
import com.aliucord.manager.R
import com.aliucord.manager.utils.Plugin
import java.util.regex.Pattern
import com.aliucord.manager.models.Plugin
private val hyperLinkPattern = Pattern.compile("\\[(.+?)]\\((.+?\\))")
private val hyperLinkPattern = Regex("\\[(.+?)]\\((.+?\\))")
@Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol
private val headerStylePattern = Pattern.compile("\\{(improved|added|fixed)( marginTop)?\\}")
private val headerStylePattern = Regex("\\{(improved|added|fixed)( marginTop)?\\}")
@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component
@Composable
private fun AnnotatedString.Builder.hyperlink(content: String) {
var idx = 0
with(hyperLinkPattern.matcher(content)) {
with(hyperLinkPattern.toPattern().matcher(content)) {
while (find()) {
val start = start()
val end = end()
@ -82,7 +81,7 @@ fun Changelog(
painter = rememberImagePainter(mediaUrl) {
transformations(RoundedCornersTransformation(14.dp.value))
},
contentDescription = "Changelog Media"
contentDescription = stringResource(R.string.changelog_media)
)
}

View file

@ -16,8 +16,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.models.Plugin
import com.aliucord.manager.ui.components.Switch
import com.aliucord.manager.utils.Plugin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -27,8 +27,7 @@ fun PluginCard(
onShowChangelog: () -> Unit
) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(12.dp)

View file

@ -1,28 +1,29 @@
package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.aliucord.manager.preferences.Preference
import com.aliucord.manager.ui.components.ListItem
import com.aliucord.manager.ui.components.Switch
@Composable
fun ListItem(
trailing: @Composable () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Column {
Text("Title", style = MaterialTheme.typography.labelMedium)
Text("Subtext", style = MaterialTheme.typography.labelSmall)
}
Spacer(Modifier.weight(1f, true))
trailing()
fun PreferenceItem(
icon: @Composable (() -> Unit)? = null,
title: @Composable () -> Unit,
description: @Composable (() -> Unit)? = null,
preference: Preference<Boolean>
) = ListItem(
icon = icon,
modifier = Modifier.clickable {
preference.set(!preference.get())
},
text = title,
secondaryText = description,
trailing = {
Switch(
checked = preference.get(),
onCheckedChange = preference::set
)
}
}
)

View file

@ -1,16 +0,0 @@
package com.aliucord.manager.ui.components.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.aliucord.manager.preferences.Preference
import com.aliucord.manager.ui.components.Switch
@Composable
fun SettingSwitch(
modifier: Modifier = Modifier,
preference: Preference<Boolean>
) = Switch(
modifier = modifier,
checked = preference.get(),
onCheckedChange = preference::set
)

View file

@ -0,0 +1,62 @@
package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.theme.Theme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThemeDialog(
onDismissRequest: () -> Unit,
onConfirm: (Theme) -> Unit
) {
var selectedTheme by remember { mutableStateOf(Theme.from(Prefs.theme.get())) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(R.string.theme)) },
text = {
Column {
Theme.values().forEach { theme ->
Row(
modifier = Modifier.clickable {
selectedTheme = theme
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
theme.displayName,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = theme == selectedTheme,
onClick = { selectedTheme = theme }
)
}
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(selectedTheme)
onDismissRequest()
}
) {
Text(stringResource(R.string.apply))
}
}
)
}

View file

@ -1,28 +1,30 @@
package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.clickable
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
import com.aliucord.manager.preferences.themePref
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.components.ListItem
import com.aliucord.manager.ui.theme.Theme
@ExperimentalMaterialApi
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ThemeSetting(openDialog: MutableState<Boolean>) {
ListItem(
text = { Text(stringResource(R.string.theme)) },
secondaryText = { Text(stringResource(R.string.theme_setting_description))},
trailing = {
FilledTonalButton(
onClick = { openDialog.value = true }
) {
Text(Theme.from(themePref.get()).displayName)
}
fun ThemeSetting(
onClick: () -> Unit,
) = ListItem(
modifier = Modifier.clickable { onClick() },
text = { Text(stringResource(R.string.theme)) },
secondaryText = { Text(stringResource(R.string.theme_setting_description)) },
trailing = {
FilledTonalButton(
onClick = onClick
) {
Text(Theme.from(Prefs.theme.get()).displayName)
}
)
}
}
)

View file

@ -1,6 +1,5 @@
package com.aliucord.manager.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
@ -14,8 +13,9 @@ import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.about.ContributorsCard
import com.aliucord.manager.ui.components.about.TeamCard
import com.ramcosta.composedestinations.annotation.Destination
@ExperimentalFoundationApi
@Destination
@Composable
fun AboutScreen() {
val uriHandler = LocalUriHandler.current

View file

@ -1,97 +1,98 @@
package com.aliucord.manager.ui.screens
import android.util.Log
import androidx.annotation.Keep
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.models.Commit
import androidx.paging.*
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.aliucord.manager.R
import com.aliucord.manager.models.github.Commit
import com.aliucord.manager.ui.components.ListItem
import com.aliucord.manager.utils.Github
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Keep
private data class CommitData(
val commit: Commit,
val buildSha: String?
)
import com.ramcosta.composedestinations.annotation.Destination
@OptIn(ExperimentalMaterialApi::class)
@ExperimentalMaterialApi
@Destination
@Composable
fun CommitsScreen() {
val commits = remember { mutableStateListOf<CommitData>() }
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (commits.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize().padding(12.dp),
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() }
val pager = remember {
Pager(
PagingConfig(
pageSize = 30,
enablePlaceholders = true,
maxSize = 200
)
) {
object : PagingSource<Int, Commit>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
val pageNumber = params.key ?: 0
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
try {
val buildCommits = Github.getCommits(
"sha" to "builds",
"path" to "Aliucord.dex",
"per_page" to "50"
)
val commits2 = Github.getCommits("per_page" to "50")
val response = Github.getCommits("page" to pageNumber.toString())
val prevKey = if (pageNumber > 0) pageNumber - 1 else null
val nextKey = if (response.isNotEmpty()) pageNumber + 1 else null
commits.addAll(commits2.map { c ->
CommitData(
commit = c,
buildSha = buildCommits.find { bc -> bc.commit.message.substring(6) == c.sha }?.sha
)
})
} catch (e: Throwable) {
Log.e(BuildConfig.TAG, "Failed to get commits", e)
}
}
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(commits.toList()) { data ->
val commit = data.commit
val localUriHandler = LocalUriHandler.current
ListItem(
modifier = Modifier.clickable {
localUriHandler.openUri(commit.htmlUrl)
},
text = {
Text(
commit.sha.substring(0, 7),
color = if (data.buildSha != null) MaterialTheme.colorScheme.primary else Color.Unspecified
)
},
secondaryText = { Text("${commit.commit.message.split("\n").first()} - ${commit.author.name}") },
return LoadResult.Page(
data = response,
prevKey = prevKey,
nextKey = nextKey
)
}
override fun getRefreshKey(state: PagingState<Int, Commit>) = state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
}
}
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
if (lazyPagingItems.loadState.refresh == LoadState.Loading) item {
Text(
text = stringResource(R.string.paging_initial_load),
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
items(lazyPagingItems) { commitData ->
val localUriHandler = LocalUriHandler.current
if (commitData == null) return@items
ListItem(
modifier = Modifier.clickable {
localUriHandler.openUri(commitData.htmlUrl)
},
overlineText = { Text(commitData.sha.substring(0, 7)) },
text = { Text("${commitData.commit.message.split("\n").first()} - ${commitData.author.name}")}
)
}
if (lazyPagingItems.loadState.append == LoadState.Loading) item {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
}

View file

@ -8,7 +8,8 @@ package com.aliucord.manager.ui.screens
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
@ -24,26 +25,28 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.core.net.toFile
import com.aliucord.manager.R
import com.aliucord.manager.models.Version
import com.aliucord.manager.preferences.devModePreference
import com.aliucord.manager.ui.Screen
import com.aliucord.manager.models.github.Version
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.components.PluginsList
import com.aliucord.manager.ui.components.installer.DownloadMethod
import com.aliucord.manager.ui.components.installer.InstallerDialog
import com.aliucord.manager.ui.screens.destinations.CommitsScreenDestination
import com.aliucord.manager.ui.screens.destinations.InstallerScreenDestination
import com.aliucord.manager.utils.Github
import com.aliucord.manager.utils.gson
import com.aliucord.manager.utils.httpClient
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@ExperimentalPermissionsApi
@ExperimentalFoundationApi
@Destination(start = true)
@Composable
fun HomeScreen(navController: NavController) {
fun HomeScreen(navigator: DestinationsNavigator) {
val packageManager = LocalContext.current.packageManager
var supportedVersion by remember { mutableStateOf<String?>(null) }
val installedVersion = remember {
@ -54,9 +57,7 @@ fun HomeScreen(navController: NavController) {
}
}
val showOptionsDialog = remember { mutableStateOf(false) }
if (supportedVersion == null) LaunchedEffect(Unit) {
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
val version = gson.fromJson(httpClient.get<String>(Github.dataUrl), Version::class.java)
@ -69,7 +70,26 @@ fun HomeScreen(navController: NavController) {
}
}
if (showOptionsDialog.value) InstallerDialog(showOptionsDialog, navController)
var showOptionsDialog by remember { mutableStateOf(false) }
if (showOptionsDialog) {
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
navigator.navigate(InstallerScreenDestination(uri.toFile()))
}
InstallerDialog(
onDismissRequest = { showOptionsDialog = false },
onConfirm = { method ->
if (method == DownloadMethod.DOWNLOAD) {
navigator.navigate(InstallerScreenDestination())
} else {
filePicker.launch(arrayOf("application/octet-stream"))
}
}
)
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
@ -78,7 +98,9 @@ fun HomeScreen(navController: NavController) {
modifier = Modifier.wrapContentHeight()
) {
Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
@ -113,81 +135,90 @@ fun HomeScreen(navController: NavController) {
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if (devModePreference.get()) {
showOptionsDialog.value = true
} else {
navController.navigate(Screen.Installer.route) {
popUpTo(Screen.Home.route) { saveState = true }
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
modifier = Modifier.weight(1f, true),
onClick = {
if (Prefs.devMode.get()) {
showOptionsDialog = true
} else {
navigator.navigate(InstallerScreenDestination())
}
}
}
) {
Icon(
painter = painterResource(drawable),
contentDescription = stringResource(description),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.padding(8.dp)
)
) {
Icon(
modifier = Modifier.padding(8.dp),
painter = painterResource(drawable),
contentDescription = stringResource(description),
tint = MaterialTheme.colorScheme.onPrimary
)
Text(stringResource(description))
Text(stringResource(description))
}
if (installedVersion == "-") Button(
modifier = Modifier.wrapContentSize(),
onClick = { navigator.navigate(CommitsScreenDestination) },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Icon(
modifier = Modifier.padding(8.dp),
painter = painterResource(R.drawable.ic_update_24dp),
contentDescription = "Commits",
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
BoxWithConstraints {
if (installedVersion != "-") {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
if (installedVersion != "-") {
val context = LocalContext.current
val context = LocalContext.current
Button(
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
onClick = {
val packageURI = Uri.parse("package:com.aliucord")
val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI)
Button(
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
onClick = {
val packageURI = Uri.parse("package:com.aliucord")
val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI)
context.startActivity(uninstallIntent)
}
) {
Icon(
modifier = Modifier.padding(8.dp),
imageVector = Icons.Default.Delete,
contentDescription = "Uninstall",
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
Button(
modifier = Modifier.weight(1f),
onClick = {
packageManager.getLaunchIntentForPackage("com.aliucord")?.let {
context.startActivity(it)
} ?: Toast.makeText(context, "Failed to launch Aliucord", Toast.LENGTH_LONG)
.show()
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Icon(
modifier = Modifier.padding(8.dp),
painter = painterResource(R.drawable.ic_launch_24dp),
contentDescription = "Launch",
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
context.startActivity(uninstallIntent)
}
) {
Icon(
modifier = Modifier.padding(8.dp),
imageVector = Icons.Default.Delete,
contentDescription = "Uninstall",
tint = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
Button(
modifier = Modifier.weight(1f),
onClick = {
navController.navigate(Screen.Commits.route) {
popUpTo(Screen.Home.route) { saveState = true }
}
packageManager.getLaunchIntentForPackage("com.aliucord")?.let {
context.startActivity(it)
} ?: Toast.makeText(context, "Failed to launch Aliucord", Toast.LENGTH_LONG)
.show()
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Icon(
modifier = Modifier.padding(8.dp),
painter = painterResource(R.drawable.ic_launch_24dp),
contentDescription = "Launch",
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
}
Button(
modifier = Modifier.weight(1f),
onClick = { navigator.navigate(CommitsScreenDestination) },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Icon(
modifier = Modifier.padding(8.dp),
@ -195,10 +226,6 @@ fun HomeScreen(navController: NavController) {
contentDescription = "Commits",
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
if (this@BoxWithConstraints.minWidth > 20.dp) {
Text("Commits", color = MaterialTheme.colorScheme.onSecondaryContainer)
}
}
}
}
@ -206,7 +233,11 @@ fun HomeScreen(navController: NavController) {
}
}
ElevatedCard(Modifier.fillMaxWidth().weight(1f)) {
ElevatedCard(
Modifier
.fillMaxWidth()
.weight(1f)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)

View file

@ -1,33 +1,31 @@
package com.aliucord.manager.ui.screens
import android.content.Intent
import android.os.Environment
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.navigation.NavController
import com.aliucord.libzip.Zip
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.Signer
import com.aliucord.manager.preferences.replaceBg
import com.aliucord.manager.ui.Screen
import com.aliucord.manager.utils.DownloadUtils
import com.aliucord.manager.utils.Github
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.screens.destinations.HomeScreenDestination
import com.aliucord.manager.utils.*
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
@OptIn(ExperimentalFoundationApi::class)
@Destination
@Composable
fun InstallerScreen(navController: NavController) {
fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
var log by remember { mutableStateOf("") }
val context = LocalContext.current
@ -35,31 +33,36 @@ fun InstallerScreen(navController: NavController) {
launch(Dispatchers.IO) {
val supportedVersion = Github.version.versionCode
log += "Checking for cached APK...\n"
val discordApk = apk ?: run {
log += "Checking for cached APK...\n"
val discordApk = File(context.externalCacheDir, "discord-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also
File(context.externalCacheDir, "discord-${supportedVersion}.apk").also { file ->
val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0)
val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0)
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also
log += "Downloading Discord APK...\n"
log += "Downloading APK...\n"
DownloadUtils.downloadDiscordApk(context, supportedVersion)
DownloadUtils.downloadDiscordApk(context, supportedVersion)
log += "Finished downloading APK\n"
log += "Done\n"
}
}
val manifest = File(context.externalCacheDir, "AndroidManifest.xml").also { file ->
if (file.exists()) return@also
val useDebuggableManifest = Prefs.debuggable.get()
val manifest =
File(
context.externalCacheDir,
if (useDebuggableManifest) "AndroidManifest-debuggable.xml" else "AndroidManifest.xml"
).also { file ->
if (file.exists()) return@also
log += "Downloading patched AndroidManifest.xml...\n"
log += "Downloading patched manifest...\n"
DownloadUtils.downloadManifest(context)
DownloadUtils.downloadManifest(context, useDebuggableManifest)
log += "Finished downloading manifest\n"
}
log += "Done\n"
}
val injector = File(context.externalCacheDir, "Injector.dex").also { file ->
if (file.exists()) return@also
@ -68,13 +71,10 @@ fun InstallerScreen(navController: NavController) {
DownloadUtils.downloadInjector(context)
log += "Finished downloading injector\n"
log += "Done\n"
}
log += "Copying original APK ()\n"
val outputDir = File(Environment.getExternalStorageDirectory(), "Aliucord").also {
val outputDir = aliucordDir.also {
if (!it.exists() && !it.mkdirs()) throw FileNotFoundException()
}
@ -148,7 +148,7 @@ fun InstallerScreen(navController: NavController) {
close()
}
if (replaceBg.get()) {
if (Prefs.replaceBg.get()) {
log += "Replacing icon background\n"
val icon1Bytes = assetManager.open("icon1.png").readBytes()
@ -158,7 +158,8 @@ fun InstallerScreen(navController: NavController) {
// androguard arsc resources.arsc --id 0x7f0f0000 (icon1)
// androguard arsc resources.arsc --id 0x7f0f0002 and androguard arsc resources.arsc --id 0x7f0f0006 (icon2)
val icon1Entries = listOf("MbV.png", "kbF.png", "_eu.png", "EtS.png")
val icon2Entries = listOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png")
val icon2Entries =
listOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png")
with(Zip(outputApk.absolutePath, 0, 'a')) {
icon1Entries.forEach { entry -> deleteEntry("res/$entry") }
@ -171,9 +172,8 @@ fun InstallerScreen(navController: NavController) {
}
}
log += "Signing APK\n"
log += "Signing APK...\n"
Signer.newKeystore(File(context.filesDir, "ks.keystore"))
Signer.signApk(outputApk)
log += "Signed APK\n"
@ -191,36 +191,23 @@ fun InstallerScreen(navController: NavController) {
context.startActivity(intent)
launch(Dispatchers.Main) {
navController.navigate(Screen.Home.route)
navigator.navigate(HomeScreenDestination)
}
}
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
modifier = Modifier.fillMaxWidth()
) {
Text(log)
Spacer(Modifier.weight(1f, true))
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(8.dp),
progress = 0.5f
)
// LinearProgressIndicator(
// modifier = Modifier
// .fillMaxWidth()
// .height(8.dp),
// progress = 0.5f
// )
}
}
private fun Zip.writeEntry(entryName: String, stream: InputStream) {
openEntry(entryName)
stream.readBytes().let { bytes -> writeEntry(bytes, bytes.size.toLong()) }
closeEntry()
}
private fun Zip.writeEntry(entryName: String, bytes: ByteArray) {
openEntry(entryName)
writeEntry(bytes, bytes.size.toLong())
closeEntry()
}

View file

@ -5,155 +5,108 @@
package com.aliucord.manager.ui.screens
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.net.toFile
import com.aliucord.manager.R
import com.aliucord.manager.preferences.*
import com.aliucord.manager.ui.components.Switch
import com.aliucord.manager.ui.components.settings.SettingSwitch
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.components.ListItem
import com.aliucord.manager.ui.components.settings.PreferenceItem
import com.aliucord.manager.ui.components.settings.ThemeDialog
import com.aliucord.manager.ui.components.settings.ThemeSetting
import com.aliucord.manager.ui.theme.Theme
import com.ramcosta.composedestinations.annotation.Destination
@OptIn(ExperimentalMaterial3Api::class)
@ExperimentalMaterialApi
@OptIn(ExperimentalMaterial3Api::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Destination
@Composable
fun SettingsScreen() = Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val openDialog = remember { mutableStateOf(false) }
val context = LocalContext.current
var showThemeDialog by remember { mutableStateOf(false) }
if (openDialog.value) {
var selected by remember { mutableStateOf(Theme.from(themePref.get())) }
AlertDialog(
onDismissRequest = { openDialog.value = false },
title = { Text("Theme") },
text = {
Column {
Theme.values().forEach { theme ->
Row(
modifier = Modifier.clickable {
selected = theme
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
theme.displayName,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = theme == selected,
onClick = { selected = theme }
)
}
}
}
},
confirmButton = {
Button(
onClick = {
openDialog.value = false
themePref.set(selected.ordinal)
}
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Confirm"
)
Spacer(Modifier.padding(4.dp))
Text("Apply")
}
}
if (showThemeDialog) {
ThemeDialog(
onDismissRequest = { showThemeDialog = false },
onConfirm = { Prefs.theme.set(it.ordinal) }
)
}
ThemeSetting(openDialog)
ThemeSetting(onClick = { showThemeDialog = true })
ListItem(
text = { Text(stringResource(R.string.use_black)) },
secondaryText = { Text(stringResource(R.string.use_black_description)) },
trailing = { SettingSwitch(preference = Preferences.useBlack) }
PreferenceItem(
title = { Text(stringResource(R.string.use_black)) },
description = { Text(stringResource(R.string.use_black_description)) },
preference = Prefs.useBlack
)
Divider()
ListItem(
text = { Text(stringResource(R.string.replace_bg)) },
trailing = { SettingSwitch(preference = replaceBg) }
PreferenceItem(
title = { Text(stringResource(R.string.replace_bg)) },
preference = Prefs.replaceBg
)
val devModeOn = devModePreference.get()
ListItem(
text = { Text(stringResource(R.string.developer_mode)) },
trailing = {
Switch(
checked = devModeOn,
onCheckedChange = devModePreference::set
)
}
val devModeOn = Prefs.devMode.get()
PreferenceItem(
title = { Text(stringResource(R.string.developer_mode)) },
preference = Prefs.devMode
)
if (devModeOn) {
ListItem(
text = { Text(stringResource(R.string.debuggable)) },
trailing = { SettingSwitch(preference = debuggablePreference) }
PreferenceItem(
title = { Text(stringResource(R.string.debuggable)) },
description = { Text(stringResource(R.string.debuggable_description)) },
preference = Prefs.debuggable
)
val udfs = useDexFromStoragePreference
val udfs = Prefs.useDexFromStorage
ListItem(
text = { Text(stringResource(R.string.use_dex_from_storage)) },
trailing = { SettingSwitch(preference = udfs) }
PreferenceItem(
title = { Text(stringResource(R.string.use_dex_from_storage)) },
preference = Prefs.useDexFromStorage
)
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) Prefs.dexLocation.set(uri.path!!)
}
val color = if (udfs.get()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.onBackground.copy(ContentAlpha.disabled)
}
val test = remember { mutableStateOf("") }
val result = remember { mutableStateOf<Uri?>(null) }
val context = LocalContext.current
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
test.value = uri.toFile().absolutePath
}
ListItem(
modifier = if (udfs.get()) Modifier.clickable {
picker.launch(arrayOf("application/octet-stream"))
} else Modifier,
text = { Text(stringResource(R.string.dex_location), color = color) },
secondaryText = { Text(dexLocationPreference.get(), color = color) }
secondaryText = { Text(Prefs.dexLocation.get(), color = color) }
)
}
Button(
modifier = Modifier.fillMaxWidth().padding(8.dp),
onClick = { /*TODO*/ }
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
onClick = {
context.cacheDir.deleteRecursively()
}
) {
Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center)
}

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.aliucord.manager.utils.httpClient
import com.ramcosta.composedestinations.annotation.Destination
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -33,6 +34,7 @@ private data class Tree(
)
}
@Destination
@Composable
fun StoreScreen() {
val plugins = remember { mutableStateListOf<Tree.File>() }

View file

@ -11,16 +11,24 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object DownloadUtils {
private const val org = "Aliucord"
private const val repo = "Aliucord"
private const val backendHost = "https://aliucord.com/"
private const val contentUrl = "https://raw.githubusercontent.com/${org}/${repo}"
suspend fun downloadDiscordApk(ctx: Context, version: String) =
download(ctx, "$backendHost/download/discord?v=$version", "discord-$version.apk")
suspend fun downloadManifest(ctx: Context) =
download(ctx, Github.getDownloadUrl("builds", "AndroidManifest.xml"), "AndroidManifest.xml")
suspend fun downloadManifest(ctx: Context, useDebuggableManifest: Boolean) {
if (useDebuggableManifest)
download(ctx, "$contentUrl/main/.assets/AndroidManifest-debuggable.xml", "AndroidManifest-debuggable.xml")
else {
download(ctx, "$contentUrl/main/.assets/AndroidManifest.xml", "AndroidManifest.xml")
}
}
suspend fun downloadInjector(ctx: Context) =
download(ctx, Github.getDownloadUrl("builds", "Injector.dex"), "Injector.dex")
suspend fun downloadInjector(ctx: Context) = download(ctx, "$contentUrl/builds/Injector.dex", "Injector.dex")
private suspend fun download(ctx: Context, url: String, fileName: String): File {
val downloadManager = ctx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
@ -42,7 +50,6 @@ object DownloadUtils {
ctx.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
// Users could cancel downloads, guhhhh
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
setDestinationUri(File(ctx.externalCacheDir, fileName).toUri())
downloadManager.enqueue(this)

View file

@ -5,8 +5,8 @@
package com.aliucord.manager.utils
import com.aliucord.manager.models.Commit
import com.aliucord.manager.models.Version
import com.aliucord.manager.models.github.Commit
import com.aliucord.manager.models.github.Version
import io.ktor.client.request.*
import java.io.InputStreamReader
import java.net.URL
@ -28,14 +28,14 @@ object Github {
gson.fromJson(InputStreamReader(URL(dataUrl).openStream()), Version::class.java)
}
private val commitsCache = HashMap<String, Array<Commit>>()
private val commitsCache = HashMap<String, List<Commit>>()
suspend fun getCommits(vararg params: Pair<String, String>): Array<Commit> {
suspend fun getCommits(vararg params: Pair<String, String>): List<Commit> {
val query = params.joinToString("&", "?") { it.first + "=" + it.second }
if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyArray()
if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyList()
val res = httpClient.get<Array<Commit>>(commitsUrl) {
val res = httpClient.get<List<Commit>>(commitsUrl) {
params.forEach { (key, value) -> parameter(key, value) }
}
@ -43,6 +43,4 @@ object Github {
return res
}
fun getDownloadUrl(ref: String, file: String) = "https://raw.githubusercontent.com/$org/$repo/$ref/$file"
}

View file

@ -2,7 +2,7 @@
* Copyright (c) 2021 Juby210 & Vendicated
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager
package com.aliucord.manager.utils
import com.aliucord.libzip.Zip
import org.bouncycastle.asn1.x500.X500Name
@ -28,10 +28,12 @@ import java.util.jar.Attributes
import java.util.jar.JarFile
import java.util.jar.Manifest
private class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey)
object Signer {
private val stripPattern = Regex("^META-INF/(.*)[.](MF|SF|RSA|DSA)$")
fun newKeystore(out: File?) {
private fun newKeystore(out: File?) {
val password = "password".toCharArray()
val key = createKey()
@ -42,12 +44,12 @@ object Signer {
}
}
@Throws(Exception::class)
private fun createKey(): KeySet {
val pair = KeyPairGenerator.getInstance("RSA").run {
initialize(2048)
generateKeyPair()
}
var serialNumber: BigInteger
do serialNumber = SecureRandom().nextInt().toBigInteger()
@ -87,10 +89,12 @@ object Signer {
)
var zip = Zip(apkFile.absolutePath, 6, 'r')
val dig = MessageDigest.getInstance("SHA1")
val digests = LinkedHashMap<String, String>()
val filesToRemove = ArrayList<String>()
val digests = linkedMapOf<String, String>()
val filesToRemove = arrayListOf<String>()
repeat(zip.totalEntries) { i ->
zip.openEntryByIndex(i)
val name = zip.entryName
if (stripPattern.matches(name))
@ -111,7 +115,7 @@ object Signer {
attrs[Attributes.Name("Created-By")] = "Aliucord Installer"
val digestAttr = Attributes.Name("SHA1-Digest")
for ((name, value) in digests) {
digests.forEach { (name, value) ->
val attributes = Attributes()
attributes[digestAttr] = value
@ -126,11 +130,13 @@ object Signer {
zip.writeEntry(baos.toByteArray(), baos.size().toLong())
zip.closeEntry()
}
val manifestHash = getManifestHash(manifest, dig)
val tmpManifest = Manifest().apply {
mainAttributes.putAll(attrs)
}
val manifestMainHash = getManifestHash(tmpManifest, dig)
manifest = Manifest()
attrs = manifest.mainAttributes
attrs[Attributes.Name.SIGNATURE_VERSION] = "1.0"
@ -142,7 +148,9 @@ object Signer {
attributes[digestAttr] = value
manifest.entries[key] = attributes
}
var sigBytes: ByteArray
ByteArrayOutputStream().use { sigStream ->
manifest.write(sigStream)
sigBytes = sigStream.toByteArray()
@ -150,6 +158,7 @@ object Signer {
zip.writeEntry(sigBytes, sigStream.size().toLong())
zip.closeEntry()
}
val signature = signSigFile(keySet, sigBytes)
zip.openEntry("META-INF/CERT.RSA")
zip.writeEntry(signature, signature.size.toLong())

View file

@ -0,0 +1,11 @@
package com.aliucord.manager.utils
import com.aliucord.libzip.Zip
import java.io.InputStream
fun Zip.writeEntry(entryName: String, stream: InputStream) = writeEntry(entryName, stream.readBytes())
fun Zip.writeEntry(entryName: String, bytes: ByteArray) {
openEntry(entryName)
writeEntry(bytes, bytes.size.toLong())
closeEntry()
}

View file

@ -1,25 +1,21 @@
<resources>
<string name="app_name">Aliucord Manager</string>
<string name="clear_files_cache">Clear files cache (Injector.dex and patched manifest)</string>
<string name="clear_files_cache">Clear files cache</string>
<string name="commits">Commits</string>
<string name="dex_location">Dex location</string>
<string name="open_settings">Open settings</string>
<string name="permission_required">You need to grant storage permission to use this app</string>
<string name="permission_grant">Grant permission</string>
<string name="replace_bg">Replace icon background with Aliucord\'s</string>
<string name="replace_bg">Replace app icon with Aliucord logo</string>
<string name="settings">Settings</string>
<string name="show_menu">Show menu</string>
<string name="theme">Theme</string>
<string name="use_dex_from_storage">Use Aliucord.dex from storage</string>
<string name="plugins">Plugins</string>
<string name="launch_plugins">Launch Plugins Page</string>
<string name="plugin_title">%1$s v%2$s</string>
<string name="plugin_authors">By %1$s</string>
<string name="uninstall_plugin">Uninstall %1$s</string>
<string name="search">Search</string>
<string name="view_plugin_changelog">View changelog for %1$s</string>
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin? This action cannot be undone.</string>
<string name="delete">Delete</string>
<string name="close">Close</string>
<string name="support_server">Support Server</string>
<string name="back">Back</string>
@ -27,12 +23,12 @@
<string name="install">Install</string>
<string name="reinstall">Reinstall</string>
<string name="update">Update</string>
<string name="developer_mode">Enable Developer Mode</string>
<string name="debuggable">Make Aliucord debuggable</string>
<string name="developer_mode">Developer Mode</string>
<string name="debuggable">Debuggable</string>
<string name="debuggable_description">Use debuggable manifest</string>
<string name="about">About</string>
<string name="team">Team</string>
<string name="contributors">Contributors</string>
<string name="loading">Loading…</string>
<string name="search_plugins">Search Plugins</string>
<string name="source_code">Source Code</string>
<string name="installer">Installer</string>
@ -41,4 +37,13 @@
<string name="theme_setting_description">Manager appearance</string>
<string name="no_plugins_installed">No plugins installed!</string>
<string name="store">Store</string>
<string name="paging_initial_load">Loading…</string>
<string name="contributor">Contributor</string>
<string name="apply">Apply</string>
<string name="confirm">Confirm</string>
<string name="dismiss">Dismiss</string>
<string name="download_apk">Download APK</string>
<string name="select_apk">Select APK</string>
<string name="download_method">Download Method</string>
<string name="changelog_media">Changelog Media</string>
</resources>

View file

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AliucordManager" parent="Theme.AppCompat.NoActionBar">
<style name="Theme.AliucordManager.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/Theme.AliucordManager</item>
</style>
<style name="Theme.AliucordManager" parent="Theme.AppCompat">
<item name="colorPrimary">@color/primary</item>
</style>
</resources>