Feat: Initial home ui + release channel option

This commit is contained in:
wingio 2023-03-17 14:50:49 -04:00
parent 92a650b067
commit d6fa04f024
28 changed files with 491 additions and 59 deletions

6
.idea/GradleUpdaterPlugin.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="me.schlaubi.intellij_gradle_version_checker.settings.ProjectPersistentGradleVersionSettings">
<option name="ignoreOutdatedVersion" value="true" />
</component>
</project>

2
.idea/discord.xml generated
View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DiscordProjectSettings"> <component name="DiscordProjectSettings">
<option name="show" value="ASK" /> <option name="show" value="PROJECT_FILES" />
<option name="description" value="" /> <option name="description" value="" />
</component> </component>
</project> </project>

View file

@ -3,6 +3,7 @@ import java.io.ByteArrayOutputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
kotlin("plugin.serialization") version "1.7.20"
} }
android { android {
@ -13,8 +14,8 @@ android {
applicationId = "dev.beefers.vendetta.manager" applicationId = "dev.beefers.vendetta.manager"
minSdk = 24 minSdk = 24
targetSdk = 33 targetSdk = 33
versionCode = 1000 versionCode = 1010
versionName = "1.0.0" versionName = "1.0.1"
buildConfigField("String", "GIT_BRANCH", "\"${getCurrentBranch()}\"") buildConfigField("String", "GIT_BRANCH", "\"${getCurrentBranch()}\"")
buildConfigField("String", "GIT_COMMIT", "\"${getLatestCommit()}\"") buildConfigField("String", "GIT_COMMIT", "\"${getLatestCommit()}\"")

View file

@ -11,7 +11,7 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 1, "versionCode": 1000,
"versionName": "1.0.0", "versionName": "1.0.0",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }

View file

@ -1,7 +1,7 @@
package dev.beefers.vendetta.manager.di 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.HttpService
import dev.beefers.vendetta.manager.network.service.RestService
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
@ -14,7 +14,7 @@ val httpModule = module {
fun provideJson() = Json { fun provideJson() = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
coerceInputValues = true isLenient = true
} }
fun provideHttpClient(json: Json) = HttpClient(CIO) { fun provideHttpClient(json: Json) = HttpClient(CIO) {
@ -26,6 +26,6 @@ val httpModule = module {
singleOf(::provideJson) singleOf(::provideJson)
singleOf(::provideHttpClient) singleOf(::provideHttpClient)
singleOf(::HttpService) singleOf(::HttpService)
singleOf(::GithubService) singleOf(::RestService)
} }

View file

@ -1,6 +1,7 @@
package dev.beefers.vendetta.manager.di package dev.beefers.vendetta.manager.di
import dev.beefers.vendetta.manager.domain.manager.DownloadManager 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.domain.manager.PreferenceManager
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -8,4 +9,5 @@ import org.koin.dsl.module
val managerModule = module { val managerModule = module {
singleOf(::DownloadManager) singleOf(::DownloadManager)
singleOf(::PreferenceManager) singleOf(::PreferenceManager)
singleOf(::InstallManager)
} }

View file

@ -1,9 +1,9 @@
package dev.beefers.vendetta.manager.di 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.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val repositoryModule = module { val repositoryModule = module {
singleOf(::GithubRepository) singleOf(::RestRepository)
} }

View file

@ -1,5 +1,6 @@
package dev.beefers.vendetta.manager.di 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.installer.InstallerViewModel
import dev.beefers.vendetta.manager.ui.viewmodel.main.MainViewModel import dev.beefers.vendetta.manager.ui.viewmodel.main.MainViewModel
import dev.beefers.vendetta.manager.ui.viewmodel.settings.SettingsViewModel import dev.beefers.vendetta.manager.ui.viewmodel.settings.SettingsViewModel
@ -10,4 +11,5 @@ val viewModelModule = module {
factoryOf(::InstallerViewModel) factoryOf(::InstallerViewModel)
factoryOf(::SettingsViewModel) factoryOf(::SettingsViewModel)
factoryOf(::MainViewModel) factoryOf(::MainViewModel)
factoryOf(::HomeViewModel)
} }

View file

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

View file

@ -5,6 +5,7 @@ import android.os.Build
import androidx.annotation.StringRes import androidx.annotation.StringRes
import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.domain.manager.base.BasePreferenceManager import dev.beefers.vendetta.manager.domain.manager.base.BasePreferenceManager
import dev.beefers.vendetta.manager.utils.DiscordVersion
class PreferenceManager(private val context: Context) : class PreferenceManager(private val context: Context) :
BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) { 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 theme by enumPreference("theme", Theme.SYSTEM)
var channel by enumPreference("channel", DiscordVersion.Type.STABLE)
} }
enum class Theme(@StringRes val labelRes: Int) { enum class Theme(@StringRes val labelRes: Int) {

View file

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

View file

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

View file

@ -5,10 +5,17 @@ import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.IBinder import android.os.IBinder
import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.domain.manager.InstallManager
import dev.beefers.vendetta.manager.utils.showToast 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 { 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)) { when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
@Suppress("DEPRECATION") // No. @Suppress("DEPRECATION") // No.
@ -18,11 +25,15 @@ class InstallService : Service() {
startActivity(confirmationIntent) startActivity(confirmationIntent)
} }
PackageInstaller.STATUS_SUCCESS -> showToast(R.string.installer_success) PackageInstaller.STATUS_SUCCESS -> {
PackageInstaller.STATUS_FAILURE_ABORTED -> showToast(R.string.installer_aborted) if(isInstall) showToast(R.string.installer_success)
installManager.getInstalled()
}
PackageInstaller.STATUS_FAILURE_ABORTED -> if(isInstall) showToast(R.string.installer_aborted)
else -> { 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 override fun onBind(intent: Intent): IBinder? = null
} }

