forked from geode-sdk/android-launcher
the switchover
This commit is contained in:
9 files changed
+772
-746
No files matched your search
@@ -47,7 +47,7 @@
|
|||||||
android:theme="@style/Theme.GeodeLauncher"
|
android:theme="@style/Theme.GeodeLauncher"
|
||||||
tools:targetApi="35">
|
tools:targetApi="35">
|
||||||
<activity
|
<activity
|
||||||
android:name=".AltMainActivity"
|
android:name=".LegacyMainActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.GeodeLauncher" />
|
android:theme="@style/Theme.GeodeLauncher" />
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -1,593 +0,0 @@
|
|||||||
package com.geode.launcher
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalResources
|
|
||||||
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 androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.geode.launcher.main.*
|
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
|
||||||
import com.geode.launcher.ui.theme.Theme
|
|
||||||
import com.geode.launcher.ui.theme.launcherTitleStyle
|
|
||||||
import com.geode.launcher.updater.ReleaseManager
|
|
||||||
import com.geode.launcher.utils.Constants
|
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.net.ConnectException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
|
|
||||||
class AltMainActivity : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
|
||||||
clearDownloadedApks(this)
|
|
||||||
|
|
||||||
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
|
||||||
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
|
||||||
|
|
||||||
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
|
||||||
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION") // the new api is android 13 only (why)
|
|
||||||
val returnError = intent.extras?.getSerializable(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR)
|
|
||||||
as? LaunchUtils.LauncherError
|
|
||||||
|
|
||||||
val loadFailureInfo = if (returnError != null && returnMessage != null) {
|
|
||||||
LoadFailureInfo(returnError, returnMessage, returnExtendedMessage)
|
|
||||||
} else if (LaunchUtils.lastSessionCrashed(this)) {
|
|
||||||
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
|
||||||
} else { null }
|
|
||||||
|
|
||||||
|
|
||||||
setContent {
|
|
||||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
|
||||||
val theme = Theme.fromInt(themeOption)
|
|
||||||
|
|
||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
|
||||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
|
||||||
|
|
||||||
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
|
||||||
|
|
||||||
if (loadFailureInfo != null) {
|
|
||||||
launchViewModel.loadFailure = loadFailureInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
AltMainScreen(launchViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gdInstalled && geodeInstalled) {
|
|
||||||
intent.getBooleanExtra("restarted", false).let {
|
|
||||||
if (it) {
|
|
||||||
onLaunch(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LaunchStatusInfo(
|
|
||||||
val title: String?,
|
|
||||||
val details: String? = null,
|
|
||||||
val progress: (() -> Float)? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifier: Modifier = Modifier) {
|
|
||||||
when (cancelReason) {
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
|
|
||||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
|
|
||||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
|
|
||||||
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
|
||||||
return when (cancelReason) {
|
|
||||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.launcher_cancelled_crash)
|
|
||||||
)
|
|
||||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.geode_download_title)
|
|
||||||
)
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.launcher_cancelled_error),
|
|
||||||
details = stringResource(R.string.game_not_found)
|
|
||||||
)
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.launcher_cancelled_error),
|
|
||||||
details = stringResource(R.string.launcher_cancelled_outdated)
|
|
||||||
)
|
|
||||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
|
||||||
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
|
||||||
title = null
|
|
||||||
)
|
|
||||||
/*
|
|
||||||
else -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.launcher_cancelled_manual)
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
|
|
||||||
return when (state) {
|
|
||||||
is LaunchViewModel.LaunchUIState.Initial,
|
|
||||||
is LaunchViewModel.LaunchUIState.Working,
|
|
||||||
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.launcher_starting_game),
|
|
||||||
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
|
|
||||||
)
|
|
||||||
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.release_fetch_in_progress)
|
|
||||||
)
|
|
||||||
is LaunchViewModel.LaunchUIState.Updating -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val downloaded = remember(state.downloaded) {
|
|
||||||
Formatter.formatShortFileSize(context, state.downloaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
val outOf = remember(state.outOf) {
|
|
||||||
state.outOf?.run {
|
|
||||||
Formatter.formatShortFileSize(context, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchStatusInfo(
|
|
||||||
title = stringResource(R.string.launcher_downloading_update),
|
|
||||||
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
|
|
||||||
progress = {
|
|
||||||
val progress = state.downloaded / state.outOf!!.toDouble()
|
|
||||||
progress.toFloat()
|
|
||||||
}.takeIf { state.outOf != null }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
|
||||||
return mapCancelReasonToInfo(state.reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
|
|
||||||
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = modifier.width(300.dp)
|
|
||||||
) {
|
|
||||||
if (statusInfo.title != null) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
|
||||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
|
||||||
icon()
|
|
||||||
Spacer(Modifier.size(8.dp))
|
|
||||||
Text(statusInfo.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inProgress) {
|
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusInfo.details != null) {
|
|
||||||
Text(
|
|
||||||
statusInfo.details,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
modifier = modifier.width(300.dp)
|
|
||||||
) {
|
|
||||||
if (statusInfo.title != null) {
|
|
||||||
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusInfo.progress != null) {
|
|
||||||
LinearProgressIndicator(
|
|
||||||
statusInfo.progress,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusInfo.details != null) {
|
|
||||||
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
|
||||||
when (reason) {
|
|
||||||
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_resume),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_cancelled_resume))
|
|
||||||
}
|
|
||||||
LaunchViewModel.LaunchCancelReason.MANUAL,
|
|
||||||
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_play_arrow),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_launch))
|
|
||||||
}
|
|
||||||
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_refresh),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_cancelled_retry))
|
|
||||||
}
|
|
||||||
// these shouldn't be allowing retries anyways
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
|
|
||||||
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
fun LaunchProgressCard(
|
|
||||||
uiState: LaunchViewModel.LaunchUIState,
|
|
||||||
crashInfo: LoadFailureInfo?,
|
|
||||||
onCancel: () -> Unit,
|
|
||||||
onResume: (safeMode: Boolean) -> Unit,
|
|
||||||
onMore: () -> Unit,
|
|
||||||
extraOptions: @Composable () -> Unit,
|
|
||||||
safeModeEnabled: Boolean,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
|
||||||
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
||||||
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
|
||||||
LaunchCancelledBody(
|
|
||||||
statusInfo = status,
|
|
||||||
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
|
|
||||||
inProgress = uiState.inProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
if (uiState.reason.isGameInstallIssue()) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
|
||||||
|
|
||||||
if (showDownload) {
|
|
||||||
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.reason.allowsRetry()) {
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
) {
|
|
||||||
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
|
||||||
showSafeModeDialog = true
|
|
||||||
}) {
|
|
||||||
RetryButtonContents(uiState.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crashInfo != null) {
|
|
||||||
FilledTonalButton(onClick = onMore) {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_question_mark),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extraOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
|
||||||
TextButton(onClick = onCancel) {
|
|
||||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.weight(1.0f))
|
|
||||||
|
|
||||||
extraOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSafeModeDialog) {
|
|
||||||
SafeModeDialog(
|
|
||||||
onDismiss = {
|
|
||||||
showSafeModeDialog = false
|
|
||||||
},
|
|
||||||
onLaunch = {
|
|
||||||
onResume(true)
|
|
||||||
showSafeModeDialog = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ExtraOptions(onSettings: () -> Unit) {
|
|
||||||
IconButton(
|
|
||||||
onClick = onSettings,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.icon_settings),
|
|
||||||
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GeodeLogo(modifier: Modifier = Modifier) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.geode_monochrome),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(64.dp, 64.dp),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.launcher_title),
|
|
||||||
style = launcherTitleStyle,
|
|
||||||
fontSize = 64.sp,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(12.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Unit) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val resources = LocalResources.current
|
|
||||||
|
|
||||||
val releaseState = ReleaseManager.get(context).uiState.value
|
|
||||||
|
|
||||||
if (releaseState !is ReleaseManager.ReleaseManagerState.Failure) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val message = when (releaseState.exception) {
|
|
||||||
is UnknownHostException, is ConnectException ->
|
|
||||||
stringResource(R.string.release_fetch_no_internet_short)
|
|
||||||
is ReleaseManager.UpdateException -> {
|
|
||||||
when (releaseState.exception.reason) {
|
|
||||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
|
||||||
R.string.release_fetch_external_in_use_short
|
|
||||||
)
|
|
||||||
else -> stringResource(R.string.release_fetch_generic_short)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CancellationException -> null
|
|
||||||
else -> stringResource(R.string.release_fetch_generic_short)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(message) {
|
|
||||||
val result = snackbarHostState.showSnackbar(
|
|
||||||
message = message,
|
|
||||||
actionLabel = resources.getString(R.string.release_fetch_button_retry),
|
|
||||||
duration = SnackbarDuration.Indefinite,
|
|
||||||
withDismissAction = true
|
|
||||||
)
|
|
||||||
|
|
||||||
when (result) {
|
|
||||||
SnackbarResult.ActionPerformed -> {
|
|
||||||
onRetry()
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AltMainScreen(
|
|
||||||
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
launchViewModel.beginLaunchFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
val launchUIState by launchViewModel.uiState.collectAsState()
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var showErrorInfo by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
|
||||||
) { innerPadding ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.safeDrawingPadding()
|
|
||||||
.padding(innerPadding),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
GeodeLogo()
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
LaunchProgressCard(
|
|
||||||
launchUIState,
|
|
||||||
crashInfo = launchViewModel.currentCrashInfo(),
|
|
||||||
onCancel = {
|
|
||||||
launchInSafeMode = false
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.cancelLaunch()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onResume = { safeMode ->
|
|
||||||
launchInSafeMode = safeMode
|
|
||||||
|
|
||||||
launchViewModel.clearCrashInfo()
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.beginLaunchFlow(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMore = {
|
|
||||||
showErrorInfo = true
|
|
||||||
},
|
|
||||||
extraOptions = {
|
|
||||||
ExtraOptions(
|
|
||||||
onSettings = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.cancelLaunch(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSettings(context)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
safeModeEnabled = launchInSafeMode
|
|
||||||
)
|
|
||||||
|
|
||||||
// only show launcher update in a case where a user won't see it ingame
|
|
||||||
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
|
||||||
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
|
|
||||||
|
|
||||||
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
|
||||||
UnsupportedVersionWarning()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
|
|
||||||
if (nextLauncherUpdate != null) {
|
|
||||||
LauncherUpdateIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
GeodeUpdateIndicator(snackbarHostState) {
|
|
||||||
launchViewModel.retryUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
|
|
||||||
UpdateWarning(launchInSafeMode) {
|
|
||||||
launchInSafeMode = false
|
|
||||||
coroutineScope.launch {
|
|
||||||
launchViewModel.cancelLaunch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val loadFailure = launchViewModel.currentCrashInfo()
|
|
||||||
if (showErrorInfo && loadFailure != null) {
|
|
||||||
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -415,8 +415,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
|
|||||||
this.mGLSurfaceView = glSurfaceView
|
this.mGLSurfaceView = glSurfaceView
|
||||||
frameLayout.addView(this.mGLSurfaceView)
|
frameLayout.addView(this.mGLSurfaceView)
|
||||||
|
|
||||||
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
val disableNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DISABLE_REDESIGN)
|
||||||
if (showNotification) {
|
if (!disableNotification) {
|
||||||
val hasCards = determineDisplayedCards(this)
|
val hasCards = determineDisplayedCards(this)
|
||||||
if (hasCards.isNotEmpty()) {
|
if (hasCards.isNotEmpty()) {
|
||||||
val notificationView = ComposeView(this)
|
val notificationView = ComposeView(this)
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package com.geode.launcher
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.geode.launcher.main.*
|
||||||
|
import com.geode.launcher.updater.ReleaseViewModel
|
||||||
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
|
import com.geode.launcher.ui.theme.Theme
|
||||||
|
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||||
|
import com.geode.launcher.utils.Constants
|
||||||
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
|
|
||||||
|
class LegacyMainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
clearDownloadedApks(this)
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
||||||
|
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
||||||
|
|
||||||
|
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
|
||||||
|
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") // the new api is android 13 only (why)
|
||||||
|
val returnError = intent.extras?.getSerializable(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR)
|
||||||
|
as? LaunchUtils.LauncherError
|
||||||
|
|
||||||
|
val loadFailureInfo = if (returnError != null && returnMessage != null) {
|
||||||
|
LoadFailureInfo(returnError, returnMessage, returnExtendedMessage)
|
||||||
|
} else if (LaunchUtils.lastSessionCrashed(this)) {
|
||||||
|
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
||||||
|
} else { null }
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||||
|
val theme = Theme.fromInt(themeOption)
|
||||||
|
|
||||||
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
MainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gdInstalled && geodeInstalled) {
|
||||||
|
intent.getBooleanExtra("restarted", false).let {
|
||||||
|
if (it) {
|
||||||
|
onLaunch(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
gdInstalled: Boolean = true,
|
||||||
|
geodePreinstalled: Boolean = true,
|
||||||
|
loadFailureInfo: LoadFailureInfo? = null,
|
||||||
|
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
||||||
|
val autoUpdateState by releaseViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
|
||||||
|
?.hasUpdated == true
|
||||||
|
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
||||||
|
|
||||||
|
var beginLaunch by remember { mutableStateOf(false) }
|
||||||
|
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(shouldUpdate) {
|
||||||
|
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
|
||||||
|
releaseViewModel.runReleaseCheck()
|
||||||
|
} else {
|
||||||
|
releaseViewModel.useGlobalCheckState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// automatically show load failed dialog (but not crash)
|
||||||
|
var showErrorInfo by remember { mutableStateOf(
|
||||||
|
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
|
||||||
|
)}
|
||||||
|
|
||||||
|
val hasError = loadFailureInfo != null
|
||||||
|
LaunchedEffect(hasError, showErrorInfo) {
|
||||||
|
if (!showErrorInfo && loadFailureInfo != null) {
|
||||||
|
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
|
||||||
|
showErrorInfo = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showErrorInfo && loadFailureInfo != null) {
|
||||||
|
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.geode_logo),
|
||||||
|
contentDescription = stringResource(R.string.launcher_logo_alt),
|
||||||
|
modifier = Modifier.size(100.dp, 100.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.launcher_title),
|
||||||
|
style = launcherTitleStyle,
|
||||||
|
modifier = Modifier.padding(12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
gdInstalled && geodeInstalled -> {
|
||||||
|
val gdVersion = remember {
|
||||||
|
GamePackageUtils.getGameVersionCode(context.packageManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN) {
|
||||||
|
val versionName = remember {
|
||||||
|
GamePackageUtils.getUnifiedVersionName(context.packageManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
|
||||||
|
} else {
|
||||||
|
val stopLaunch = releaseViewModel.isInUpdate || hasError
|
||||||
|
PlayButton(
|
||||||
|
stopAutomaticLaunch = stopLaunch,
|
||||||
|
blockLaunch = releaseViewModel.isInUpdate,
|
||||||
|
onPlayGame = { safeMode ->
|
||||||
|
launchInSafeMode = safeMode
|
||||||
|
beginLaunch = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||||
|
UnsupportedVersionWarning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gdInstalled -> {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.geode_download_title),
|
||||||
|
modifier = Modifier.padding(12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Button(
|
||||||
|
onClick = { releaseViewModel.runReleaseCheck(true) },
|
||||||
|
enabled = !releaseViewModel.isInUpdate
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_download),
|
||||||
|
contentDescription = stringResource(R.string.launcher_download_icon_alt)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_download))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(2.dp))
|
||||||
|
IconButton(onClick = { onSettings(context) }) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_settings),
|
||||||
|
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCard(
|
||||||
|
releaseViewModel,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beginLaunch) {
|
||||||
|
UpdateWarning(launchInSafeMode) {
|
||||||
|
beginLaunch = false
|
||||||
|
launchInSafeMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun MainScreenLightPreview() {
|
||||||
|
GeodeLauncherTheme {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
MainScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview(showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
fun MainScreenDarkPreview() {
|
||||||
|
GeodeLauncherTheme(theme = Theme.DARK) {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
MainScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun MainScreenNoGeometryDashPreview() {
|
||||||
|
GeodeLauncherTheme {
|
||||||
|
MainScreen(gdInstalled = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,40 @@ package com.geode.launcher
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.format.Formatter
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -17,34 +43,40 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geode.launcher.main.*
|
import com.geode.launcher.main.*
|
||||||
import com.geode.launcher.updater.ReleaseViewModel
|
|
||||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||||
import com.geode.launcher.ui.theme.LocalTheme
|
import com.geode.launcher.ui.theme.LocalTheme
|
||||||
import com.geode.launcher.ui.theme.Theme
|
import com.geode.launcher.ui.theme.Theme
|
||||||
import com.geode.launcher.ui.theme.launcherTitleStyle
|
import com.geode.launcher.ui.theme.launcherTitleStyle
|
||||||
|
import com.geode.launcher.updater.ReleaseManager
|
||||||
import com.geode.launcher.utils.Constants
|
import com.geode.launcher.utils.Constants
|
||||||
|
import com.geode.launcher.utils.GamePackageUtils
|
||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import com.geode.launcher.utils.PreferenceUtils
|
import com.geode.launcher.utils.PreferenceUtils
|
||||||
import com.geode.launcher.utils.GamePackageUtils
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
clearDownloadedApks(this)
|
clearDownloadedApks(this)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
|
||||||
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
|
||||||
|
|
||||||
@@ -61,9 +93,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
|
||||||
} else { null }
|
} else { null }
|
||||||
|
|
||||||
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DISABLE_REDESIGN)
|
||||||
if (redirectToAlt) {
|
if (redirectToAlt) {
|
||||||
val launchIntent = Intent(this, AltMainActivity::class.java)
|
val launchIntent = Intent(this, LegacyMainActivity::class.java)
|
||||||
launchIntent.putExtras(intent)
|
launchIntent.putExtras(intent)
|
||||||
startActivity(launchIntent)
|
startActivity(launchIntent)
|
||||||
|
|
||||||
@@ -77,17 +109,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||||
|
|
||||||
|
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||||
|
|
||||||
|
if (loadFailureInfo != null) {
|
||||||
|
launchViewModel.loadFailure = loadFailureInfo
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTheme provides theme) {
|
CompositionLocalProvider(LocalTheme provides theme) {
|
||||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize()
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
) {
|
||||||
MainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
|
AltMainScreen(launchViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gdInstalled && geodeInstalled) {
|
if (gdInstalled && geodeInstalled) {
|
||||||
intent.getBooleanExtra("restarted", false).let {
|
intent.getBooleanExtra("restarted", false).let {
|
||||||
if (it) {
|
if (it) {
|
||||||
@@ -98,174 +136,465 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class LaunchStatusInfo(
|
||||||
|
val title: String?,
|
||||||
|
val details: String? = null,
|
||||||
|
val progress: (() -> Float)? = null
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifier: Modifier = Modifier) {
|
||||||
gdInstalled: Boolean = true,
|
when (cancelReason) {
|
||||||
geodePreinstalled: Boolean = true,
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
|
||||||
loadFailureInfo: LoadFailureInfo? = null,
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
|
||||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
|
||||||
) {
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
|
||||||
val context = LocalContext.current
|
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
|
@Composable
|
||||||
val autoUpdateState by releaseViewModel.uiState.collectAsState()
|
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
|
||||||
|
return when (cancelReason) {
|
||||||
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_crash)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.geode_download_title)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
|
details = stringResource(R.string.game_not_found)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_error),
|
||||||
|
details = stringResource(R.string.launcher_cancelled_outdated)
|
||||||
|
)
|
||||||
|
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
|
||||||
|
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
|
||||||
|
title = null
|
||||||
|
)
|
||||||
|
/*
|
||||||
|
else -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_cancelled_manual)
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
|
@Composable
|
||||||
?.hasUpdated == true
|
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
|
||||||
val geodeInstalled = geodePreinstalled || geodeJustInstalled
|
return when (state) {
|
||||||
|
is LaunchViewModel.LaunchUIState.Initial,
|
||||||
|
is LaunchViewModel.LaunchUIState.Working,
|
||||||
|
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_starting_game),
|
||||||
|
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
|
||||||
|
)
|
||||||
|
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.release_fetch_in_progress)
|
||||||
|
)
|
||||||
|
is LaunchViewModel.LaunchUIState.Updating -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
var beginLaunch by remember { mutableStateOf(false) }
|
val downloaded = remember(state.downloaded) {
|
||||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
Formatter.formatShortFileSize(context, state.downloaded)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(shouldUpdate) {
|
val outOf = remember(state.outOf) {
|
||||||
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
|
state.outOf?.run {
|
||||||
releaseViewModel.runReleaseCheck()
|
Formatter.formatShortFileSize(context, this)
|
||||||
} else {
|
}
|
||||||
releaseViewModel.useGlobalCheckState()
|
}
|
||||||
|
|
||||||
|
LaunchStatusInfo(
|
||||||
|
title = stringResource(R.string.launcher_downloading_update),
|
||||||
|
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
|
||||||
|
progress = {
|
||||||
|
val progress = state.downloaded / state.outOf!!.toDouble()
|
||||||
|
progress.toFloat()
|
||||||
|
}.takeIf { state.outOf != null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LaunchViewModel.LaunchUIState.Cancelled -> {
|
||||||
|
return mapCancelReasonToInfo(state.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
|
||||||
|
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier.width(300.dp)
|
||||||
|
) {
|
||||||
|
if (statusInfo.title != null) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||||
|
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
|
||||||
|
icon()
|
||||||
|
Spacer(Modifier.size(8.dp))
|
||||||
|
Text(statusInfo.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inProgress) {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.details != null) {
|
||||||
|
Text(
|
||||||
|
statusInfo.details,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
}
|
||||||
|
|
||||||
// automatically show load failed dialog (but not crash)
|
@Composable
|
||||||
var showErrorInfo by remember { mutableStateOf(
|
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
|
||||||
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
|
Column(
|
||||||
)}
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
modifier = modifier.width(300.dp)
|
||||||
|
) {
|
||||||
|
if (statusInfo.title != null) {
|
||||||
|
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
|
||||||
val hasError = loadFailureInfo != null
|
if (statusInfo.progress != null) {
|
||||||
LaunchedEffect(hasError, showErrorInfo) {
|
LinearProgressIndicator(
|
||||||
if (!showErrorInfo && loadFailureInfo != null) {
|
statusInfo.progress,
|
||||||
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
|
modifier = Modifier.fillMaxWidth()
|
||||||
showErrorInfo = true
|
)
|
||||||
|
} else {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.details != null) {
|
||||||
|
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
|
||||||
|
when (reason) {
|
||||||
|
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_resume),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_resume))
|
||||||
|
}
|
||||||
|
LaunchViewModel.LaunchCancelReason.MANUAL,
|
||||||
|
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_play_arrow),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_launch))
|
||||||
|
}
|
||||||
|
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_refresh),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_retry))
|
||||||
|
}
|
||||||
|
// these shouldn't be allowing retries anyways
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
|
||||||
|
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LaunchProgressCard(
|
||||||
|
uiState: LaunchViewModel.LaunchUIState,
|
||||||
|
crashInfo: LoadFailureInfo?,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onResume: (safeMode: Boolean) -> Unit,
|
||||||
|
onMore: () -> Unit,
|
||||||
|
extraOptions: @Composable () -> Unit,
|
||||||
|
safeModeEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||||
|
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||||
|
LaunchCancelledBody(
|
||||||
|
statusInfo = status,
|
||||||
|
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
|
||||||
|
inProgress = uiState.inProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
if (uiState.reason.isGameInstallIssue()) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
|
||||||
|
|
||||||
|
if (showDownload) {
|
||||||
|
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.reason.allowsRetry()) {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
LongPressButton(onClick = { onResume(false) }, onLongPress = {
|
||||||
|
showSafeModeDialog = true
|
||||||
|
}) {
|
||||||
|
RetryButtonContents(uiState.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crashInfo != null) {
|
||||||
|
FilledTonalButton(onClick = onMore) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_question_mark),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(stringResource(R.string.launcher_cancelled_help_alt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extraOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
||||||
|
TextButton(onClick = onCancel) {
|
||||||
|
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1.0f))
|
||||||
|
|
||||||
|
extraOptions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showErrorInfo && loadFailureInfo != null) {
|
if (showSafeModeDialog) {
|
||||||
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
|
SafeModeDialog(
|
||||||
|
onDismiss = {
|
||||||
|
showSafeModeDialog = false
|
||||||
|
},
|
||||||
|
onLaunch = {
|
||||||
|
onResume(true)
|
||||||
|
showSafeModeDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtraOptions(onSettings: () -> Unit) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onSettings,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.icon_settings),
|
||||||
|
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GeodeLogo(modifier: Modifier = Modifier) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.geode_monochrome),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp, 64.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.launcher_title),
|
||||||
|
style = launcherTitleStyle,
|
||||||
|
fontSize = 64.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val resources = LocalResources.current
|
||||||
|
|
||||||
|
val releaseState = ReleaseManager.get(context).uiState.value
|
||||||
|
|
||||||
|
if (releaseState !is ReleaseManager.ReleaseManagerState.Failure) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = when (releaseState.exception) {
|
||||||
|
is UnknownHostException, is ConnectException ->
|
||||||
|
stringResource(R.string.release_fetch_no_internet_short)
|
||||||
|
is ReleaseManager.UpdateException -> {
|
||||||
|
when (releaseState.exception.reason) {
|
||||||
|
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||||
|
R.string.release_fetch_external_in_use_short
|
||||||
|
)
|
||||||
|
else -> stringResource(R.string.release_fetch_generic_short)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CancellationException -> null
|
||||||
|
else -> stringResource(R.string.release_fetch_generic_short)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(message) {
|
||||||
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
message = message,
|
||||||
|
actionLabel = resources.getString(R.string.release_fetch_button_retry),
|
||||||
|
duration = SnackbarDuration.Indefinite,
|
||||||
|
withDismissAction = true
|
||||||
|
)
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed -> {
|
||||||
|
onRetry()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AltMainScreen(
|
||||||
|
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launchViewModel.beginLaunchFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
val launchUIState by launchViewModel.uiState.collectAsState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showErrorInfo by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(innerPadding)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState()),
|
.safeDrawingPadding()
|
||||||
verticalArrangement = Arrangement.Center,
|
.padding(innerPadding),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
Column(
|
||||||
painter = painterResource(id = R.drawable.geode_logo),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentDescription = stringResource(R.string.launcher_logo_alt),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.size(100.dp, 100.dp)
|
modifier = Modifier
|
||||||
)
|
.padding(8.dp)
|
||||||
Text(
|
.verticalScroll(rememberScrollState())
|
||||||
stringResource(R.string.launcher_title),
|
) {
|
||||||
style = launcherTitleStyle,
|
GeodeLogo()
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
when {
|
Column(
|
||||||
gdInstalled && geodeInstalled -> {
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
val gdVersion = remember {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
GamePackageUtils.getGameVersionCode(context.packageManager)
|
) {
|
||||||
}
|
LaunchProgressCard(
|
||||||
|
launchUIState,
|
||||||
|
crashInfo = launchViewModel.currentCrashInfo(),
|
||||||
|
onCancel = {
|
||||||
|
launchInSafeMode = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
launchViewModel.cancelLaunch()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResume = { safeMode ->
|
||||||
|
launchInSafeMode = safeMode
|
||||||
|
|
||||||
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN) {
|
launchViewModel.clearCrashInfo()
|
||||||
val versionName = remember {
|
coroutineScope.launch {
|
||||||
GamePackageUtils.getUnifiedVersionName(context.packageManager)
|
launchViewModel.beginLaunchFlow(true)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onMore = {
|
||||||
|
showErrorInfo = true
|
||||||
|
},
|
||||||
|
extraOptions = {
|
||||||
|
ExtraOptions(
|
||||||
|
onSettings = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
launchViewModel.cancelLaunch(true)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
|
onSettings(context)
|
||||||
} else {
|
}
|
||||||
val stopLaunch = releaseViewModel.isInUpdate || hasError
|
)
|
||||||
PlayButton(
|
},
|
||||||
stopAutomaticLaunch = stopLaunch,
|
safeModeEnabled = launchInSafeMode
|
||||||
blockLaunch = releaseViewModel.isInUpdate,
|
|
||||||
onPlayGame = { safeMode ->
|
|
||||||
launchInSafeMode = safeMode
|
|
||||||
beginLaunch = true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
|
||||||
UnsupportedVersionWarning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gdInstalled -> {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.geode_download_title),
|
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row {
|
// only show launcher update in a case where a user won't see it ingame
|
||||||
Button(
|
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
|
||||||
onClick = { releaseViewModel.runReleaseCheck(true) },
|
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
|
||||||
enabled = !releaseViewModel.isInUpdate
|
|
||||||
) {
|
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
|
||||||
Icon(
|
UnsupportedVersionWarning()
|
||||||
painterResource(R.drawable.icon_download),
|
|
||||||
contentDescription = stringResource(R.string.launcher_download_icon_alt)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
||||||
Text(stringResource(R.string.launcher_download))
|
|
||||||
}
|
}
|
||||||
Spacer(Modifier.size(2.dp))
|
|
||||||
IconButton(onClick = { onSettings(context) }) {
|
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
|
||||||
Icon(
|
if (nextLauncherUpdate != null) {
|
||||||
painterResource(R.drawable.icon_settings),
|
LauncherUpdateIndicator()
|
||||||
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
|
}
|
||||||
)
|
|
||||||
|
GeodeUpdateIndicator(snackbarHostState) {
|
||||||
|
launchViewModel.retryUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateCard(
|
|
||||||
releaseViewModel,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(12.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beginLaunch) {
|
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
|
||||||
UpdateWarning(launchInSafeMode) {
|
UpdateWarning(launchInSafeMode) {
|
||||||
beginLaunch = false
|
|
||||||
launchInSafeMode = false
|
launchInSafeMode = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
launchViewModel.cancelLaunch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showSystemUi = true)
|
val loadFailure = launchViewModel.currentCrashInfo()
|
||||||
@Composable
|
if (showErrorInfo && loadFailure != null) {
|
||||||
fun MainScreenLightPreview() {
|
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
|
||||||
GeodeLauncherTheme {
|
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
|
||||||
MainScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Preview(showSystemUi = true)
|
|
||||||
@Composable
|
|
||||||
fun MainScreenDarkPreview() {
|
|
||||||
GeodeLauncherTheme(theme = Theme.DARK) {
|
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
|
||||||
MainScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun MainScreenNoGeometryDashPreview() {
|
|
||||||
GeodeLauncherTheme {
|
|
||||||
MainScreen(gdInstalled = false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,6 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val COOLDOWN_LENGTH_MS = 3000L
|
|
||||||
|
|
||||||
class LaunchViewModel(private val application: Application): ViewModel() {
|
class LaunchViewModel(private val application: Application): ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
@@ -37,7 +35,13 @@ class LaunchViewModel(private val application: Application): ViewModel() {
|
|||||||
private var readyTimer: CountDownTimer? = null
|
private var readyTimer: CountDownTimer? = null
|
||||||
|
|
||||||
private fun initReadyTimer() {
|
private fun initReadyTimer() {
|
||||||
readyTimer = object : CountDownTimer(COOLDOWN_LENGTH_MS, 1000) {
|
val cooldownLength = PreferenceUtils.get(application).getInt(PreferenceUtils.Key.WAIT_PERIOD) * 1000L
|
||||||
|
if (cooldownLength == 0L) {
|
||||||
|
readyTimerPassed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
readyTimer = object : CountDownTimer(cooldownLength, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
// no tick necessary
|
// no tick necessary
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -208,9 +209,21 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||||
|
SettingsSelectCard(
|
||||||
|
title = stringResource(R.string.preference_launch_delay),
|
||||||
|
dialogTitle = stringResource(R.string.preference_launch_delay_select),
|
||||||
|
preferenceKey = PreferenceUtils.Key.WAIT_PERIOD,
|
||||||
|
options = linkedMapOf(
|
||||||
|
0 to stringResource(R.string.preference_launch_delay_instant),
|
||||||
|
1 to pluralStringResource(R.plurals.preference_launch_delay_value, 1, 1),
|
||||||
|
3 to pluralStringResource(R.plurals.preference_launch_delay_value, 3, 3),
|
||||||
|
5 to pluralStringResource(R.plurals.preference_launch_delay_value, 5, 5),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
title = stringResource(R.string.preference_enable_redesign),
|
title = stringResource(R.string.preference_disable_redesign),
|
||||||
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
|
preferenceKey = PreferenceUtils.Key.DISABLE_REDESIGN,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
LIMIT_ASPECT_RATIO,
|
LIMIT_ASPECT_RATIO,
|
||||||
DISPLAY_MODE,
|
DISPLAY_MODE,
|
||||||
FORCE_HRR,
|
FORCE_HRR,
|
||||||
ENABLE_REDESIGN,
|
|
||||||
RELEASE_CHANNEL_TAG,
|
RELEASE_CHANNEL_TAG,
|
||||||
DEVELOPER_MODE,
|
DEVELOPER_MODE,
|
||||||
CLEANUP_APKS,
|
CLEANUP_APKS,
|
||||||
@@ -149,7 +148,9 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
SCREEN_ZOOM_FIT,
|
SCREEN_ZOOM_FIT,
|
||||||
LIMIT_FRAME_RATE,
|
LIMIT_FRAME_RATE,
|
||||||
DISABLE_USER_THEME,
|
DISABLE_USER_THEME,
|
||||||
SELECTED_ICON
|
SELECTED_ICON,
|
||||||
|
DISABLE_REDESIGN,
|
||||||
|
WAIT_PERIOD
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||||
@@ -163,6 +164,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
private fun defaultValueForIntKey(key: Key) = when (key) {
|
private fun defaultValueForIntKey(key: Key) = when (key) {
|
||||||
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
|
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
|
||||||
Key.SCREEN_ZOOM -> 100
|
Key.SCREEN_ZOOM -> 100
|
||||||
|
Key.WAIT_PERIOD -> 3
|
||||||
// people wanted a reset on nightly anyways, so this is a good excuse to do so
|
// people wanted a reset on nightly anyways, so this is a good excuse to do so
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
@@ -189,7 +191,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
|
||||||
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
|
||||||
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
|
||||||
Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign"
|
Key.DISABLE_REDESIGN -> "PreferenceDisableRedesign"
|
||||||
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
|
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
|
||||||
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
|
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
|
||||||
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
|
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
|
||||||
@@ -200,6 +202,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
|||||||
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
||||||
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
||||||
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
||||||
|
Key.WAIT_PERIOD -> "PreferenceWaitPeriod"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,15 @@
|
|||||||
<string name="preference_set_application_icon">Application icon</string>
|
<string name="preference_set_application_icon">Application icon</string>
|
||||||
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
||||||
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
|
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
|
||||||
|
<string name="preference_disable_redesign">Disable UI refresh</string>
|
||||||
|
|
||||||
|
<string name="preference_launch_delay">Launch delay</string>
|
||||||
|
<string name="preference_launch_delay_select">Select launch delay</string>
|
||||||
|
<string name="preference_launch_delay_instant">None</string>
|
||||||
|
<plurals name="preference_launch_delay_value">
|
||||||
|
<item quantity="one">%1$d second</item>
|
||||||
|
<item quantity="other">%1$d seconds</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<string name="preference_display_mode_name">Display mode</string>
|
<string name="preference_display_mode_name">Display mode</string>
|
||||||
<string name="preference_display_mode_select">Select display mode</string>
|
<string name="preference_display_mode_select">Select display mode</string>
|
||||||
|
|||||||
Reference in new issue
Block a user