diff --git a/.idea/GradleUpdaterPlugin.xml b/.idea/GradleUpdaterPlugin.xml new file mode 100644 index 0000000..e359d57 --- /dev/null +++ b/.idea/GradleUpdaterPlugin.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml index 30bab2a..d8e9561 100644 --- a/.idea/discord.xml +++ b/.idea/discord.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 46b502a..b12ff52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ import java.io.ByteArrayOutputStream plugins { id("com.android.application") kotlin("android") + kotlin("plugin.serialization") version "1.7.20" } android { @@ -13,8 +14,8 @@ android { applicationId = "dev.beefers.vendetta.manager" minSdk = 24 targetSdk = 33 - versionCode = 1000 - versionName = "1.0.0" + versionCode = 1010 + versionName = "1.0.1" buildConfigField("String", "GIT_BRANCH", "\"${getCurrentBranch()}\"") buildConfigField("String", "GIT_COMMIT", "\"${getLatestCommit()}\"") diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index ba9b13f..91b4614 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,7 +11,7 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 1, + "versionCode": 1000, "versionName": "1.0.0", "outputFile": "app-release.apk" } diff --git a/app/src/main/java/dev/beefers/vendetta/manager/di/HttpModule.kt b/app/src/main/java/dev/beefers/vendetta/manager/di/HttpModule.kt index 20d40dd..8e8760e 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/di/HttpModule.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/di/HttpModule.kt @@ -1,7 +1,7 @@ package dev.beefers.vendetta.manager.di -import dev.beefers.vendetta.manager.network.service.GithubService import dev.beefers.vendetta.manager.network.service.HttpService +import dev.beefers.vendetta.manager.network.service.RestService import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation @@ -14,7 +14,7 @@ val httpModule = module { fun provideJson() = Json { ignoreUnknownKeys = true - coerceInputValues = true + isLenient = true } fun provideHttpClient(json: Json) = HttpClient(CIO) { @@ -26,6 +26,6 @@ val httpModule = module { singleOf(::provideJson) singleOf(::provideHttpClient) singleOf(::HttpService) - singleOf(::GithubService) + singleOf(::RestService) } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/di/ManagerModule.kt b/app/src/main/java/dev/beefers/vendetta/manager/di/ManagerModule.kt index 299188a..4590030 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/di/ManagerModule.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/di/ManagerModule.kt @@ -1,6 +1,7 @@ package dev.beefers.vendetta.manager.di import dev.beefers.vendetta.manager.domain.manager.DownloadManager +import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -8,4 +9,5 @@ import org.koin.dsl.module val managerModule = module { singleOf(::DownloadManager) singleOf(::PreferenceManager) + singleOf(::InstallManager) } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/di/RepositoryModule.kt b/app/src/main/java/dev/beefers/vendetta/manager/di/RepositoryModule.kt index 5295dc9..6ba01d7 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/di/RepositoryModule.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/di/RepositoryModule.kt @@ -1,9 +1,9 @@ package dev.beefers.vendetta.manager.di -import dev.beefers.vendetta.manager.domain.repository.GithubRepository +import dev.beefers.vendetta.manager.domain.repository.RestRepository import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val repositoryModule = module { - singleOf(::GithubRepository) + singleOf(::RestRepository) } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/di/ViewModelModule.kt b/app/src/main/java/dev/beefers/vendetta/manager/di/ViewModelModule.kt index 87fa0c0..6f69613 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/di/ViewModelModule.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/di/ViewModelModule.kt @@ -1,5 +1,6 @@ package dev.beefers.vendetta.manager.di +import dev.beefers.vendetta.manager.ui.viewmodel.home.HomeViewModel import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel import dev.beefers.vendetta.manager.ui.viewmodel.main.MainViewModel import dev.beefers.vendetta.manager.ui.viewmodel.settings.SettingsViewModel @@ -10,4 +11,5 @@ val viewModelModule = module { factoryOf(::InstallerViewModel) factoryOf(::SettingsViewModel) factoryOf(::MainViewModel) + factoryOf(::HomeViewModel) } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/InstallManager.kt b/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/InstallManager.kt new file mode 100644 index 0000000..2234be2 --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/InstallManager.kt @@ -0,0 +1,66 @@ +package dev.beefers.vendetta.manager.domain.manager + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import dev.beefers.vendetta.manager.installer.service.InstallService + +class InstallManager( + private val context: Context, + private val prefs: PreferenceManager, +) { + + var current by mutableStateOf(null) + + init { + getInstalled() + } + + fun getInstalled() { + current = try { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + context.packageManager.getPackageInfo( + prefs.packageName.ifBlank { "dev.beefers.vendetta" }, + PackageManager.PackageInfoFlags.of( + 0L + ) + ) + } + + else -> { + context.packageManager.getPackageInfo( + prefs.packageName.ifBlank { "dev.beefers.vendetta" }, + 0 + ) + } + } + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + fun uninstall() { + current?.let { + val callbackIntent = Intent(context, InstallService::class.java).apply { + action = "vendetta.actions.ACTION_UNINSTALL" + } + val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE) + } else { + PendingIntent.getService(context, 0, callbackIntent, 0) + } + + context.packageManager.packageInstaller.uninstall( + it.packageName, + contentIntent.intentSender + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/PreferenceManager.kt b/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/PreferenceManager.kt index 002d00f..75ff376 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/PreferenceManager.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/domain/manager/PreferenceManager.kt @@ -5,6 +5,7 @@ import android.os.Build import androidx.annotation.StringRes import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.base.BasePreferenceManager +import dev.beefers.vendetta.manager.utils.DiscordVersion class PreferenceManager(private val context: Context) : BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) { @@ -25,6 +26,8 @@ class PreferenceManager(private val context: Context) : var theme by enumPreference("theme", Theme.SYSTEM) + var channel by enumPreference("channel", DiscordVersion.Type.STABLE) + } enum class Theme(@StringRes val labelRes: Int) { diff --git a/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/GithubRepository.kt b/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/GithubRepository.kt deleted file mode 100644 index 3a0c98e..0000000 --- a/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/GithubRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.beefers.vendetta.manager.domain.repository - -import dev.beefers.vendetta.manager.network.service.GithubService - -class GithubRepository( - private val service: GithubService -) { - - suspend fun getLatestRelease() = service.getLatestRelease() - -} \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt b/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt new file mode 100644 index 0000000..d433235 --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt @@ -0,0 +1,21 @@ +package dev.beefers.vendetta.manager.domain.repository + +import dev.beefers.vendetta.manager.network.service.RestService +import dev.beefers.vendetta.manager.network.utils.transform +import dev.beefers.vendetta.manager.utils.DiscordVersion + +class RestRepository( + private val service: RestService +) { + + suspend fun getLatestRelease() = service.getLatestRelease() + + suspend fun getLatestDiscordVersions() = service.getLatestDiscordVersions().transform { + mapOf( + DiscordVersion.Type.ALPHA to DiscordVersion.fromVersionCode(it.latest.alpha), + DiscordVersion.Type.BETA to DiscordVersion.fromVersionCode(it.latest.beta), + DiscordVersion.Type.STABLE to DiscordVersion.fromVersionCode(it.latest.stable) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/installer/service/InstallService.kt b/app/src/main/java/dev/beefers/vendetta/manager/installer/service/InstallService.kt index f27b3ca..8508480 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/installer/service/InstallService.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/installer/service/InstallService.kt @@ -5,10 +5,17 @@ import android.content.Intent import android.content.pm.PackageInstaller import android.os.IBinder import dev.beefers.vendetta.manager.R +import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.utils.showToast +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class InstallService : Service(), KoinComponent { + + private val installManager: InstallManager by inject() -class InstallService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + val isInstall = intent.action == "vendetta.actions.ACTION_INSTALL" when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { @Suppress("DEPRECATION") // No. @@ -18,11 +25,15 @@ class InstallService : Service() { startActivity(confirmationIntent) } - PackageInstaller.STATUS_SUCCESS -> showToast(R.string.installer_success) - PackageInstaller.STATUS_FAILURE_ABORTED -> showToast(R.string.installer_aborted) + PackageInstaller.STATUS_SUCCESS -> { + if(isInstall) showToast(R.string.installer_success) + installManager.getInstalled() + } + + PackageInstaller.STATUS_FAILURE_ABORTED -> if(isInstall) showToast(R.string.installer_aborted) else -> { - showToast(R.string.installer_failed, statusCode) + if(isInstall) showToast(R.string.installer_failed, statusCode) } } @@ -31,4 +42,5 @@ class InstallService : Service() { } override fun onBind(intent: Intent): IBinder? = null + } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/installer/util/PackageInstaller.kt b/app/src/main/java/dev/beefers/vendetta/manager/installer/util/PackageInstaller.kt index 7ada0d5..dba0f98 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/installer/util/PackageInstaller.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/installer/util/PackageInstaller.kt @@ -32,7 +32,9 @@ fun Context.installApks(silent: Boolean = false, vararg apks: File) { } } - val callbackIntent = Intent(this, InstallService::class.java) + val callbackIntent = Intent(this, InstallService::class.java).apply { + action = "vendetta.actions.ACTION_INSTALL" + } @SuppressLint("UnspecifiedImmutableFlag") val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/dto/Index.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/dto/Index.kt new file mode 100644 index 0000000..8ade0f9 --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/dto/Index.kt @@ -0,0 +1,17 @@ +package dev.beefers.vendetta.manager.network.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class Index( + val latest: Versions +) { + + @Serializable + data class Versions( + val alpha: String, + val beta: String, + val stable: String + ) + +} diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/service/GithubService.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt similarity index 65% rename from app/src/main/java/dev/beefers/vendetta/manager/network/service/GithubService.kt rename to app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt index 13c37df..03dde5e 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/network/service/GithubService.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt @@ -1,11 +1,12 @@ package dev.beefers.vendetta.manager.network.service +import dev.beefers.vendetta.manager.network.dto.Index import dev.beefers.vendetta.manager.network.dto.Release import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class GithubService( +class RestService( private val httpService: HttpService ) { @@ -15,4 +16,10 @@ class GithubService( } } + suspend fun getLatestDiscordVersions() = withContext(Dispatchers.IO) { + httpService.request { + url("https://discord.k6.tf/index.json") + } + } + } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt index 51f178b..794e717 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt @@ -23,4 +23,13 @@ val ApiResponse.dataOrThrow inline fun ApiResponse.ifSuccessful(block: (D) -> Unit) { if (this is ApiResponse.Success) block(data) -} \ No newline at end of file +} + +@Suppress("UNCHECKED_CAST") +fun ApiResponse.transform(block: (T) -> R): ApiResponse { + return when (this) { + is ApiResponse.Success -> ApiResponse.Success(block(data)) + is ApiResponse.Error -> this as ApiResponse.Error + is ApiResponse.Failure -> this as ApiResponse.Failure + } +} diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/components/SegmentedButton.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/components/SegmentedButton.kt new file mode 100644 index 0000000..388479d --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/components/SegmentedButton.kt @@ -0,0 +1,60 @@ +package dev.beefers.vendetta.manager.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun RowScope.SegmentedButton( + icon: Any, + iconDescription: String? = null, + text: String, + onClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + modifier = Modifier + .clickable(onClick = onClick) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) + .weight(1f) + .padding(16.dp) + ) { + when (icon) { + is ImageVector -> { + Icon( + imageVector = icon, + contentDescription = iconDescription, + tint = MaterialTheme.colorScheme.primary + ) + } + + is Painter -> { + Icon( + painter = icon, + contentDescription = iconDescription, + tint = MaterialTheme.colorScheme.primary + ) + } + } + + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt index ead2fc3..3317c6e 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt @@ -1,29 +1,49 @@ package dev.beefers.vendetta.manager.ui.screen.home +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.outlined.Home import androidx.compose.material3.Button +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.TabOptions import dev.beefers.vendetta.manager.R +import dev.beefers.vendetta.manager.domain.manager.PreferenceManager +import dev.beefers.vendetta.manager.ui.components.SegmentedButton import dev.beefers.vendetta.manager.ui.screen.installer.InstallerScreen +import dev.beefers.vendetta.manager.ui.viewmodel.home.HomeViewModel +import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.ManagerTab import dev.beefers.vendetta.manager.utils.TabOptions import dev.beefers.vendetta.manager.utils.navigate +import org.koin.androidx.compose.get class HomeScreen : ManagerTab { override val options: TabOptions @@ -36,24 +56,105 @@ class HomeScreen : ManagerTab { @Composable override fun Content() { val nav = LocalNavigator.currentOrThrow + val prefs: PreferenceManager = get() + val viewModel: HomeViewModel = getScreenModel() + val iconColor = when { + prefs.patchIcon -> Color(0xFF3AB8BA) + prefs.channel == DiscordVersion.Type.ALPHA -> Color(0xFFFBB33C) + else -> Color(0xFF5865F2) + } Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .padding(16.dp) - .fillMaxSize() + .fillMaxWidth() ) { - Button( - onClick = { nav.navigate(InstallerScreen()) }, + Image( + painter = painterResource(id = R.drawable.ic_discord_icon), + contentDescription = null, + modifier = Modifier + .size(60.dp) + .clip(CircleShape) + .background(iconColor) + ) + + Text( + text = prefs.appName, + style = MaterialTheme.typography.titleLarge + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - Text(stringResource(R.string.action_install)) + AnimatedVisibility(visible = viewModel.discordVersions != null) { + Text( + text = "Latest: ${viewModel.discordVersions!![prefs.channel]!!}", + style = MaterialTheme.typography.labelLarge, + color = LocalContentColor.current.copy(alpha = 0.5f), + textAlign = TextAlign.Center + ) + } + + AnimatedVisibility(visible = viewModel.installManager.current != null) { + Text( + text = "Current: ${viewModel.installManager.current!!.versionName}", + style = MaterialTheme.typography.labelLarge, + color = LocalContentColor.current.copy(alpha = 0.5f), + textAlign = TextAlign.Center + ) + } } - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "This UI is temporary, check back later for something prettier", - textAlign = TextAlign.Center, + + Button( + onClick = { + val version = viewModel.discordVersions!![prefs.channel]!! + nav.navigate(InstallerScreen(version)) + }, + enabled = viewModel.discordVersions != null, modifier = Modifier.fillMaxWidth() - ) + ) { + val label = when { + viewModel.installManager.current == null -> R.string.action_install + viewModel.installManager.current?.versionName == viewModel.discordVersions?.get( + prefs.channel + ).toString() -> R.string.action_reinstall + + else -> R.string.action_update + } + Text(stringResource(label)) + } + + AnimatedVisibility(visible = viewModel.installManager.current != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.clip(RoundedCornerShape(16.dp)) + ) { + SegmentedButton( + icon = Icons.Filled.OpenInNew, + text = stringResource(R.string.action_launch), + onClick = { viewModel.launchVendetta() } + ) + SegmentedButton( + icon = Icons.Filled.Info, + text = stringResource(R.string.action_info), + onClick = { viewModel.launchVendettaInfo() } + ) + SegmentedButton( + icon = Icons.Filled.Delete, + text = stringResource(R.string.action_uninstall), + onClick = { viewModel.uninstallVendetta() } + ) + } + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + + } } } diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt index f50b867..69dc962 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt @@ -3,7 +3,9 @@ package dev.beefers.vendetta.manager.ui.screen.installer import androidx.compose.foundation.layout.Arrangement 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.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -11,6 +13,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold @@ -32,13 +35,19 @@ import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel import dev.beefers.vendetta.manager.ui.widgets.installer.StepGroupCard +import dev.beefers.vendetta.manager.utils.DiscordVersion +import org.koin.core.parameter.parametersOf -class InstallerScreen : Screen { +class InstallerScreen( + val version: DiscordVersion +) : Screen { @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { - val viewModel: InstallerViewModel = getScreenModel() + val viewModel: InstallerViewModel = getScreenModel { + parametersOf(version) + } var expandedGroup by remember { mutableStateOf(null) @@ -65,20 +74,33 @@ class InstallerScreen : Screen { steps = viewModel.getSteps(group), ) } + if (viewModel.isFinished) { + Spacer(modifier = Modifier.height(16.dp)) + + viewModel.installManager.current?.let { + Button( + onClick = { viewModel.launchVendetta() }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.action_launch)) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() - .padding(16.dp) ) { - Button( + FilledTonalButton( onClick = { viewModel.copyDebugInfo() }, modifier = Modifier.weight(1f) ) { Text(stringResource(R.string.action_copy_logs)) } - Button( + FilledTonalButton( onClick = { viewModel.clearCache() }, modifier = Modifier.weight(1f) ) { diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/SettingsScreen.kt index 74b138b..7fdb78f 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/SettingsScreen.kt @@ -79,6 +79,16 @@ class SettingsScreen : ManagerTab { prefs.patchIcon = it } ) + SettingsItemChoice( + label = stringResource(R.string.settings_channel), + pref = prefs.channel, + labelFactory = { + ctx.getString(it.labelRes) + }, + onPrefChange = { + prefs.channel = it + } + ) SettingsButton( label = stringResource(R.string.action_clear_cache), diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt new file mode 100644 index 0000000..e31645d --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt @@ -0,0 +1,63 @@ +package dev.beefers.vendetta.manager.ui.viewmodel.home + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import dev.beefers.vendetta.manager.domain.manager.InstallManager +import dev.beefers.vendetta.manager.domain.manager.PreferenceManager +import dev.beefers.vendetta.manager.domain.repository.RestRepository +import dev.beefers.vendetta.manager.network.utils.dataOrNull +import dev.beefers.vendetta.manager.utils.DiscordVersion +import kotlinx.coroutines.launch + +class HomeViewModel( + private val repo: RestRepository, + val context: Context, + val prefs: PreferenceManager, + val installManager: InstallManager +) : ScreenModel { + + var discordVersions by mutableStateOf?>(null) + private set + + init { + getDiscordVersions() + } + + private fun getDiscordVersions() { + coroutineScope.launch { + discordVersions = repo.getLatestDiscordVersions().dataOrNull + } + } + + fun launchVendetta() { + installManager.current?.let { + val intent = context.packageManager.getLaunchIntentForPackage(it.packageName)?.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + } + + fun uninstallVendetta() { + installManager.uninstall() + } + + fun launchVendettaInfo() { + installManager.current?.let { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + data = Uri.parse("package:${it.packageName}") + context.startActivity(this) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt index 4843aad..27c51a2 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt @@ -1,6 +1,7 @@ package dev.beefers.vendetta.manager.ui.viewmodel.installer import android.content.Context +import android.content.Intent import android.os.Build import android.util.Log import androidx.annotation.StringRes @@ -17,11 +18,13 @@ import com.github.diamondminer88.zip.ZipWriter import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.DownloadManager +import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.util.ManifestPatcher import dev.beefers.vendetta.manager.installer.util.Patcher import dev.beefers.vendetta.manager.installer.util.installApks import dev.beefers.vendetta.manager.network.utils.Signer +import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.copyText import dev.beefers.vendetta.manager.utils.showToast import kotlinx.coroutines.Dispatchers @@ -36,7 +39,9 @@ import kotlin.time.measureTimedValue class InstallerViewModel( private val context: Context, private val downloadManager: DownloadManager, - private val preferences: PreferenceManager + private val preferences: PreferenceManager, + private val discordVersion: DiscordVersion, + val installManager: InstallManager ) : ScreenModel { private val installationRunning = AtomicBoolean(false) private val cacheDir = context.externalCacheDir!! @@ -82,6 +87,15 @@ class InstallerViewModel( context.showToast(R.string.msg_cleared_cache) } + fun launchVendetta() { + installManager.current?.let { + val intent = context.packageManager.getLaunchIntentForPackage(it.packageName)?.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + } + private val job = coroutineScope.launch(Dispatchers.Main) { if (installationRunning.getAndSet(true)) { return@launch @@ -131,7 +145,7 @@ class InstallerViewModel( InstallStatus.QUEUED ) - val version = preferences.discordVersion.ifBlank { "168018" } + val version = preferences.discordVersion.ifBlank { discordVersion.toVersionCode() } val arch = Build.SUPPORTED_ABIS.first() val discordCacheDir = cacheDir.resolve(version) val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() } diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/main/MainViewModel.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/main/MainViewModel.kt index c33546f..e7e54ef 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/main/MainViewModel.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/main/MainViewModel.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.setValue import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.coroutineScope import dev.beefers.vendetta.manager.domain.manager.DownloadManager -import dev.beefers.vendetta.manager.domain.repository.GithubRepository +import dev.beefers.vendetta.manager.domain.repository.RestRepository import dev.beefers.vendetta.manager.installer.util.installApks import dev.beefers.vendetta.manager.network.dto.Release import dev.beefers.vendetta.manager.network.utils.dataOrNull @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch import java.io.File class MainViewModel( - private val githubRepo: GithubRepository, + private val repo: RestRepository, private val downloadManager: DownloadManager, private val context: Context ) : ScreenModel { @@ -29,7 +29,7 @@ class MainViewModel( private fun checkForUpdate() { coroutineScope.launch { - release = githubRepo.getLatestRelease().dataOrNull + release = repo.getLatestRelease().dataOrNull } } diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/updater/UpdateDialog.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/updater/UpdateDialog.kt index b9b7974..1c0c543 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/updater/UpdateDialog.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/updater/UpdateDialog.kt @@ -20,7 +20,7 @@ fun UpdateDialog( onDismissRequest = {}, confirmButton = { FilledTonalButton(onClick = onConfirm) { - Text(stringResource(R.string.action_update)) + Text(stringResource(R.string.action_start_update)) } }, title = { diff --git a/app/src/main/java/dev/beefers/vendetta/manager/utils/VersionUtils.kt b/app/src/main/java/dev/beefers/vendetta/manager/utils/VersionUtils.kt index 6869d51..20e78b5 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/utils/VersionUtils.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/utils/VersionUtils.kt @@ -1,17 +1,25 @@ package dev.beefers.vendetta.manager.utils +import androidx.annotation.StringRes +import dev.beefers.vendetta.manager.R +import java.io.Serializable + data class DiscordVersion( val major: Int, val minor: Int, val type: Type -) { +) : Serializable { - enum class Type { - STABLE, - BETA, - ALPHA + enum class Type(val label: String, @StringRes val labelRes: Int) { + STABLE("Stable", R.string.channel_stable), + BETA("Beta", R.string.channel_beta), + ALPHA("Alpha", R.string.channel_alpha) } + override fun toString() = "$major.$minor - ${type.label}" + + fun toVersionCode() = "$major${type.ordinal}${if (minor < 10) 0 else ""}${minor}" + companion object { fun fromVersionCode(string: String): DiscordVersion? = with(string) { @@ -21,8 +29,8 @@ data class DiscordVersion( val typeInt = codeReversed[2].toString().toInt() val type = Type.values().getOrNull(typeInt) ?: return@with null DiscordVersion( - codeReversed.slice(3..codeReversed.lastIndex).toInt(), - codeReversed.substring(0, 2).toInt(), + codeReversed.slice(3..codeReversed.lastIndex).reversed().toInt(), + codeReversed.substring(0, 2).reversed().toInt(), type ) } diff --git a/app/src/main/res/drawable/ic_discord_icon.xml b/app/src/main/res/drawable/ic_discord_icon.xml new file mode 100644 index 0000000..e0ee934 --- /dev/null +++ b/app/src/main/res/drawable/ic_discord_icon.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e16e3d..14f3738 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,8 +35,13 @@ Copy logs Clear cache Confirm - Start update + Start update Install + Update + Reinstall + Launch + Uninstall + Info Cached @@ -58,6 +63,11 @@ App name Replace app icon Uses the Vendetta icon instead of Discord\'s + Release channel Vendetta Manager version %1$s is now available! + + Stable + Beta + Alpha \ No newline at end of file