View file

@ -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") @SuppressLint("UnspecifiedImmutableFlag")
val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

View file

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

View file

@ -1,11 +1,12 @@
package dev.beefers.vendetta.manager.network.service package dev.beefers.vendetta.manager.network.service
import dev.beefers.vendetta.manager.network.dto.Index
import dev.beefers.vendetta.manager.network.dto.Release import dev.beefers.vendetta.manager.network.dto.Release
import io.ktor.client.request.url import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class GithubService( class RestService(
private val httpService: HttpService private val httpService: HttpService
) { ) {
@ -15,4 +16,10 @@ class GithubService(
} }
} }
suspend fun getLatestDiscordVersions() = withContext(Dispatchers.IO) {
httpService.request<Index> {
url("https://discord.k6.tf/index.json")
}
}
} }

View file

@ -24,3 +24,12 @@ val <D> ApiResponse<D>.dataOrThrow
inline fun <D> ApiResponse<D>.ifSuccessful(block: (D) -> Unit) { inline fun <D> ApiResponse<D>.ifSuccessful(block: (D) -> Unit) {
if (this is ApiResponse.Success) block(data) if (this is ApiResponse.Success) block(data)
} }
@Suppress("UNCHECKED_CAST")
fun <T, R> ApiResponse<T>.transform(block: (T) -> R): ApiResponse<R> {
return when (this) {
is ApiResponse.Success -> ApiResponse.Success(block(data))
is ApiResponse.Error -> this as ApiResponse.Error<R>
is ApiResponse.Failure -> this as ApiResponse.Failure<R>
}
}

View file

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

View file

