diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fdb70d2..1134a2a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/app/libs/libzip-release.aar b/app/libs/libzip-release.aar new file mode 100644 index 0000000..3627f30 Binary files /dev/null and b/app/libs/libzip-release.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 80f29cd..6992941 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -17,4 +17,5 @@ -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile --keep class com.aliucord.manager.models.** { private final *; } \ No newline at end of file +-keep class com.aliucord.manager.models.** { private final *; } +-keep class com.aliucord.libzip.Zip { private final *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 497e75a..e4a2d24 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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.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() } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/Plugin.kt b/app/src/main/kotlin/com/aliucord/manager/models/Plugin.kt similarity index 61% rename from app/src/main/kotlin/com/aliucord/manager/utils/Plugin.kt rename to app/src/main/kotlin/com/aliucord/manager/models/Plugin.kt index 9a9d13d..83e3fce 100644 --- a/app/src/main/kotlin/com/aliucord/manager/utils/Plugin.kt +++ b/app/src/main/kotlin/com/aliucord/manager/models/Plugin.kt @@ -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 } } diff --git a/app/src/main/kotlin/com/aliucord/manager/models/Commit.kt b/app/src/main/kotlin/com/aliucord/manager/models/github/Commit.kt similarity index 90% rename from app/src/main/kotlin/com/aliucord/manager/models/Commit.kt rename to app/src/main/kotlin/com/aliucord/manager/models/github/Commit.kt index fbcd2e0..e860632 100644 --- a/app/src/main/kotlin/com/aliucord/manager/models/Commit.kt +++ b/app/src/main/kotlin/com/aliucord/manager/models/github/Commit.kt @@ -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 diff --git a/app/src/main/kotlin/com/aliucord/manager/models/GithubUser.kt b/app/src/main/kotlin/com/aliucord/manager/models/github/GithubUser.kt similarity index 77% rename from app/src/main/kotlin/com/aliucord/manager/models/GithubUser.kt rename to app/src/main/kotlin/com/aliucord/manager/models/github/GithubUser.kt index 1eb55ac..341412b 100644 --- a/app/src/main/kotlin/com/aliucord/manager/models/GithubUser.kt +++ b/app/src/main/kotlin/com/aliucord/manager/models/github/GithubUser.kt @@ -1,4 +1,4 @@ -package com.aliucord.manager.models +package com.aliucord.manager.models.github import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/aliucord/manager/models/Version.kt b/app/src/main/kotlin/com/aliucord/manager/models/github/Version.kt similarity index 71% rename from app/src/main/kotlin/com/aliucord/manager/models/Version.kt rename to app/src/main/kotlin/com/aliucord/manager/models/github/Version.kt index 181f06c..41cd878 100644 --- a/app/src/main/kotlin/com/aliucord/manager/models/Version.kt +++ b/app/src/main/kotlin/com/aliucord/manager/models/github/Version.kt @@ -1,4 +1,4 @@ -package com.aliucord.manager.models +package com.aliucord.manager.models.github data class Version( val versionCode: String, diff --git a/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt b/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt index 2fd9832..1f3f42a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt +++ b/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt @@ -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( private val key: String, @@ -18,7 +18,7 @@ class Preference( 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( value.value = newValue - sharedPreferences!!.edit { setter(key, newValue) } + sharedPreferences.edit { setter(key, newValue) } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt b/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt index 5c64556..782d75e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt +++ b/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt @@ -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") } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/Screen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/Screen.kt deleted file mode 100644 index a985bb1..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/Screen.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt index c0e78cb..8b3fe0b 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt @@ -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)) } ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/ListItem.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/ListItem.kt new file mode 100644 index 0000000..e863c97 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/ListItem.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/ManagerScaffold.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/ManagerScaffold.kt new file mode 100644 index 0000000..d59eadf --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/ManagerScaffold.kt @@ -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() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt index 00e1f10..0958c64 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorEntry.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorEntry.kt index a031790..edb9253 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorEntry.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorEntry.kt @@ -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) ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt index 7d56cf9..2700ed9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt @@ -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 ) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt index 3daaa5c..5b217e9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt @@ -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") diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/UserEntry.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/UserEntry.kt index 38ea1ec..e23aedb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/UserEntry.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/UserEntry.kt @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallerDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallerDialog.kt index e529925..5cdfcf5 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallerDialog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallerDialog.kt @@ -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, - 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 } -) \ No newline at end of file + }, + onDismissRequest = onDismissRequest + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt index 2069f48..e12638d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt @@ -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) ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt index 3bd82a5..efffecb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt index 0fc3c1f..e8c4d94 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt @@ -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 +) = ListItem( + icon = icon, + modifier = Modifier.clickable { + preference.set(!preference.get()) + }, + text = title, + secondaryText = description, + trailing = { + Switch( + checked = preference.get(), + onCheckedChange = preference::set + ) } -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/SettingSwitch.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/SettingSwitch.kt deleted file mode 100644 index 16d9d91..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/SettingSwitch.kt +++ /dev/null @@ -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 -) = Switch( - modifier = modifier, - checked = preference.get(), - onCheckedChange = preference::set -) \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeDialog.kt new file mode 100644 index 0000000..1b0430a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeDialog.kt @@ -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)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeSetting.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeSetting.kt index a23d67d..ff49a95 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeSetting.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeSetting.kt @@ -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) { - 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) } - ) -} \ No newline at end of file + } +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt index e51f170..d8f3452 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt @@ -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 diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt index df76c11..2428f10 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt @@ -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() } - - 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() { + override suspend fun load(params: LoadParams): LoadResult { + 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) = 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) + ) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt index d615b4f..4f0bd2d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt @@ -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(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(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) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt index 0438fea..affab81 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt index 80d29c6..59d1f76 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt @@ -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(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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt index 5c93f54..f1388f6 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt @@ -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() } diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/Constants.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Const.kt similarity index 100% rename from app/src/main/kotlin/com/aliucord/manager/utils/Constants.kt rename to app/src/main/kotlin/com/aliucord/manager/utils/Const.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt b/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt index 855d00b..049ead7 100644 --- a/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt index 744d679..7dab008 100644 --- a/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt @@ -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>() + private val commitsCache = HashMap>() - suspend fun getCommits(vararg params: Pair): Array { + suspend fun getCommits(vararg params: Pair): List { 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>(commitsUrl) { + val res = httpClient.get>(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" } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/Signer.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt similarity index 96% rename from app/src/main/kotlin/com/aliucord/manager/Signer.kt rename to app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt index 228e924..8652b1f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/Signer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt @@ -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() - val filesToRemove = ArrayList() + val digests = linkedMapOf() + val filesToRemove = arrayListOf() + 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()) diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/ZipUtils.kt b/app/src/main/kotlin/com/aliucord/manager/utils/ZipUtils.kt new file mode 100644 index 0000000..5936447 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/utils/ZipUtils.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 806fd99..27cd126 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,25 +1,21 @@ Aliucord Manager - Clear files cache (Injector.dex and patched manifest) + Clear files cache Commits Dex location Open settings You need to grant storage permission to use this app Grant permission - Replace icon background with Aliucord\'s + Replace app icon with Aliucord logo Settings Show menu Theme Use Aliucord.dex from storage Plugins - Launch Plugins Page - %1$s v%2$s - By %1$s Uninstall %1$s Search View changelog for %1$s Are you sure you want to uninstall this plugin? This action cannot be undone. - Delete Close Support Server Back @@ -27,12 +23,12 @@ Install Reinstall Update - Enable Developer Mode - Make Aliucord debuggable + Developer Mode + Debuggable + Use debuggable manifest About Team Contributors - Loading… Search Plugins Source Code Installer @@ -41,4 +37,13 @@ Manager appearance No plugins installed! Store + Loading… + Contributor + Apply + Confirm + Dismiss + Download APK + Select APK + Download Method + Changelog Media \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 884dc47..aebbc33 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,6 +1,10 @@ - + \ No newline at end of file