diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2db598d56..4f76521b0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -128,6 +128,9 @@ dependencies {
// gif support
implementation("io.coil-kt:coil-gif:2.4.0")
+ // crash handling
+ implementation("com.github.FunkyMuse:Crashy:1.2.0")
+
// To use Kotlin annotation processing tool
ksp("androidx.room:room-compiler:2.5.2")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a28931f54..b4b681a12 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -130,6 +130,15 @@
+
+
+
diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt
index da914304c..e93f49c37 100644
--- a/app/src/main/java/com/jerboa/MainActivity.kt
+++ b/app/src/main/java/com/jerboa/MainActivity.kt
@@ -79,6 +79,7 @@ import com.jerboa.ui.components.report.post.CreatePostReportActivity
import com.jerboa.ui.components.settings.SettingsActivity
import com.jerboa.ui.components.settings.about.AboutActivity
import com.jerboa.ui.components.settings.account.AccountSettingsActivity
+import com.jerboa.ui.components.settings.crashlogs.CrashLogsActivity
import com.jerboa.ui.components.settings.lookandfeel.LookAndFeelActivity
import com.jerboa.ui.theme.JerboaTheme
import com.jerboa.util.BackConfirmation.addConfirmationDialog
@@ -658,6 +659,12 @@ class MainActivity : AppCompatActivity() {
)
}
+ composable(route = Route.CRASH_LOGS) {
+ CrashLogsActivity(
+ navController = navController,
+ )
+ }
+
composable(
route = Route.VIEW,
arguments = listOf(
diff --git a/app/src/main/java/com/jerboa/Utils.kt b/app/src/main/java/com/jerboa/Utils.kt
index fac8d83ab..9b411429c 100644
--- a/app/src/main/java/com/jerboa/Utils.kt
+++ b/app/src/main/java/com/jerboa/Utils.kt
@@ -22,6 +22,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.DrawerState
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TabPosition
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateListOf
@@ -782,6 +784,24 @@ fun scrollToTop(
}
}
+fun showSnackbar(
+ scope: CoroutineScope,
+ snackbarHostState: SnackbarHostState,
+ message: String,
+ actionLabel: String?,
+ withDismissAction: Boolean = false,
+ snackbarDuration: SnackbarDuration,
+) {
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message,
+ actionLabel,
+ withDismissAction,
+ snackbarDuration,
+ )
+ }
+}
+
// https://stackoverflow.com/questions/69234880/how-to-get-intent-data-in-a-composable
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
diff --git a/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt b/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt
index 5b713b7c8..64097a31c 100644
--- a/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt
+++ b/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt
@@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
import com.jerboa.R
import com.jerboa.datatypes.samplePerson
import com.jerboa.datatypes.samplePost
@@ -59,6 +60,7 @@ fun SimpleTopAppBar(
text: String,
navController: NavController,
scrollBehavior: TopAppBarScrollBehavior? = null,
+ actions: @Composable RowScope.() -> Unit = {},
) {
TopAppBar(
scrollBehavior = scrollBehavior,
@@ -75,9 +77,18 @@ fun SimpleTopAppBar(
)
}
},
+ actions = actions,
)
}
+@Preview
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SimpleTopAppBarPreview() {
+ SimpleTopAppBar(text = "Preview", navController = rememberNavController()) {
+ }
+}
+
@Composable
fun BottomAppBarAll(
selectedTab: NavTab,
@@ -552,5 +563,7 @@ fun Modifier.simpleVerticalScrollbar(
fun LoadingBar(
padding: PaddingValues = PaddingValues(0.dp),
) {
- LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(padding).testTag("jerboa:loading"))
+ LinearProgressIndicator(
+ modifier = Modifier.fillMaxWidth().padding(padding).testTag("jerboa:loading"),
+ )
}
diff --git a/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt b/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt
index 73cdee211..06b712244 100644
--- a/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt
+++ b/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt
@@ -153,6 +153,8 @@ fun NavController.toLookAndFeel() = navigate(Route.LOOK_AND_FEEL)
fun NavController.toAbout() = navigate(Route.ABOUT)
+fun NavController.toCrashLogs() = navigate(Route.CRASH_LOGS)
+
fun NavController.toView(url: String) {
val encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.name())
navigate(Route.ViewArgs.makeRoute(encodedUrl))
diff --git a/app/src/main/java/com/jerboa/ui/components/common/Route.kt b/app/src/main/java/com/jerboa/ui/components/common/Route.kt
index 1f97669dc..d3488295f 100644
--- a/app/src/main/java/com/jerboa/ui/components/common/Route.kt
+++ b/app/src/main/java/com/jerboa/ui/components/common/Route.kt
@@ -41,6 +41,7 @@ object Route {
const val LOOK_AND_FEEL = "lookAndFeel"
const val ACCOUNT_SETTINGS = "accountSettings"
const val ABOUT = "about"
+ const val CRASH_LOGS = "crashLogs"
val VIEW = ViewArgs.route
diff --git a/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt
index 27156ff72..b0b916e26 100644
--- a/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt
+++ b/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt
@@ -32,6 +32,7 @@ import com.alorma.compose.settings.ui.SettingsMenuLink
import com.jerboa.R
import com.jerboa.openLink
import com.jerboa.ui.components.common.SimpleTopAppBar
+import com.jerboa.ui.components.common.toCrashLogs
const val githubUrl = "https://github.com/dessalines/jerboa"
const val jerboaMatrixChat = "https://matrix.to/#/#jerboa-dev:matrix.org"
@@ -97,6 +98,16 @@ fun AboutActivity(
openLink("$githubUrl/issues")
},
)
+ SettingsMenuLink(
+ title = { Text(stringResource(R.string.crash_logs)) },
+ icon = {
+ Icon(
+ imageVector = Icons.Outlined.Build,
+ contentDescription = null,
+ )
+ },
+ onClick = { navController.toCrashLogs() },
+ )
SettingsMenuLink(
title = { Text(stringResource(R.string.settings_about_developer_matrix_chatroom)) },
icon = {
diff --git a/app/src/main/java/com/jerboa/ui/components/settings/crashlogs/CrashLogsActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/crashlogs/CrashLogsActivity.kt
new file mode 100644
index 000000000..662c6d4b4
--- /dev/null
+++ b/app/src/main/java/com/jerboa/ui/components/settings/crashlogs/CrashLogsActivity.kt
@@ -0,0 +1,161 @@
+package com.jerboa.ui.components.settings.crashlogs
+
+import android.content.Context
+import android.util.Log
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ContentCopy
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import com.crazylegend.crashyreporter.CrashyReporter
+import com.jerboa.R
+import com.jerboa.copyToClipboard
+import com.jerboa.showSnackbar
+import com.jerboa.ui.components.common.SimpleTopAppBar
+import com.jerboa.ui.theme.MEDIUM_PADDING
+import com.jerboa.ui.theme.SMALL_PADDING
+
+@Composable
+fun CrashLogsActivity(
+ navController: NavController,
+) {
+ Log.d("jerboa", "Got to Crash log activity")
+
+ val ctx = LocalContext.current
+
+ val crashes = CrashyReporter.getLogsAsStrings()?.toMutableStateList() ?: run { mutableStateListOf() }
+
+ CrashLogs(ctx = ctx, navController = navController, crashes = crashes)
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CrashLogs(ctx: Context, navController: NavController, crashes: MutableList) {
+ val scope = rememberCoroutineScope()
+
+ val snackbarHostState = remember { SnackbarHostState() }
+ val deleteMessage = stringResource(R.string.crash_logs_all_deleted)
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ topBar = {
+ SimpleTopAppBar(
+ text = stringResource(R.string.crash_logs),
+ navController = navController,
+ actions = {
+ IconButton(
+ onClick = {
+ CrashyReporter.purgeLogs()
+ crashes.clear()
+ showSnackbar(
+ scope,
+ snackbarHostState,
+ deleteMessage,
+ null,
+ true,
+ SnackbarDuration.Short,
+ )
+ },
+ ) {
+ Icon(
+ Icons.Outlined.Delete,
+ contentDescription = stringResource(R.string.crash_logs_delete),
+ )
+ }
+ },
+ )
+ },
+ content = { padding ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(padding),
+ ) {
+ crashes.forEachIndexed { _, crash ->
+ CrashLog(ctx = ctx, crash = crash)
+ }
+ }
+ },
+ )
+}
+
+@Composable
+fun CrashLog(ctx: Context, crash: String) {
+ var expanded by remember { mutableStateOf(false) }
+ val textModifier = Modifier.clickable(onClick = { expanded = !expanded })
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(MEDIUM_PADDING),
+ ) {
+ Column(
+ modifier = Modifier.padding(MEDIUM_PADDING),
+ ) {
+ IconButton(onClick = {
+ copyToClipboard(ctx, crash, "crash")
+ }) {
+ Icon(
+ Icons.Outlined.ContentCopy,
+ contentDescription = stringResource(R.string.crash_logs_copy),
+ )
+ }
+ }
+ if (expanded) {
+ Text(
+ text = crash,
+ modifier = textModifier,
+ )
+ } else {
+ Text(
+ text = crash,
+ maxLines = 4,
+ overflow = TextOverflow.Ellipsis,
+ modifier = textModifier,
+ )
+ }
+ }
+ Divider(modifier = Modifier.padding(bottom = SMALL_PADDING))
+}
+
+@Preview
+@Composable
+fun CrashLogsPreview() {
+ CrashLogs(
+ ctx = LocalContext.current,
+ navController = rememberNavController(),
+ crashes = mutableListOf(
+ "A really bad one\nlots\nof\ntrace\nlines\nhere",
+ "NullPointerException!",
+ ),
+ )
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 72fe613a5..442c0f041 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -102,6 +102,10 @@
Sort by
Subscribe
%1$s users / month
+ Crash Logs
+ All crash logs deleted
+ Copy crash log
+ Delete all crash logs
Select community from list
a title here....
Community