diff --git a/app/src/main/java/com/paulcoding/hviewer/MainApp.kt b/app/src/main/java/com/paulcoding/hviewer/MainApp.kt index be392d6..495f3d4 100644 --- a/app/src/main/java/com/paulcoding/hviewer/MainApp.kt +++ b/app/src/main/java/com/paulcoding/hviewer/MainApp.kt @@ -2,11 +2,13 @@ package com.paulcoding.hviewer import android.app.Application import android.content.Context +import com.paulcoding.hviewer.helper.CrashHandler import com.paulcoding.hviewer.helper.setupPaths import com.tencent.mmkv.MMKV class MainApp : Application() { override fun onCreate() { + CrashHandler.install() super.onCreate() appContext = this MMKV.initialize(this) diff --git a/app/src/main/java/com/paulcoding/hviewer/helper/CrashHandler.kt b/app/src/main/java/com/paulcoding/hviewer/helper/CrashHandler.kt new file mode 100644 index 0000000..fc3bb87 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/helper/CrashHandler.kt @@ -0,0 +1,42 @@ +package com.paulcoding.hviewer.helper + +import com.paulcoding.hviewer.MainApp.Companion.appContext +import java.io.File +import java.io.PrintWriter + +// Reference: https://github.com/FooIbar/EhViewer/blob/main/app/src/main/kotlin/com/hippo/ehviewer/util/CrashHandler.kt +object CrashHandler { + fun install() { + val handler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { t, e -> + runCatching { saveCrashLog(e) } + handler?.uncaughtException(t, e) + } + } + + private fun getThrowableInfo(t: Throwable, writer: PrintWriter) { + t.printStackTrace(writer) + var cause = t.cause + while (cause != null) { + cause.printStackTrace(writer) + cause = cause.cause + } + } + + private fun saveCrashLog(e: Throwable) { + val nowString = System.currentTimeMillis() + val fileName = "crash-$nowString.log" + val file = File(appContext.crashLogDir, fileName) + + runCatching { + file.printWriter().use { writer -> + writer.write("======== CrashInfo ========\n") + getThrowableInfo(e, writer) + writer.write("\n") + } + }.onFailure { + log(it) + file.delete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/helper/File.kt b/app/src/main/java/com/paulcoding/hviewer/helper/File.kt index c7d2ce7..2f5bffe 100644 --- a/app/src/main/java/com/paulcoding/hviewer/helper/File.kt +++ b/app/src/main/java/com/paulcoding/hviewer/helper/File.kt @@ -7,28 +7,33 @@ import java.io.File import java.io.FileOutputStream const val SCRIPTS_DIR = "scripts" +const val CRASH_LOG_DIR = "crash_logs" const val CONFIG_FILE = "config.json" val Context.scriptsDir get() = File(filesDir, SCRIPTS_DIR) +val Context.crashLogDir + get() = File(filesDir, CRASH_LOG_DIR) + val Context.configFile get() = File(scriptsDir, CONFIG_FILE) fun Context.setupPaths() { scriptsDir.mkdir() + crashLogDir.mkdir() } -fun Context.writeFile(data: String, fileName: String): File { - val file = File(scriptsDir, fileName) +fun Context.writeFile(data: String, fileName: String, fileDir: File = scriptsDir): File { + val file = File(fileDir, fileName) FileOutputStream(file).use { fos -> fos.write(data.toByteArray()) } return file } -fun Context.readFile(fileName: String): String { - val file = File(scriptsDir, fileName) +fun Context.readFile(fileName: String, fileDir: File = scriptsDir): String { + val file = File(fileDir, fileName) file.bufferedReader().use { reader -> return reader.readText() diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/icon/BugReport.kt b/app/src/main/java/com/paulcoding/hviewer/ui/icon/BugReport.kt new file mode 100644 index 0000000..d04a86d --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/icon/BugReport.kt @@ -0,0 +1,104 @@ +package com.paulcoding.hviewer.ui.icon + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +public val BugReport: ImageVector + get() { + if (_Bug_report != null) { + return _Bug_report!! + } + _Bug_report = ImageVector.Builder( + name = "Bug_report", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1.0f, + stroke = null, + strokeAlpha = 1.0f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(480f, 760f) + quadToRelative(66f, 0f, 113f, -47f) + reflectiveQuadToRelative(47f, -113f) + verticalLineToRelative(-160f) + quadToRelative(0f, -66f, -47f, -113f) + reflectiveQuadToRelative(-113f, -47f) + reflectiveQuadToRelative(-113f, 47f) + reflectiveQuadToRelative(-47f, 113f) + verticalLineToRelative(160f) + quadToRelative(0f, 66f, 47f, 113f) + reflectiveQuadToRelative(113f, 47f) + moveToRelative(-80f, -120f) + horizontalLineToRelative(160f) + verticalLineToRelative(-80f) + horizontalLineTo(400f) + close() + moveToRelative(0f, -160f) + horizontalLineToRelative(160f) + verticalLineToRelative(-80f) + horizontalLineTo(400f) + close() + moveToRelative(80f, 360f) + quadToRelative(-65f, 0f, -120.5f, -32f) + reflectiveQuadTo(272f, 720f) + horizontalLineTo(160f) + verticalLineToRelative(-80f) + horizontalLineToRelative(84f) + quadToRelative(-3f, -20f, -3.5f, -40f) + reflectiveQuadToRelative(-0.5f, -40f) + horizontalLineToRelative(-80f) + verticalLineToRelative(-80f) + horizontalLineToRelative(80f) + quadToRelative(0f, -20f, 0.5f, -40f) + reflectiveQuadToRelative(3.5f, -40f) + horizontalLineToRelative(-84f) + verticalLineToRelative(-80f) + horizontalLineToRelative(112f) + quadToRelative(14f, -23f, 31.5f, -43f) + reflectiveQuadToRelative(40.5f, -35f) + lineToRelative(-64f, -66f) + lineToRelative(56f, -56f) + lineToRelative(86f, 86f) + quadToRelative(28f, -9f, 57f, -9f) + reflectiveQuadToRelative(57f, 9f) + lineToRelative(88f, -86f) + lineToRelative(56f, 56f) + lineToRelative(-66f, 66f) + quadToRelative(23f, 15f, 41.5f, 34.5f) + reflectiveQuadTo(688f, 320f) + horizontalLineToRelative(112f) + verticalLineToRelative(80f) + horizontalLineToRelative(-84f) + quadToRelative(3f, 20f, 3.5f, 40f) + reflectiveQuadToRelative(0.5f, 40f) + horizontalLineToRelative(80f) + verticalLineToRelative(80f) + horizontalLineToRelative(-80f) + quadToRelative(0f, 20f, -0.5f, 40f) + reflectiveQuadToRelative(-3.5f, 40f) + horizontalLineToRelative(84f) + verticalLineToRelative(80f) + horizontalLineTo(688f) + quadToRelative(-32f, 56f, -87.5f, 88f) + reflectiveQuadTo(480f, 840f) + } + }.build() + return _Bug_report!! + } + +private var _Bug_report: ImageVector? = null diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt index 4796227..5382c09 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt @@ -65,7 +65,10 @@ fun AppEntry() { navController.navigate(Route.FAVORITE) }, navToListScript = { - navController.navigate(Route.LIST_SCRIPT) + navController.navigate(Route.LIST_SCRIPT + "/script") + }, + navToListCrashLog = { + navController.navigate(Route.LIST_SCRIPT + "/crash_log") }, goBack = { navController.popBackStack() }) } @@ -114,18 +117,24 @@ fun AppEntry() { goBack = { navController.popBackStack() } ) } - animatedComposable(Route.LIST_SCRIPT) { + animatedComposable(Route.LIST_SCRIPT + "/{type}") { backStackEntry -> + val type = backStackEntry.arguments?.getString("type")!! + ListScriptPage( appViewModel = appViewModel, + type = type, goBack = { navController.popBackStack() }, navToEditor = { - navController.navigate(Route.EDITOR + "/$it") + navController.navigate(Route.EDITOR + "/$type" + "/$it") }) } - animatedComposable(Route.EDITOR + "/{scriptFile}") { backStackEntry -> + animatedComposable(Route.EDITOR + "/{type}" + "/{scriptFile}") { backStackEntry -> + val type = backStackEntry.arguments?.getString("type")!! val scriptFile = backStackEntry.arguments?.getString("scriptFile")!! + EditorPage( appViewModel = appViewModel, + type = type, scriptFile = scriptFile, goBack = { navController.popBackStack() }) } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt index 2610eb6..2d13c6d 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.paulcoding.hviewer.MainApp.Companion.appContext import com.paulcoding.hviewer.database.DatabaseProvider +import com.paulcoding.hviewer.helper.crashLogDir import com.paulcoding.hviewer.helper.scriptsDir import com.paulcoding.hviewer.model.PostItem import com.paulcoding.hviewer.model.SiteConfig @@ -14,9 +15,11 @@ import kotlinx.coroutines.launch import java.io.File class AppViewModel : ViewModel() { - private val scriptsDir = appContext.scriptsDir val listScriptFiles: List - get() = scriptsDir.listFiles()?.toList() ?: listOf() + get() = appContext.scriptsDir.listFiles()?.toList() ?: listOf() + + val listCrashLogFiles: List + get() = appContext.crashLogDir.listFiles()?.toList() ?: listOf() private var _stateFlow = MutableStateFlow(UiState()) val stateFlow = _stateFlow.asStateFlow() diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/CodeEditor.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/CodeEditor.kt index 7ee1ab9..87d0bdd 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/CodeEditor.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/CodeEditor.kt @@ -14,9 +14,11 @@ import io.github.rosemoe.sora.widget.CodeEditor data class CodeEditorState( var editor: CodeEditor? = null, - val initialContent: Content = Content() + val initialContent: Content = Content(), + val isEditable: Boolean = true, ) { var content by mutableStateOf(initialContent) + var editable by mutableStateOf(isEditable) } private fun setCodeEditorFactory( @@ -26,6 +28,7 @@ private fun setCodeEditorFactory( val editor = CodeEditor(context) editor.apply { setText(state.content) + editor.editable = state.editable } state.editor = editor return editor @@ -54,9 +57,11 @@ fun CodeEditor( @Composable fun rememberCodeEditorState( - initialContent: Content = Content() + initialContent: Content = Content(), + editable: Boolean = true, ) = remember { CodeEditorState( - initialContent = initialContent + initialContent = initialContent, + isEditable = editable, ) } \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/EditorPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/EditorPage.kt index d849542..66066a4 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/EditorPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/EditorPage.kt @@ -10,8 +10,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import com.paulcoding.hviewer.helper.crashLogDir import com.paulcoding.hviewer.helper.makeToast import com.paulcoding.hviewer.helper.readFile +import com.paulcoding.hviewer.helper.scriptsDir import com.paulcoding.hviewer.helper.writeFile import com.paulcoding.hviewer.network.Github import com.paulcoding.hviewer.ui.component.HBackIcon @@ -22,10 +24,21 @@ import io.github.rosemoe.sora.text.Content @OptIn(ExperimentalMaterial3Api::class) @Composable -fun EditorPage(appViewModel: AppViewModel, goBack: () -> Boolean, scriptFile: String) { +fun EditorPage( + appViewModel: AppViewModel, + type: String, + goBack: () -> Boolean, + scriptFile: String +) { val context = LocalContext.current - val script = context.readFile(scriptFile) - val state = rememberCodeEditorState(initialContent = Content(script)) + val dir = when (type) { + "script" -> context.scriptsDir + "crash_log" -> context.crashLogDir + else -> return makeToast("Unknown type $type") + } + val editable = type == "script" + val script = context.readFile(scriptFile, dir) + val state = rememberCodeEditorState(initialContent = Content(script), editable = editable) val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current Scaffold( diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/ListScriptPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/ListScriptPage.kt index ff90099..c86f89d 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/ListScriptPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/editor/ListScriptPage.kt @@ -17,7 +17,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.paulcoding.hviewer.helper.makeToast import com.paulcoding.hviewer.ui.component.HBackIcon +import com.paulcoding.hviewer.ui.icon.BugReport import com.paulcoding.hviewer.ui.icon.Javascript import com.paulcoding.hviewer.ui.page.AppViewModel @@ -25,14 +27,33 @@ import com.paulcoding.hviewer.ui.page.AppViewModel @Composable fun ListScriptPage( appViewModel: AppViewModel, + type: String, goBack: () -> Unit, - navToEditor: (String) -> Unit + navToEditor: (String) -> Unit, ) { + val listFiles = when (type) { + "script" -> appViewModel.listScriptFiles + "crash_log" -> appViewModel.listCrashLogFiles + else -> emptyList() + } + + val title = when (type) { + "script" -> "List Script" + "crash_log" -> "List Crash Log" + else -> "" + } + + val fileIcon = when (type) { + "script" -> Javascript + "crash_log" -> BugReport + else -> return makeToast("Unknown type $type") + } + Scaffold( modifier = Modifier.fillMaxSize(), topBar = { - TopAppBar(title = { Text(text = "List Script") }, + TopAppBar(title = { Text(text = title) }, navigationIcon = { HBackIcon { goBack() @@ -45,7 +66,7 @@ fun ListScriptPage( .padding(paddings) .fillMaxSize() ) { - items(appViewModel.listScriptFiles, key = { it.name }) { + items(listFiles, key = { it.name }) { Row( modifier = Modifier .fillMaxWidth() @@ -56,7 +77,7 @@ fun ListScriptPage( verticalAlignment = Alignment.CenterVertically, ) { Icon( - Javascript, + fileIcon, contentDescription = "Javascript", modifier = Modifier.size(32.dp), ) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt index 994414d..80621ff 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt @@ -32,6 +32,7 @@ import com.paulcoding.hviewer.model.SiteConfigs import com.paulcoding.hviewer.ui.component.HEmpty import com.paulcoding.hviewer.ui.component.HFavoriteIcon import com.paulcoding.hviewer.ui.component.HIcon +import com.paulcoding.hviewer.ui.icon.BugReport import com.paulcoding.hviewer.ui.icon.EditIcon import com.paulcoding.hviewer.ui.icon.SettingsIcon import kotlinx.coroutines.delay @@ -45,6 +46,7 @@ fun SitesPage( goBack: () -> Unit, siteConfigs: SiteConfigs, navToSettings: () -> Unit, + navToListCrashLog: () -> Unit, navToListScript: () -> Unit, refresh: () -> Unit, navToFavorite: () -> Unit, @@ -59,10 +61,14 @@ fun SitesPage( Scaffold(topBar = { TopAppBar(title = { Text("Sites") }, actions = { - if (isDevMode) + if (isDevMode) { + HIcon(BugReport) { + navToListCrashLog() + } HIcon(EditIcon) { navToListScript() } + } HFavoriteIcon(isFavorite = false) { navToFavorite() }