crash logs activity is now composable

This commit is contained in:
Chloe committed 2026-02-24 15:55:22 -07:00
1 parent 6d5de931fc
commit 7bab94bf40
4 files changed
+238 -150

No files matched your search

@@ -21,6 +21,8 @@ abstract class BaseDirectoryViewModel<T>(protected val baseDirectory: File) : Vi
abstract fun getFileList(): List<T>
abstract fun getItemKey(item: T): Any
open fun clearAllFiles() {
logJob?.cancel()
@@ -31,6 +31,8 @@ class CrashViewModel(crashDirectory: File) : BaseDirectoryViewModel<CrashDump>(c
var hasIndicator = false
override fun getItemKey(item: CrashDump) = item.filename
override fun getFileList(): List<CrashDump> {
val crashDirectory = baseDirectory
@@ -0,0 +1,196 @@
package com.geode.launcher.preferences
import androidx.activity.OnBackPressedDispatcher
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
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.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.geode.launcher.R
import com.geode.launcher.log.BaseDirectoryViewModel
import com.geode.launcher.ui.theme.Typography
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrashLogsTopBar(
onBackPressedDispatcher: OnBackPressedDispatcher?,
directoryViewModel: BaseDirectoryViewModel<*>,
scrollBehavior: TopAppBarScrollBehavior,
@StringRes titleId: Int,
additionalOptions: (@Composable (() -> Unit) -> Unit)? = null,
) {
val isLoading by directoryViewModel.isLoading.collectAsState()
var showMoreOptions by remember { mutableStateOf(false) }
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
actions = {
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
painterResource(R.drawable.icon_more_vert),
contentDescription = stringResource(R.string.application_logs_more)
)
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
directoryViewModel.clearAllFiles()
showMoreOptions = false
}
)
additionalOptions?.invoke {
showMoreOptions = false
}
}
},
title = {
Text(
stringResource(titleId),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
fun <T> CrashLogsListing(
modifier: Modifier = Modifier,
directoryViewModel: BaseDirectoryViewModel<T>,
@StringRes noneAvailableId: Int,
logCard: @Composable (T, Shape) -> Unit,
) {
val logLines = directoryViewModel.lineState
val isLoading by directoryViewModel.isLoading.collectAsState()
LazyColumn(
modifier,
verticalArrangement = Arrangement.SpaceEvenly,
) {
if (!isLoading && logLines.isEmpty()) {
item {
Text(
stringResource(noneAvailableId),
textAlign = TextAlign.Center,
style = Typography.bodyLarge,
modifier = Modifier.fillMaxSize().padding(8.dp)
)
}
}
val itemsSize = logLines.size
itemsIndexed(
logLines,
key = { index, line -> directoryViewModel.getItemKey(line) }
) { index, line ->
val shape = RoundedCornerShape(
topStart = if (index == 0) 16.dp else 4.dp,
topEnd = if (index == 0) 16.dp else 4.dp,
bottomStart = if (index + 1 == itemsSize) 16.dp else 4.dp,
bottomEnd = if (index + 1 == itemsSize) 16.dp else 4.dp
)
logCard(line, shape)
}
item {
Spacer(Modifier.height(4.dp))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> DirectoryListingScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
directoryViewModel: BaseDirectoryViewModel<T>,
@StringRes titleId: Int,
@StringRes noneAvailableId: Int,
additionalOptions: (@Composable (() -> Unit) -> Unit)? = null,
logCard: @Composable (T, Shape) -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
CrashLogsTopBar(
onBackPressedDispatcher,
directoryViewModel,
scrollBehavior,
titleId,
additionalOptions = additionalOptions
)
}
) { innerPadding ->
CrashLogsListing(
modifier = Modifier
.padding(innerPadding)
.fillMaxWidth(),
directoryViewModel,
noneAvailableId = noneAvailableId,
logCard = logCard
)
}
}
@@ -15,43 +15,28 @@ import androidx.compose.foundation.background
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -63,7 +48,6 @@ import com.geode.launcher.log.CrashViewModel
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.ui.theme.robotoMonoFamily
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
@@ -180,154 +164,58 @@ fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Mo
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrashLogsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
crashViewModel: CrashViewModel = viewModel(factory = CrashViewModel.Factory)
) {
val logLines = crashViewModel.lineState
val isLoading by crashViewModel.isLoading.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
var showMoreOptions by remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
DirectoryListingScreen(
onBackPressedDispatcher,
crashViewModel,
titleId = R.string.title_activity_crash_logs,
noneAvailableId = R.string.application_crashes_no_dumps,
additionalOptions = { onDismiss ->
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
if (developerMode) {
if (crashViewModel.hasIndicator) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
painterResource(R.drawable.icon_remove_selection),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_crashes_clear_indicator))
}, onClick = {
crashViewModel.clearIndicator()
onDismiss()
}
},
actions = {
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
)
} else {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_more_vert),
contentDescription = stringResource(R.string.application_logs_more)
painterResource(R.drawable.icon_skull),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_crashes_create_indicator))
}, onClick = {
crashViewModel.createIndicator()
onDismiss()
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
crashViewModel.clearAllFiles()
showMoreOptions = false
}
)
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
if (developerMode) {
if (crashViewModel.hasIndicator) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_remove_selection),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_crashes_clear_indicator))
}, onClick = {
crashViewModel.clearIndicator()
showMoreOptions = false
}
)
} else {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_skull),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_crashes_create_indicator))
}, onClick = {
crashViewModel.createIndicator()
showMoreOptions = false
}
)
}
}
}
},
title = {
Text(
stringResource(R.string.title_activity_crash_logs),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
) { innerPadding ->
LazyColumn(
Modifier
.padding(innerPadding)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceEvenly,
) {
if (!isLoading && logLines.isEmpty()) {
item {
Text(
stringResource(R.string.application_crashes_no_dumps),
textAlign = TextAlign.Center,
style = Typography.bodyLarge,
modifier = Modifier.fillMaxSize().padding(8.dp)
)
}
}
val itemsSize = logLines.size
itemsIndexed(
logLines,
key = { index, line -> line.filename }
) { index, line ->
val shape = RoundedCornerShape(
topStart = if (index == 0) 16.dp else 4.dp,
topEnd = if (index == 0) 16.dp else 4.dp,
bottomStart = if (index + 1 == itemsSize) 16.dp else 4.dp,
bottomEnd = if (index + 1 == itemsSize) 16.dp else 4.dp
)
CrashCard(
line,
crashViewModel,
modifier = Modifier.clip(shape)
)
}
item {
Spacer(Modifier.height(4.dp))
}
}
) { line, shape ->
CrashCard(
line,
crashViewModel,
modifier = Modifier.clip(shape)
)
}
}
@@ -337,4 +225,4 @@ fun CrashLogsPreview() {
GeodeLauncherTheme {
CrashLogsScreen(null)
}
}
}