mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-06-18 05:23:12 +10:00
Overhaul Manager Updates & Logging
This commit is contained in:
@@ -18,7 +18,6 @@ import com.meowarex.rlmobile.ui.screens.about.AboutModel
|
||||
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsModel
|
||||
import com.meowarex.rlmobile.ui.screens.home.HomeModel
|
||||
import com.meowarex.rlmobile.ui.screens.log.LogScreenModel
|
||||
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreenModel
|
||||
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreenModel
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsModel
|
||||
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
|
||||
@@ -64,7 +63,6 @@ class ManagerApplication : Application() {
|
||||
factoryOf(::PatchOptionsModel)
|
||||
factoryOf(::ComponentOptionsModel)
|
||||
factoryOf(::LogScreenModel)
|
||||
factoryOf(::LogsListScreenModel)
|
||||
factoryOf(::PermissionsModel)
|
||||
viewModelOf(::UpdaterViewModel)
|
||||
})
|
||||
|
||||
@@ -12,8 +12,6 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||
import com.meowarex.rlmobile.util.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@@ -21,52 +19,19 @@ import kotlin.time.Duration
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Central manager for storing all attempted installations and
|
||||
* their associated logs/crashes (not including manager crashes themselves).
|
||||
* In-memory store for the most recent install attempts
|
||||
*/
|
||||
class InstallLogManager(
|
||||
private val application: Application,
|
||||
private val prefs: PreferencesManager,
|
||||
private val json: Json,
|
||||
@Suppress("unused") private val json: Json,
|
||||
) {
|
||||
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
|
||||
private val entries = linkedMapOf<String, InstallLogData>()
|
||||
|
||||
/**
|
||||
* Lists all the install data entries that exist on disk, sorted decreasing by
|
||||
* the file creation date.
|
||||
* @return List of installation ids, most recent installation first.
|
||||
*/
|
||||
fun fetchInstallDataEntries(): List<String> {
|
||||
val files = logsDir.listFiles { it.extension == "json" } ?: emptyArray()
|
||||
|
||||
return files
|
||||
.sortedByDescending { it.lastModified() }
|
||||
.map { it.nameWithoutExtension }
|
||||
fun fetchInstallData(id: String): InstallLogData? = synchronized(entries) {
|
||||
entries[id]
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the install log from disk, if it exists.
|
||||
*/
|
||||
fun fetchInstallData(id: String): InstallLogData? {
|
||||
val path = logsDir.resolve("$id.json")
|
||||
if (!path.exists()) return null
|
||||
|
||||
return try {
|
||||
json.decodeFromStream(path.inputStream())
|
||||
} catch (t: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to open install log $id", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntries() {
|
||||
logsDir.deleteRecursively()
|
||||
logsDir.mkdir()
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an install log entry to disk.
|
||||
*/
|
||||
suspend fun storeInstallData(
|
||||
id: String,
|
||||
installDate: Instant,
|
||||
@@ -75,8 +40,6 @@ class InstallLogManager(
|
||||
log: String,
|
||||
error: Throwable?,
|
||||
) {
|
||||
val path = logsDir.resolve("$id.json")
|
||||
|
||||
val data = InstallLogData(
|
||||
id = id,
|
||||
installDate = installDate,
|
||||
@@ -87,16 +50,16 @@ class InstallLogManager(
|
||||
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
|
||||
)
|
||||
|
||||
try {
|
||||
path.writeText(json.encodeToString(data))
|
||||
} catch (e: IOException) {
|
||||
Log.e(BuildConfig.TAG, "Failed to write log to disk", e)
|
||||
synchronized(entries) {
|
||||
entries[id] = data
|
||||
// Keep only the most recent few in memory.
|
||||
while (entries.size > MAX_ENTRIES) {
|
||||
val oldest = entries.keys.iterator().next()
|
||||
entries.remove(oldest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of details about the current installation environment.
|
||||
*/
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
@SuppressLint("UsableSpace")
|
||||
suspend fun getEnvironmentInfo(): String {
|
||||
@@ -139,6 +102,10 @@ class InstallLogManager(
|
||||
SOC: $soc
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_ENTRIES = 8
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.previews.dialogs
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.meowarex.rlmobile.ui.screens.logs.components.dialogs.DeleteLogsDialog
|
||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun DeleteLogsDialogPreview() {
|
||||
ManagerTheme {
|
||||
DeleteLogsDialog(
|
||||
onConfirm = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.previews.screens.logs
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.meowarex.rlmobile.ui.screens.logs.LogEntry
|
||||
import com.meowarex.rlmobile.ui.screens.logs.LogsScreenContent
|
||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun LogsListScreenNonePreview() {
|
||||
ManagerTheme {
|
||||
LogsScreenContent(
|
||||
logs = logs,
|
||||
onOpenLog = {},
|
||||
onDeleteLogs = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val logs: SnapshotStateList<LogEntry> = mutableStateListOf(
|
||||
LogEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
isError = false,
|
||||
installDate = "5 min. ago, 10:18 AM",
|
||||
durationSecs = 18.555f,
|
||||
stacktracePreview = null,
|
||||
),
|
||||
LogEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
isError = true,
|
||||
installDate = "7 min. ago, 10:17 AM",
|
||||
durationSecs = 73.095f,
|
||||
stacktracePreview = persistentListOf(
|
||||
"kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@833e76f]",
|
||||
),
|
||||
),
|
||||
LogEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
isError = false,
|
||||
installDate = "Yesterday, 11:37 PM",
|
||||
durationSecs = 58.439f,
|
||||
stacktracePreview = null,
|
||||
),
|
||||
LogEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
isError = true,
|
||||
installDate = "Yesterday, 11:17 PM",
|
||||
durationSecs = 24.405f,
|
||||
stacktracePreview = persistentListOf(
|
||||
"java.lang.Error: Installation was aborted or cancelled",
|
||||
),
|
||||
),
|
||||
LogEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
isError = true,
|
||||
installDate = "Yesterday, 11:17 PM",
|
||||
durationSecs = 0.057f,
|
||||
stacktracePreview = persistentListOf(
|
||||
"java.lang.IllegalStateException: balls",
|
||||
"\tat com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep.execute(FetchInfoStep.kt:31)",
|
||||
"\tat com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep\$execute\\$1.invokeSuspend(Unknown Source:15)",
|
||||
),
|
||||
),
|
||||
LogEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
isError = false,
|
||||
installDate = "Yesterday, 1:11 PM",
|
||||
durationSecs = 210.539f,
|
||||
stacktracePreview = null,
|
||||
),
|
||||
)
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.previews.screens.logs
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.meowarex.rlmobile.ui.screens.logs.LogsScreenContent
|
||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun LogsListScreenNonePreview() {
|
||||
ManagerTheme {
|
||||
LogsScreenContent(
|
||||
logs = remember { mutableStateListOf() },
|
||||
onOpenLog = {},
|
||||
onDeleteLogs = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -32,13 +33,14 @@ import com.meowarex.rlmobile.ui.components.SegmentedButton
|
||||
import com.meowarex.rlmobile.ui.components.Tag
|
||||
import com.meowarex.rlmobile.ui.screens.about.AboutScreen
|
||||
import com.meowarex.rlmobile.ui.screens.home.components.CommitList
|
||||
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreen
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsScreen
|
||||
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
|
||||
import com.meowarex.rlmobile.ui.widgets.managerupdate.ManagerUpdateDialog
|
||||
import com.meowarex.rlmobile.ui.widgets.updater.UpdaterViewModel
|
||||
import com.meowarex.rlmobile.util.*
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Parcelize
|
||||
class HomeScreen : Screen, Parcelable {
|
||||
@@ -50,6 +52,9 @@ class HomeScreen : Screen, Parcelable {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val scope = rememberCoroutineScope()
|
||||
val model = koinScreenModel<HomeModel>()
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
val updater = koinViewModel<UpdaterViewModel>(viewModelStoreOwner = activity)
|
||||
val managerUpdateAvailable = updater.targetVersion != null
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
model.refresh(delay = true)
|
||||
@@ -67,18 +72,20 @@ class HomeScreen : Screen, Parcelable {
|
||||
contentDescription = stringResource(R.string.navigation_refresh),
|
||||
)
|
||||
}
|
||||
if (managerUpdateAvailable) {
|
||||
IconButton(onClick = updater::reopenDialog) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_update),
|
||||
contentDescription = stringResource(R.string.action_update),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { navigator.push(AboutScreen()) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_info),
|
||||
contentDescription = stringResource(R.string.navigation_about),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { navigator.push(LogsListScreen()) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_receipt),
|
||||
contentDescription = stringResource(R.string.navigation_logs),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { navigator.push(SettingsScreen()) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_settings),
|
||||
@@ -107,6 +114,7 @@ class HomeScreen : Screen, Parcelable {
|
||||
is HomeState.Loaded -> HomeContent(
|
||||
state = state,
|
||||
commits = model.commits,
|
||||
managerUpdateAvailable = managerUpdateAvailable,
|
||||
onInstall = { navigator.pushOnce(PatchOptionsScreen()) },
|
||||
onRepatch = {
|
||||
scope.launchIO {
|
||||
@@ -134,6 +142,7 @@ class HomeScreen : Screen, Parcelable {
|
||||
private fun ColumnScope.HomeContent(
|
||||
state: HomeState.Loaded,
|
||||
commits: kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<com.meowarex.rlmobile.network.models.GithubCommit>>,
|
||||
managerUpdateAvailable: Boolean,
|
||||
onInstall: () -> Unit,
|
||||
onRepatch: () -> Unit,
|
||||
onLaunch: () -> Unit,
|
||||
@@ -200,12 +209,14 @@ private fun ColumnScope.HomeContent(
|
||||
}
|
||||
}
|
||||
|
||||
val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind)
|
||||
Button(
|
||||
onClick = if (install == null) onInstall else onRepatch,
|
||||
enabled = state.latestTidalVersionCode != null,
|
||||
enabled = state.latestTidalVersionCode != null && !blockedByManagerUpdate,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val label = when {
|
||||
blockedByManagerUpdate -> "Manager Update Required"
|
||||
state.latestTidalVersionCode == null -> "Loading…"
|
||||
install == null -> "Install"
|
||||
patchesBehind && tidalBehind -> "Update Patches & TIDAL"
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.screens.logs
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.koin.koinScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.meowarex.rlmobile.ui.screens.log.LogScreen
|
||||
import com.meowarex.rlmobile.ui.screens.logs.components.*
|
||||
import com.meowarex.rlmobile.ui.screens.logs.components.dialogs.DeleteLogsDialog
|
||||
import com.meowarex.rlmobile.ui.util.paddings.PaddingValuesSides
|
||||
import com.meowarex.rlmobile.ui.util.paddings.exclude
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class LogsListScreen : Screen, Parcelable {
|
||||
@IgnoredOnParcel
|
||||
override val key: ScreenKey
|
||||
get() = "LogsScreen"
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = koinScreenModel<LogsListScreenModel>()
|
||||
|
||||
var showWipeConfirmDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showWipeConfirmDialog) {
|
||||
DeleteLogsDialog(
|
||||
onConfirm = {
|
||||
showWipeConfirmDialog = false
|
||||
model.deleteLogs()
|
||||
},
|
||||
onDismiss = {
|
||||
showWipeConfirmDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LogsScreenContent(
|
||||
logs = model.logEntries,
|
||||
onOpenLog = { navigator.push(LogScreen(installId = it)) },
|
||||
onDeleteLogs = { showWipeConfirmDialog = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogsScreenContent(
|
||||
logs: SnapshotStateList<LogEntry>,
|
||||
onOpenLog: (id: String) -> Unit,
|
||||
onDeleteLogs: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
LogsListAppBar(
|
||||
onDeleteLogs = onDeleteLogs,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = paddingValues.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top),
|
||||
modifier = Modifier
|
||||
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
|
||||
.padding(vertical = 12.dp, horizontal = 22.dp)
|
||||
) {
|
||||
if (logs.isEmpty()) {
|
||||
item(key = "EMPTY") {
|
||||
LogsNone(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = logs,
|
||||
contentType = { "LOG" },
|
||||
key = { it.id },
|
||||
) { data ->
|
||||
LogEntryCard(
|
||||
data = data,
|
||||
onClick = remember(onOpenLog, data.id) { { onOpenLog(data.id) } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.screens.logs
|
||||
|
||||
import android.app.Application
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.manager.InstallLogManager
|
||||
import com.meowarex.rlmobile.util.*
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class LogsListScreenModel(
|
||||
private val logsManager: InstallLogManager,
|
||||
private val application: Application,
|
||||
) : ScreenModel {
|
||||
/**
|
||||
* All the loaded log entries sorted descending by creation date.
|
||||
*/
|
||||
val logEntries = mutableStateListOf<LogEntry>()
|
||||
|
||||
init {
|
||||
loadLogsList()
|
||||
}
|
||||
|
||||
fun deleteLogs() = screenModelScope.launchIO {
|
||||
logsManager.deleteAllEntries()
|
||||
|
||||
mainThread {
|
||||
logEntries.clear()
|
||||
application.showToast(R.string.logs_status_delete_success)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLogsList() = screenModelScope.launchIO {
|
||||
for (installId in logsManager.fetchInstallDataEntries()) {
|
||||
val data = logsManager.fetchInstallData(id = installId)
|
||||
?: continue
|
||||
|
||||
val entry = LogEntry(
|
||||
id = data.id,
|
||||
isError = data.isError,
|
||||
installDate = DateUtils.getRelativeDateTimeString(
|
||||
/* c = */ application,
|
||||
/* time = */ data.installDate.toEpochMilliseconds(),
|
||||
/* minResolution = */ DateUtils.SECOND_IN_MILLIS,
|
||||
/* transitionResolution = */ DateUtils.WEEK_IN_MILLIS,
|
||||
/* flags = */ DateUtils.FORMAT_ABBREV_ALL,
|
||||
).toString(),
|
||||
durationSecs = data.installDuration.inWholeMilliseconds / 1000f,
|
||||
stacktracePreview = data.errorStacktrace
|
||||
?.splitToSequence('\n')
|
||||
?.take(3)
|
||||
?.toImmutableList(),
|
||||
)
|
||||
|
||||
mainThread { logEntries += entry }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class LogEntry(
|
||||
val id: String,
|
||||
val isError: Boolean,
|
||||
val installDate: String,
|
||||
val durationSecs: Float,
|
||||
val stacktracePreview: ImmutableList<String>?,
|
||||
)
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.screens.logs.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.patcher.steps.base.StepState
|
||||
import com.meowarex.rlmobile.ui.screens.logs.LogEntry
|
||||
import com.meowarex.rlmobile.ui.screens.patching.components.StepStateIcon
|
||||
import com.meowarex.rlmobile.ui.screens.patching.components.TimeElapsed
|
||||
|
||||
@Composable
|
||||
fun LogEntryCard(
|
||||
data: LogEntry,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val errorColor = MaterialTheme.colorScheme.error
|
||||
|
||||
ElevatedCard(
|
||||
shape = RectangleShape,
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClick)
|
||||
.clip(RoundedCornerShape(topStart = 6.dp, 12.0.dp, bottomStart = 6.dp, bottomEnd = 12.0.dp))
|
||||
.drawWithCache {
|
||||
val color = when (data.isError) {
|
||||
true -> errorColor
|
||||
false -> Color(0xFF59B463)
|
||||
}
|
||||
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
color = color,
|
||||
alpha = .8f,
|
||||
topLeft = Offset.Zero,
|
||||
size = Size(4.dp.toPx(), size.height),
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 18.dp, horizontal = 22.dp),
|
||||
) {
|
||||
StepStateIcon(
|
||||
state = if (data.isError) StepState.Error else StepState.Success,
|
||||
size = 24.dp,
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = when (data.isError) {
|
||||
true -> stringResource(R.string.status_failed)
|
||||
false -> stringResource(R.string.status_success)
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = data.installDate,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.alpha(.6f),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f, fill = true))
|
||||
|
||||
TimeElapsed(
|
||||
seconds = data.durationSecs,
|
||||
modifier = Modifier.alpha(.9f),
|
||||
)
|
||||
}
|
||||
|
||||
if (data.stacktracePreview != null) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 26.dp, end = 20.dp, bottom = 18.dp)
|
||||
// https://stackoverflow.com/a/76270310/13964629
|
||||
.graphicsLayer(
|
||||
alpha = .95f,
|
||||
compositingStrategy = CompositingStrategy.Offscreen,
|
||||
)
|
||||
.drawWithContent {
|
||||
val colors = listOf(Color.Black, Color.Black, Color.Transparent)
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(colors),
|
||||
blendMode = BlendMode.DstIn,
|
||||
)
|
||||
}
|
||||
) {
|
||||
// The stacktrace is separated into multiple Text elements because ellipsis is not supported per-line
|
||||
for (line in data.stacktracePreview) key(line) {
|
||||
Text(
|
||||
text = line,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 18.sp,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontFamily = FontFamily.Companion.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.screens.logs.components
|
||||
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.ui.components.BackButton
|
||||
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
|
||||
|
||||
@Composable
|
||||
fun LogsListAppBar(
|
||||
onDeleteLogs: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
navigationIcon = { BackButton() },
|
||||
title = { Text(stringResource(R.string.logs_title)) },
|
||||
actions = {
|
||||
val navigator = LocalNavigator.current
|
||||
|
||||
IconButton(onClick = onDeleteLogs) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete_forever),
|
||||
contentDescription = stringResource(R.string.logs_action_delete_all)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { navigator?.push(SettingsScreen()) }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_settings),
|
||||
contentDescription = stringResource(R.string.navigation_settings)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.screens.logs.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meowarex.rlmobile.R
|
||||
|
||||
@Composable
|
||||
fun LogsNone(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_reciept_off),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.logs_none),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.alpha(.8f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package com.meowarex.rlmobile.ui.screens.logs.components.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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 com.meowarex.rlmobile.R
|
||||
|
||||
@Composable
|
||||
fun DeleteLogsDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete_forever),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.logs_wipe_title)) },
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.logs_wipe_desc),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
) {
|
||||
Text(stringResource(R.string.action_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+4
@@ -55,6 +55,10 @@ class UpdaterViewModel(
|
||||
showDialog = false
|
||||
}
|
||||
|
||||
fun reopenDialog() {
|
||||
if (targetVersion != null) showDialog = true
|
||||
}
|
||||
|
||||
fun triggerUpdate() = viewModelScope.launchIO {
|
||||
if (!isWorking.compareAndSet(expect = false, update = true))
|
||||
return@launchIO
|
||||
|
||||
Reference in New Issue
Block a user