mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-21 16:32:39 +00:00
reorganize mainactivity components
This commit is contained in:
6 files changed
+934
-802
No files matched your search
@@ -1,32 +1,16 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.text.format.Formatter.formatShortFileSize
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -38,47 +22,22 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.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.Typography
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.geode.launcher.updater.ReleaseManager
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.GeodeUtils
|
||||
import com.geode.launcher.utils.useCountdownTimer
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
data class LoadFailureInfo(
|
||||
val title: LaunchUtils.LauncherError,
|
||||
val description: String? = null,
|
||||
val extendedMessage: String? = null
|
||||
)
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -129,764 +88,6 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDismiss: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ElevatedCard(modifier) {
|
||||
Column(
|
||||
// buttons add enough padding already, lower to compensate
|
||||
modifier = Modifier.padding(
|
||||
top = 20.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.launcher_update_available),
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.align(Alignment.End)) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.launcher_update_dismiss))
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(4.dp))
|
||||
|
||||
TextButton(onClick = { uriHandler.openUri(openTo) }) {
|
||||
Text(stringResource(R.string.launcher_download))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateProgressIndicator(
|
||||
message: String,
|
||||
releaseViewModel: ReleaseViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
progress: (() -> Float)? = null
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(message)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
if (progress == null) {
|
||||
LinearProgressIndicator()
|
||||
} else {
|
||||
LinearProgressIndicator(progress = progress)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
releaseViewModel.cancelUpdate()
|
||||
},
|
||||
modifier = Modifier.offset((-12).dp)
|
||||
) {
|
||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateMessageIndicator(
|
||||
message: String,
|
||||
releaseViewModel: ReleaseViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
allowRetry: Boolean = false
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
message,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
if (allowRetry) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
releaseViewModel.runReleaseCheck(true)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.release_fetch_button_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val packageManager = context.packageManager
|
||||
|
||||
val gdVersionCode = remember { GamePackageUtils.getGameVersionCode(packageManager) }
|
||||
val gdVersionString = remember { GamePackageUtils.getGameVersionString(packageManager) }
|
||||
|
||||
var lastDismissedVersion by PreferenceUtils.useLongPreference(
|
||||
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
|
||||
)
|
||||
|
||||
val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
|
||||
val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
|
||||
|
||||
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.launcher_unsupported_version_title))
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(
|
||||
R.string.launcher_unsupported_version_description,
|
||||
gdVersionString,
|
||||
Constants.SUPPORTED_VERSION_STRING
|
||||
))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDismiss()
|
||||
lastDismissedVersion = gdVersionCode
|
||||
|
||||
onLaunch(context, inSafeMode)
|
||||
}) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
onDismissRequest = { onDismiss() }
|
||||
)
|
||||
} else {
|
||||
LaunchedEffect(gdVersionCode) {
|
||||
onLaunch(context, inSafeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
|
||||
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onLaunch) {
|
||||
Text(stringResource(R.string.message_box_continue))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val releaseState by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
when (val state = releaseState) {
|
||||
is ReleaseViewModel.ReleaseUIState.Failure -> {
|
||||
val message = when (state.exception) {
|
||||
is UnknownHostException, is ConnectException ->
|
||||
stringResource(R.string.release_fetch_no_internet)
|
||||
is ReleaseManager.UpdateException -> {
|
||||
when (state.exception.reason) {
|
||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||
R.string.release_fetch_manual_check_required
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
UpdateMessageIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_failed,
|
||||
message ?: stringResource(R.string.release_fetch_try_later)
|
||||
),
|
||||
modifier = modifier,
|
||||
allowRetry = true,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||
val downloaded = remember(state.downloaded) {
|
||||
formatShortFileSize(context, state.downloaded)
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
formatShortFileSize(context, state.outOf)
|
||||
}
|
||||
|
||||
UpdateProgressIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_downloading,
|
||||
downloaded,
|
||||
outOf
|
||||
),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel,
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf.toDouble()
|
||||
progress.toFloat()
|
||||
}
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||
UpdateProgressIndicator(
|
||||
stringResource(R.string.release_fetch_in_progress),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
||||
if (state.hasUpdated) {
|
||||
UpdateMessageIndicator(
|
||||
stringResource(R.string.release_fetch_success),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Cancelled -> {
|
||||
if (state.isCancelling) {
|
||||
UpdateProgressIndicator(
|
||||
stringResource(R.string.release_fetch_cancelling),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
} else {
|
||||
UpdateMessageIndicator(
|
||||
stringResource(R.string.release_fetch_cancelled),
|
||||
modifier = modifier,
|
||||
allowRetry = true,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
|
||||
val nextUpdateValue = nextUpdate
|
||||
|
||||
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
|
||||
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
|
||||
?: nextUpdateValue.htmlUrl
|
||||
|
||||
LauncherUpdateIndicator(
|
||||
modifier = modifier,
|
||||
openTo = updateUrl,
|
||||
onDismiss = {
|
||||
releaseViewModel.dismissLauncherUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorInfoBody(failureReason: LaunchUtils.LauncherError, modifier: Modifier = Modifier) {
|
||||
val headline = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error_description)
|
||||
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed_description)
|
||||
else -> stringResource(R.string.load_failed_generic_error_description)
|
||||
}
|
||||
|
||||
val recommendations = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> listOf(
|
||||
stringResource(R.string.load_failed_recommendation_reinstall),
|
||||
stringResource(R.string.load_failed_recommendation_update),
|
||||
)
|
||||
LaunchUtils.LauncherError.CRASHED -> listOf(
|
||||
stringResource(R.string.load_failed_recommendation_safe_mode),
|
||||
stringResource(R.string.load_failed_recommendation_report)
|
||||
)
|
||||
else -> listOf(
|
||||
stringResource(R.string.load_failed_recommendation_reinstall),
|
||||
stringResource(R.string.load_failed_recommendation_update),
|
||||
stringResource(R.string.load_failed_recommendation_report)
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Text(headline)
|
||||
|
||||
recommendations.forEach { r ->
|
||||
Text("\u2022\u00A0\u00A0$r")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorInfoTitle(failureReason: LaunchUtils.LauncherError) {
|
||||
val message = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
|
||||
LaunchUtils.LauncherError.LINKER_NEEDS_32BIT,
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error)
|
||||
LaunchUtils.LauncherError.GENERIC -> stringResource(R.string.load_failed_generic_error)
|
||||
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_error),
|
||||
contentDescription = stringResource(R.string.launcher_error_icon_alt),
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Text(message, style = Typography.headlineMedium)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onShareCrash(context: Context) {
|
||||
val lastCrash = LaunchUtils.getLastCrash(context)
|
||||
if (lastCrash == null) {
|
||||
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
|
||||
val providerLocation = lastCrash.toRelativeString(baseDirectory)
|
||||
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
|
||||
|
||||
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(shareIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onShowLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
stringResource(R.string.launcher_error_details),
|
||||
style = Typography.headlineSmall
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(
|
||||
description,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(4.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_error_device_info, Build.MODEL, Build.VERSION.RELEASE),
|
||||
style = Typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
12.dp, alignment = Alignment.CenterHorizontally
|
||||
)
|
||||
) {
|
||||
if (extraDetails != null) {
|
||||
Button(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = {
|
||||
clipboardManager.setText(AnnotatedString(extraDetails))
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_content_copy),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_copy))
|
||||
}
|
||||
} else {
|
||||
Button(onClick = { onShareCrash(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_export_crash))
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDownloadGame(context: Context) {
|
||||
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(appUrl)
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GooglePlayBadge(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.google_play_badge),
|
||||
contentDescription = stringResource(R.string.launcher_download_game),
|
||||
modifier = modifier
|
||||
.width(196.dp)
|
||||
.clip(AbsoluteRoundedCornerShape(4.dp))
|
||||
.clickable { onDownloadGame(context) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadRecommendation(needsUniversal: Boolean, modifier: Modifier = Modifier) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
val version = BuildConfig.VERSION_NAME
|
||||
|
||||
val showDownload = remember {
|
||||
!GamePackageUtils.isGameInstalled(context.packageManager) ||
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
|
||||
val downloadBase = "https://github.com/geode-sdk/android-launcher/releases/download/$version"
|
||||
val legacyDownloadUrl = "$downloadBase/geode-launcher-v$version-android32.apk"
|
||||
val universalDownloadUrl = "$downloadBase/geode-launcher-v$version.apk"
|
||||
|
||||
val downloadUrl = if (needsUniversal)
|
||||
universalDownloadUrl else legacyDownloadUrl
|
||||
|
||||
val downloadText = if (needsUniversal)
|
||||
stringResource(R.string.launcher_download_universal)
|
||||
else
|
||||
stringResource(R.string.launcher_download_32bit)
|
||||
|
||||
val promptTitle = stringResource(R.string.load_failed_abi_error)
|
||||
val reinstallText = stringResource(R.string.load_failed_recommendation_reinstall)
|
||||
val recommendationText = if (needsUniversal)
|
||||
stringResource(R.string.load_failed_recommendation_switch_64bit_abi)
|
||||
else
|
||||
stringResource(R.string.load_failed_recommendation_switch_32bit_abi)
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
12.dp, alignment = Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
Text(promptTitle)
|
||||
Text("\u2022\u00A0\u00A0$reinstallText")
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge()
|
||||
}
|
||||
|
||||
Text("\u2022\u00A0\u00A0$recommendationText")
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(downloadText),
|
||||
onClick = {
|
||||
uriHandler.openUri(downloadUrl)
|
||||
},
|
||||
|
||||
style = TextStyle.Default.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ErrorInfoSheet(failureInfo: LoadFailureInfo, onDismiss: () -> Unit) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
||||
// second container created for scroll (before padding is added)
|
||||
Box(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Column(modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
ErrorInfoTitle(failureInfo.title)
|
||||
|
||||
Spacer(Modifier.size(16.dp))
|
||||
|
||||
if (failureInfo.title.isAbiFailure()) {
|
||||
DownloadRecommendation(
|
||||
needsUniversal = failureInfo.title == LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
)
|
||||
} else {
|
||||
ErrorInfoBody(failureInfo.title)
|
||||
|
||||
if (failureInfo.description != null) {
|
||||
ErrorInfoDescription(
|
||||
failureInfo.description,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
ErrorInfoActions(
|
||||
extraDetails = failureInfo.extendedMessage ?: failureInfo.description,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onLaunch(context: Context, safeMode: Boolean = false) {
|
||||
if (safeMode) {
|
||||
GeodeUtils.setAdditionalLaunchArguments(GeodeUtils.ARGUMENT_SAFE_MODE)
|
||||
} else {
|
||||
GeodeUtils.clearLaunchArguments()
|
||||
}
|
||||
|
||||
val launchIntent = Intent(context, GeometryDashActivity::class.java)
|
||||
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
fun onSettings(context: Context) {
|
||||
val launchIntent = Intent(context, SettingsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
suspend fun showFailureSnackbar(
|
||||
context: Context,
|
||||
loadFailureInfo: LoadFailureInfo,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onActionPerformed: () -> Unit
|
||||
) {
|
||||
val message = if (loadFailureInfo.title == LaunchUtils.LauncherError.CRASHED) {
|
||||
context.getString(R.string.launcher_crashed)
|
||||
} else {
|
||||
context.getString(R.string.launcher_failed_to_load)
|
||||
}
|
||||
|
||||
val res = snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = context.getString(R.string.launcher_error_more),
|
||||
duration = SnackbarDuration.Indefinite,
|
||||
)
|
||||
|
||||
when (res) {
|
||||
SnackbarResult.ActionPerformed -> {
|
||||
onActionPerformed()
|
||||
}
|
||||
SnackbarResult.Dismissed -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayButton(
|
||||
stopAutomaticLaunch: Boolean,
|
||||
blockLaunch: Boolean,
|
||||
onPlayGame: (Boolean) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||
|
||||
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
|
||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
||||
)
|
||||
|
||||
if (shouldAutomaticallyLaunch && !stopAutomaticLaunch && !showSafeModeDialog) {
|
||||
val countdownTimer = useCountdownTimer(
|
||||
time = 3000,
|
||||
onCountdownFinish = { onPlayGame(launchInSafeMode) }
|
||||
)
|
||||
|
||||
if (countdownTimer != 0L) {
|
||||
Text(
|
||||
pluralStringResource(
|
||||
R.plurals.automatically_load_countdown,
|
||||
countdownTimer.toInt(),
|
||||
countdownTimer
|
||||
),
|
||||
style = Typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// compose apis don't provide a good way of adding long press to a button
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val haptics = LocalHapticFeedback.current
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collectLatest { interaction ->
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> {
|
||||
launchInSafeMode = false
|
||||
|
||||
delay(viewConfiguration.longPressTimeoutMillis)
|
||||
|
||||
// perform a second delay to make the action more obvious
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
delay(viewConfiguration.longPressTimeoutMillis)
|
||||
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
|
||||
showSafeModeDialog = true
|
||||
}
|
||||
|
||||
is PressInteraction.Release -> {
|
||||
if (!showSafeModeDialog) {
|
||||
onPlayGame(launchInSafeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Button(
|
||||
onClick = { },
|
||||
enabled = !blockLaunch,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = context.getString(R.string.launcher_launch_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(context.getString(R.string.launcher_launch))
|
||||
}
|
||||
Spacer(Modifier.size(2.dp))
|
||||
IconButton(onClick = { onSettings(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSafeModeDialog) {
|
||||
SafeModeDialog(
|
||||
onDismiss = {
|
||||
launchInSafeMode = false
|
||||
showSafeModeDialog = false
|
||||
},
|
||||
onLaunch = {
|
||||
launchInSafeMode = true
|
||||
|
||||
onPlayGame(true)
|
||||
|
||||
showSafeModeDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LaunchBlockedLabel(text: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val showDownload = remember {
|
||||
GamePackageUtils.isGameInstalled(context.packageManager) &&
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text, textAlign = TextAlign.Center, modifier = Modifier.padding(12.dp))
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
gdInstalled: Boolean = true,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.geode.launcher.main
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.utils.Constants
|
||||
|
||||
fun onDownloadGame(context: Context) {
|
||||
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(appUrl)
|
||||
setPackage("com.android.vending")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GooglePlayBadge(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.google_play_badge),
|
||||
contentDescription = stringResource(R.string.launcher_download_game),
|
||||
modifier = modifier
|
||||
.width(196.dp)
|
||||
.clip(AbsoluteRoundedCornerShape(4.dp))
|
||||
.clickable { onDownloadGame(context) }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package com.geode.launcher.main
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.ApplicationLogsActivity
|
||||
import com.geode.launcher.BuildConfig
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.UserDirectoryProvider
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
|
||||
data class LoadFailureInfo(
|
||||
val title: LaunchUtils.LauncherError,
|
||||
val description: String? = null,
|
||||
val extendedMessage: String? = null
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ErrorInfoBody(failureReason: LaunchUtils.LauncherError, modifier: Modifier = Modifier) {
|
||||
val headline = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error_description)
|
||||
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed_description)
|
||||
else -> stringResource(R.string.load_failed_generic_error_description)
|
||||
}
|
||||
|
||||
val recommendations = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> listOf(
|
||||
stringResource(R.string.load_failed_recommendation_reinstall),
|
||||
stringResource(R.string.load_failed_recommendation_update),
|
||||
)
|
||||
LaunchUtils.LauncherError.CRASHED -> listOf(
|
||||
stringResource(R.string.load_failed_recommendation_safe_mode),
|
||||
stringResource(R.string.load_failed_recommendation_report)
|
||||
)
|
||||
else -> listOf(
|
||||
stringResource(R.string.load_failed_recommendation_reinstall),
|
||||
stringResource(R.string.load_failed_recommendation_update),
|
||||
stringResource(R.string.load_failed_recommendation_report)
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Text(headline)
|
||||
|
||||
recommendations.forEach { r ->
|
||||
Text("\u2022\u00A0\u00A0$r")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorInfoTitle(failureReason: LaunchUtils.LauncherError) {
|
||||
val message = when (failureReason) {
|
||||
LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
|
||||
LaunchUtils.LauncherError.LINKER_NEEDS_32BIT,
|
||||
LaunchUtils.LauncherError.LINKER_FAILED -> stringResource(R.string.load_failed_link_error)
|
||||
LaunchUtils.LauncherError.GENERIC -> stringResource(R.string.load_failed_generic_error)
|
||||
LaunchUtils.LauncherError.CRASHED -> stringResource(R.string.load_failed_crashed)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_error),
|
||||
contentDescription = stringResource(R.string.launcher_error_icon_alt),
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Text(message, style = Typography.headlineMedium)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onShareCrash(context: Context) {
|
||||
val lastCrash = LaunchUtils.getLastCrash(context)
|
||||
if (lastCrash == null) {
|
||||
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
|
||||
val providerLocation = lastCrash.toRelativeString(baseDirectory)
|
||||
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
|
||||
|
||||
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(shareIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onShowLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
stringResource(R.string.launcher_error_details),
|
||||
style = Typography.headlineSmall
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(
|
||||
description,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(4.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.launcher_error_device_info, Build.MODEL, Build.VERSION.RELEASE),
|
||||
style = Typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
12.dp, alignment = Alignment.CenterHorizontally
|
||||
)
|
||||
) {
|
||||
if (extraDetails != null) {
|
||||
Button(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = {
|
||||
clipboardManager.setText(AnnotatedString(extraDetails))
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_content_copy),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_copy))
|
||||
}
|
||||
} else {
|
||||
Button(onClick = { onShareCrash(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_export_crash))
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DownloadRecommendation(needsUniversal: Boolean, modifier: Modifier = Modifier) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
val version = BuildConfig.VERSION_NAME
|
||||
|
||||
val showDownload = remember {
|
||||
!GamePackageUtils.isGameInstalled(context.packageManager) ||
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
|
||||
val downloadBase = "https://github.com/geode-sdk/android-launcher/releases/download/$version"
|
||||
val legacyDownloadUrl = "$downloadBase/geode-launcher-v$version-android32.apk"
|
||||
val universalDownloadUrl = "$downloadBase/geode-launcher-v$version.apk"
|
||||
|
||||
val downloadUrl = if (needsUniversal)
|
||||
universalDownloadUrl else legacyDownloadUrl
|
||||
|
||||
val downloadText = if (needsUniversal)
|
||||
stringResource(R.string.launcher_download_universal)
|
||||
else
|
||||
stringResource(R.string.launcher_download_32bit)
|
||||
|
||||
val promptTitle = stringResource(R.string.load_failed_abi_error)
|
||||
val reinstallText = stringResource(R.string.load_failed_recommendation_reinstall)
|
||||
val recommendationText = if (needsUniversal)
|
||||
stringResource(R.string.load_failed_recommendation_switch_64bit_abi)
|
||||
else
|
||||
stringResource(R.string.load_failed_recommendation_switch_32bit_abi)
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
12.dp, alignment = Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
Text(promptTitle)
|
||||
Text("\u2022\u00A0\u00A0$reinstallText")
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge()
|
||||
}
|
||||
|
||||
Text("\u2022\u00A0\u00A0$recommendationText")
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(downloadText),
|
||||
onClick = {
|
||||
uriHandler.openUri(downloadUrl)
|
||||
},
|
||||
|
||||
style = TextStyle.Default.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ErrorInfoSheet(failureInfo: LoadFailureInfo, onDismiss: () -> Unit) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
|
||||
// second container created for scroll (before padding is added)
|
||||
Box(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Column(modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
ErrorInfoTitle(failureInfo.title)
|
||||
|
||||
Spacer(Modifier.size(16.dp))
|
||||
|
||||
if (failureInfo.title.isAbiFailure()) {
|
||||
DownloadRecommendation(
|
||||
needsUniversal = failureInfo.title == LaunchUtils.LauncherError.LINKER_NEEDS_64BIT,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
)
|
||||
} else {
|
||||
ErrorInfoBody(failureInfo.title)
|
||||
|
||||
if (failureInfo.description != null) {
|
||||
ErrorInfoDescription(
|
||||
failureInfo.description,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
ErrorInfoActions(
|
||||
extraDetails = failureInfo.extendedMessage ?: failureInfo.description,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun showFailureSnackbar(
|
||||
context: Context,
|
||||
loadFailureInfo: LoadFailureInfo,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onActionPerformed: () -> Unit
|
||||
) {
|
||||
val message = if (loadFailureInfo.title == LaunchUtils.LauncherError.CRASHED) {
|
||||
context.getString(R.string.launcher_crashed)
|
||||
} else {
|
||||
context.getString(R.string.launcher_failed_to_load)
|
||||
}
|
||||
|
||||
val res = snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = context.getString(R.string.launcher_error_more),
|
||||
duration = SnackbarDuration.Indefinite,
|
||||
)
|
||||
|
||||
when (res) {
|
||||
SnackbarResult.ActionPerformed -> {
|
||||
onActionPerformed()
|
||||
}
|
||||
SnackbarResult.Dismissed -> {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.geode.launcher.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.GeometryDashActivity
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.SettingsActivity
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.GeodeUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.geode.launcher.utils.useCountdownTimer
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
|
||||
@Composable
|
||||
fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val packageManager = context.packageManager
|
||||
|
||||
val gdVersionCode = remember { GamePackageUtils.getGameVersionCode(packageManager) }
|
||||
val gdVersionString = remember { GamePackageUtils.getGameVersionString(packageManager) }
|
||||
|
||||
var lastDismissedVersion by PreferenceUtils.useLongPreference(
|
||||
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
|
||||
)
|
||||
|
||||
val canDismissRelease = gdVersionCode >= Constants.SUPPORTED_VERSION_CODE
|
||||
val shouldDismiss = canDismissRelease && gdVersionCode == lastDismissedVersion
|
||||
|
||||
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && !shouldDismiss) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.launcher_unsupported_version_title))
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.launcher_unsupported_version_description,
|
||||
gdVersionString,
|
||||
Constants.SUPPORTED_VERSION_STRING
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDismiss()
|
||||
lastDismissedVersion = gdVersionCode
|
||||
|
||||
onLaunch(context, inSafeMode)
|
||||
}) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
onDismissRequest = { onDismiss() }
|
||||
)
|
||||
} else {
|
||||
LaunchedEffect(gdVersionCode) {
|
||||
onLaunch(context, inSafeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Warning,
|
||||
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
|
||||
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onLaunch) {
|
||||
Text(stringResource(R.string.message_box_continue))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
fun onLaunch(context: Context, safeMode: Boolean = false) {
|
||||
if (safeMode) {
|
||||
GeodeUtils.setAdditionalLaunchArguments(GeodeUtils.ARGUMENT_SAFE_MODE)
|
||||
} else {
|
||||
GeodeUtils.clearLaunchArguments()
|
||||
}
|
||||
|
||||
val launchIntent = Intent(context, GeometryDashActivity::class.java)
|
||||
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
fun onSettings(context: Context) {
|
||||
val launchIntent = Intent(context, SettingsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlayButton(
|
||||
stopAutomaticLaunch: Boolean,
|
||||
blockLaunch: Boolean,
|
||||
onPlayGame: (Boolean) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showSafeModeDialog by remember { mutableStateOf(false) }
|
||||
var launchInSafeMode by remember { mutableStateOf(false) }
|
||||
|
||||
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
|
||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
||||
)
|
||||
|
||||
if (shouldAutomaticallyLaunch && !stopAutomaticLaunch && !showSafeModeDialog) {
|
||||
val countdownTimer = useCountdownTimer(
|
||||
time = 3000,
|
||||
onCountdownFinish = { onPlayGame(launchInSafeMode) }
|
||||
)
|
||||
|
||||
if (countdownTimer != 0L) {
|
||||
Text(
|
||||
pluralStringResource(
|
||||
R.plurals.automatically_load_countdown,
|
||||
countdownTimer.toInt(),
|
||||
countdownTimer
|
||||
),
|
||||
style = Typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// compose apis don't provide a good way of adding long press to a button
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val viewConfiguration = LocalViewConfiguration.current
|
||||
val haptics = LocalHapticFeedback.current
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collectLatest { interaction ->
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> {
|
||||
launchInSafeMode = false
|
||||
|
||||
delay(viewConfiguration.longPressTimeoutMillis)
|
||||
|
||||
// perform a second delay to make the action more obvious
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
delay(viewConfiguration.longPressTimeoutMillis)
|
||||
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
|
||||
showSafeModeDialog = true
|
||||
}
|
||||
|
||||
is PressInteraction.Release -> {
|
||||
if (!showSafeModeDialog) {
|
||||
onPlayGame(launchInSafeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Button(
|
||||
onClick = { },
|
||||
enabled = !blockLaunch,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = context.getString(R.string.launcher_launch_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(context.getString(R.string.launcher_launch))
|
||||
}
|
||||
Spacer(Modifier.size(2.dp))
|
||||
IconButton(onClick = { onSettings(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSafeModeDialog) {
|
||||
SafeModeDialog(
|
||||
onDismiss = {
|
||||
launchInSafeMode = false
|
||||
showSafeModeDialog = false
|
||||
},
|
||||
onLaunch = {
|
||||
launchInSafeMode = true
|
||||
|
||||
onPlayGame(true)
|
||||
|
||||
showSafeModeDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LaunchBlockedLabel(text: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val showDownload = remember {
|
||||
GamePackageUtils.isGameInstalled(context.packageManager) &&
|
||||
!GamePackageUtils.identifyGameLegitimacy(context.packageManager)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text, textAlign = TextAlign.Center, modifier = Modifier.padding(12.dp))
|
||||
|
||||
if (showDownload) {
|
||||
GooglePlayBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -33,7 +34,7 @@ import com.geode.launcher.ui.theme.LocalTheme
|
||||
import com.geode.launcher.ui.theme.Theme
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
fun onSettings(context: Context) {
|
||||
fun onNotificationSettings(context: Context) {
|
||||
val launchIntent = Intent(context, SettingsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
@@ -64,6 +65,7 @@ fun FadeOutContainer() {
|
||||
|
||||
AnimatedVisibility(
|
||||
visibleState = state,
|
||||
enter = slideInHorizontally(),
|
||||
exit = slideOutHorizontally()
|
||||
) {
|
||||
Box(modifier = Modifier.padding(8.dp)) {
|
||||
@@ -82,7 +84,7 @@ fun MainView() {
|
||||
containerColor = surfaceColor
|
||||
),
|
||||
onClick = {
|
||||
onSettings(context)
|
||||
onNotificationSettings(context)
|
||||
}
|
||||
) {
|
||||
NotificationContent()
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.geode.launcher.main
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.updater.ReleaseManager
|
||||
import com.geode.launcher.updater.ReleaseViewModel
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
|
||||
@Composable
|
||||
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDismiss: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ElevatedCard(modifier) {
|
||||
Column(
|
||||
// buttons add enough padding already, lower to compensate
|
||||
modifier = Modifier.padding(
|
||||
top = 20.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.launcher_update_available),
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.align(Alignment.End)) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.launcher_update_dismiss))
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(4.dp))
|
||||
|
||||
TextButton(onClick = { uriHandler.openUri(openTo) }) {
|
||||
Text(stringResource(R.string.launcher_download))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateProgressIndicator(
|
||||
message: String,
|
||||
releaseViewModel: ReleaseViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
progress: (() -> Float)? = null
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(message)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
if (progress == null) {
|
||||
LinearProgressIndicator()
|
||||
} else {
|
||||
LinearProgressIndicator(progress = progress)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
releaseViewModel.cancelUpdate()
|
||||
},
|
||||
modifier = Modifier.offset((-12).dp)
|
||||
) {
|
||||
Text(stringResource(R.string.release_fetch_button_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateMessageIndicator(
|
||||
message: String,
|
||||
releaseViewModel: ReleaseViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
allowRetry: Boolean = false
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
message,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
|
||||
if (allowRetry) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
releaseViewModel.runReleaseCheck(true)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.release_fetch_button_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val releaseState by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
when (val state = releaseState) {
|
||||
is ReleaseViewModel.ReleaseUIState.Failure -> {
|
||||
val message = when (state.exception) {
|
||||
is UnknownHostException, is ConnectException ->
|
||||
stringResource(R.string.release_fetch_no_internet)
|
||||
is ReleaseManager.UpdateException -> {
|
||||
when (state.exception.reason) {
|
||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||
R.string.release_fetch_manual_check_required
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
UpdateMessageIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_failed,
|
||||
message ?: stringResource(R.string.release_fetch_try_later)
|
||||
),
|
||||
modifier = modifier,
|
||||
allowRetry = true,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||
val downloaded = remember(state.downloaded) {
|
||||
Formatter.formatShortFileSize(context, state.downloaded)
|
||||
}
|
||||
|
||||
val outOf = remember(state.outOf) {
|
||||
Formatter.formatShortFileSize(context, state.outOf)
|
||||
}
|
||||
|
||||
UpdateProgressIndicator(
|
||||
stringResource(
|
||||
R.string.release_fetch_downloading,
|
||||
downloaded,
|
||||
outOf
|
||||
),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel,
|
||||
progress = {
|
||||
val progress = state.downloaded / state.outOf.toDouble()
|
||||
progress.toFloat()
|
||||
}
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||
UpdateProgressIndicator(
|
||||
stringResource(R.string.release_fetch_in_progress),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
||||
if (state.hasUpdated) {
|
||||
UpdateMessageIndicator(
|
||||
stringResource(R.string.release_fetch_success),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Cancelled -> {
|
||||
if (state.isCancelling) {
|
||||
UpdateProgressIndicator(
|
||||
stringResource(R.string.release_fetch_cancelling),
|
||||
modifier = modifier,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
} else {
|
||||
UpdateMessageIndicator(
|
||||
stringResource(R.string.release_fetch_cancelled),
|
||||
modifier = modifier,
|
||||
allowRetry = true,
|
||||
releaseViewModel = releaseViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
|
||||
val nextUpdateValue = nextUpdate
|
||||
|
||||
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
|
||||
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
|
||||
?: nextUpdateValue.htmlUrl
|
||||
|
||||
LauncherUpdateIndicator(
|
||||
modifier = modifier,
|
||||
openTo = updateUrl,
|
||||
onDismiss = {
|
||||
releaseViewModel.dismissLauncherUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user