@ -1,29 +1,49 @@
package dev.beefers.vendetta.manager.ui.screen.home 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Home 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.material.icons.outlined.Home
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import dev.beefers.vendetta.manager.R 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.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.ManagerTab
import dev.beefers.vendetta.manager.utils.TabOptions import dev.beefers.vendetta.manager.utils.TabOptions
import dev.beefers.vendetta.manager.utils.navigate import dev.beefers.vendetta.manager.utils.navigate
import org.koin.androidx.compose.get
class HomeScreen : ManagerTab { class HomeScreen : ManagerTab {
override val options: TabOptions override val options: TabOptions
@ -36,24 +56,105 @@ class HomeScreen : ManagerTab {
@Composable @Composable
override fun Content() { override fun Content() {
val nav = LocalNavigator.currentOrThrow 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( Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(16.dp)
.fillMaxSize() .fillMaxWidth()
) { ) {
Button( Image(
onClick = { nav.navigate(InstallerScreen()) }, 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() 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( Button(
text = "This UI is temporary, check back later for something prettier", onClick = {
textAlign = TextAlign.Center, val version = viewModel.discordVersions!![prefs.channel]!!
nav.navigate(InstallerScreen(version))
},
enabled = viewModel.discordVersions != null,
modifier = Modifier.fillMaxWidth() 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)
) {
}
} }
} }

View file

@ -3,7 +3,9 @@ package dev.beefers.vendetta.manager.ui.screen.installer
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll 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.material.icons.filled.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold 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.R
import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel
import dev.beefers.vendetta.manager.ui.widgets.installer.StepGroupCard 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
override fun Content() { override fun Content() {
val viewModel: InstallerViewModel = getScreenModel() val viewModel: InstallerViewModel = getScreenModel {
parametersOf(version)
}
var expandedGroup by remember { var expandedGroup by remember {
mutableStateOf<InstallerViewModel.InstallStepGroup?>(null) mutableStateOf<InstallerViewModel.InstallStepGroup?>(null)
@ -65,20 +74,33 @@ class InstallerScreen : Screen {
steps = viewModel.getSteps(group), steps = viewModel.getSteps(group),
) )
} }
if (viewModel.isFinished) { 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( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp)
) { ) {
Button( FilledTonalButton(
onClick = { viewModel.copyDebugInfo() }, onClick = { viewModel.copyDebugInfo() },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text(stringResource(R.string.action_copy_logs)) Text(stringResource(R.string.action_copy_logs))
} }
Button( FilledTonalButton(
onClick = { viewModel.clearCache() }, onClick = { viewModel.clearCache() },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {

View file

@ -79,6 +79,16 @@ class SettingsScreen : ManagerTab {
prefs.patchIcon = it prefs.patchIcon = it
} }
) )
SettingsItemChoice(
label = stringResource(R.string.settings_channel),
pref = prefs.channel,
labelFactory = {
ctx.getString(it.labelRes)
},
onPrefChange = {
prefs.channel = it
}
)
SettingsButton( SettingsButton(
label = stringResource(R.string.action_clear_cache), label = stringResource(R.string.action_clear_cache),

View file

@ -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<Map<DiscordVersion.Type, DiscordVersion?>?>(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)
}
}
}
}

View file

@ -1,6 +1,7 @@
package dev.beefers.vendetta.manager.ui.viewmodel.installer package dev.beefers.vendetta.manager.ui.viewmodel.installer
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.StringRes 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.BuildConfig
import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.domain.manager.DownloadManager 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.domain.manager.PreferenceManager
import dev.beefers.vendetta.manager.installer.util.ManifestPatcher import dev.beefers.vendetta.manager.installer.util.ManifestPatcher
import dev.beefers.vendetta.manager.installer.util.Patcher import dev.beefers.vendetta.manager.installer.util.Patcher
import dev.beefers.vendetta.manager.installer.util.installApks import dev.beefers.vendetta.manager.installer.util.installApks
import dev.beefers.vendetta.manager.network.utils.Signer 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.copyText
import dev.beefers.vendetta.manager.utils.showToast import dev.beefers.vendetta.manager.utils.showToast
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,7 +39,9 @@ import kotlin.time.measureTimedValue
class InstallerViewModel( class InstallerViewModel(
private val context: Context, private val context: Context,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val preferences: PreferenceManager private val preferences: PreferenceManager,
private val discordVersion: DiscordVersion,
val installManager: InstallManager
) : ScreenModel { ) : ScreenModel {
private val installationRunning = AtomicBoolean(false) private val installationRunning = AtomicBoolean(false)
private val cacheDir = context.externalCacheDir!! private val cacheDir = context.externalCacheDir!!
@ -82,6 +87,15 @@ class InstallerViewModel(
context.showToast(R.string.msg_cleared_cache) 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) { private val job = coroutineScope.launch(Dispatchers.Main) {
if (installationRunning.getAndSet(true)) { if (installationRunning.getAndSet(true)) {
return@launch return@launch
@ -131,7 +145,7 @@ class InstallerViewModel(
InstallStatus.QUEUED InstallStatus.QUEUED
) )
val version = preferences.discordVersion.ifBlank { "168018" } val version = preferences.discordVersion.ifBlank { discordVersion.toVersionCode() }
val arch = Build.SUPPORTED_ABIS.first() val arch = Build.SUPPORTED_ABIS.first()
val discordCacheDir = cacheDir.resolve(version) val discordCacheDir = cacheDir.resolve(version)
val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() } val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() }

View file

@ -7,7 +7,7 @@ import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import dev.beefers.vendetta.manager.domain.manager.DownloadManager 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.installer.util.installApks
import dev.beefers.vendetta.manager.network.dto.Release import dev.beefers.vendetta.manager.network.dto.Release
import dev.beefers.vendetta.manager.network.utils.dataOrNull import dev.beefers.vendetta.manager.network.utils.dataOrNull
@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
import java.io.File import java.io.File
class MainViewModel( class MainViewModel(
private val githubRepo: GithubRepository, private val repo: RestRepository,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val context: Context private val context: Context
) : ScreenModel { ) : ScreenModel {
@ -29,7 +29,7 @@ class MainViewModel(
private fun checkForUpdate() { private fun checkForUpdate() {
coroutineScope.launch { coroutineScope.launch {
release = githubRepo.getLatestRelease().dataOrNull release = repo.getLatestRelease().dataOrNull
} }
} }

View file

@ -20,7 +20,7 @@ fun UpdateDialog(
onDismissRequest = {}, onDismissRequest = {},
confirmButton = { confirmButton = {
FilledTonalButton(onClick = onConfirm) { FilledTonalButton(onClick = onConfirm) {
Text(stringResource(R.string.action_update)) Text(stringResource(R.string.action_start_update))
} }
}, },
title = { title = {

View file

@ -1,17 +1,25 @@
package dev.beefers.vendetta.manager.utils package dev.beefers.vendetta.manager.utils
import androidx.annotation.StringRes
import dev.beefers.vendetta.manager.R
import java.io.Serializable
data class DiscordVersion( data class DiscordVersion(
val major: Int, val major: Int,
val minor: Int, val minor: Int,
val type: Type val type: Type
) { ) : Serializable {
enum class Type { enum class Type(val label: String, @StringRes val labelRes: Int) {
STABLE, STABLE("Stable", R.string.channel_stable),
BETA, BETA("Beta", R.string.channel_beta),
ALPHA 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 { companion object {
fun fromVersionCode(string: String): DiscordVersion? = with(string) { fun fromVersionCode(string: String): DiscordVersion? = with(string) {
@ -21,8 +29,8 @@ data class DiscordVersion(
val typeInt = codeReversed[2].toString().toInt() val typeInt = codeReversed[2].toString().toInt()
val type = Type.values().getOrNull(typeInt) ?: return@with null val type = Type.values().getOrNull(typeInt) ?: return@with null
DiscordVersion( DiscordVersion(
codeReversed.slice(3..codeReversed.lastIndex).toInt(), codeReversed.slice(3..codeReversed.lastIndex).reversed().toInt(),
codeReversed.substring(0, 2).toInt(), codeReversed.substring(0, 2).reversed().toInt(),
type type
) )
} }

View file

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h512v512h-512z"/>
<path android:fillColor="#FFFFFF" android:pathData="M366.27,150.56C346.2,141.35 324.68,134.57 302.17,130.68C301.77,130.61 301.36,130.79 301.15,131.16C298.38,136.08 295.31,142.51 293.16,147.56C268.97,143.94 244.89,143.94 221.18,147.56C219.03,142.4 215.85,136.08 213.08,131.16C212.86,130.8 212.46,130.62 212.04,130.68C189.55,134.55 168.03,141.34 147.96,150.56C147.77,150.63 147.63,150.76 147.53,150.92C106.71,211.91 95.53,271.4 101.01,330.15C101.03,330.43 101.19,330.7 101.42,330.88C128.36,350.66 154.45,362.67 180.05,370.62C180.46,370.75 180.9,370.6 181.15,370.26C187.2,362 192.6,353.28 197.24,344.1C197.51,343.56 197.25,342.92 196.69,342.71C188.13,339.46 179.98,335.5 172.13,331.01C171.51,330.65 171.46,329.75 172.03,329.33C173.68,328.1 175.34,326.8 176.9,325.5C177.19,325.27 177.59,325.23 177.93,325.38C229.46,348.9 285.24,348.9 336.17,325.38C336.51,325.2 336.9,325.26 337.2,325.49C338.77,326.79 340.43,328.1 342.09,329.33C342.66,329.75 342.63,330.65 342.01,331.01C334.16,335.59 326.01,339.46 317.42,342.7C316.87,342.91 316.63,343.56 316.89,344.1C321.62,353.26 327.02,361.98 332.97,370.25C333.21,370.6 333.66,370.75 334.08,370.62C359.8,362.67 385.89,350.66 412.83,330.88C413.06,330.7 413.21,330.45 413.23,330.16C419.8,262.24 402.24,203.23 366.67,150.93C366.59,150.76 366.44,150.63 366.27,150.56ZM204.93,294.37C189.41,294.37 176.64,280.13 176.64,262.63C176.64,245.14 189.17,230.9 204.93,230.9C220.82,230.9 233.48,245.27 233.23,262.63C233.23,280.13 220.7,294.37 204.93,294.37ZM309.56,294.37C294.05,294.37 281.26,280.13 281.26,262.63C281.26,245.14 293.79,230.9 309.56,230.9C325.45,230.9 338.11,245.27 337.86,262.63C337.86,280.13 325.45,294.37 309.56,294.37Z"/>
</group>
</vector>

View file

@ -35,8 +35,13 @@
<string name="action_copy_logs">Copy logs</string> <string name="action_copy_logs">Copy logs</string>
<string name="action_clear_cache">Clear cache</string> <string name="action_clear_cache">Clear cache</string>
<string name="action_confirm">Confirm</string> <string name="action_confirm">Confirm</string>
<string name="action_update">Start update</string> <string name="action_start_update">Start update</string>
<string name="action_install">Install</string> <string name="action_install">Install</string>
<string name="action_update">Update</string>
<string name="action_reinstall">Reinstall</string>
<string name="action_launch">Launch</string>
<string name="action_uninstall">Uninstall</string>
<string name="action_info">Info</string>
<string name="installer_cached">Cached</string> <string name="installer_cached">Cached</string>
@ -58,6 +63,11 @@
<string name="settings_app_name">App name</string> <string name="settings_app_name">App name</string>
<string name="settings_app_icon">Replace app icon</string> <string name="settings_app_icon">Replace app icon</string>
<string name="settings_app_icon_description">Uses the Vendetta icon instead of Discord\'s</string> <string name="settings_app_icon_description">Uses the Vendetta icon instead of Discord\'s</string>
<string name="settings_channel">Release channel</string>
<string name="update_description">Vendetta Manager version %1$s is now available!</string> <string name="update_description">Vendetta Manager version %1$s is now available!</string>
<string name="channel_stable">Stable</string>
<string name="channel_beta">Beta</string>
<string name="channel_alpha">Alpha</string>
</resources> </resources>