Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app/src/main/java/xyz/sevive/arcaeaoffline/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import org.opencv.android.OpenCVLoader
import xyz.sevive.arcaeaoffline.ui.theme.ArcaeaOfflineTheme

class MainActivity : ComponentActivity() {
companion object {
/** Extra carrying the route a fresh session should open on, e.g. an OCR sub-screen route. */
const val EXTRA_INITIAL_ROUTE = "xyz.sevive.arcaeaoffline.extra.INITIAL_ROUTE"
}

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Expand All @@ -26,13 +31,15 @@ class MainActivity : ComponentActivity() {
OpenCVLoader.initLocal()
FileKit.init(this)

val initialRoute = intent?.getStringExtra(EXTRA_INITIAL_ROUTE)

setContent {
ArcaeaOfflineTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
MainScreen()
MainScreen(initialNavigateRoute = initialRoute)
}
}
}
Expand Down
40 changes: 37 additions & 3 deletions app/src/main/java/xyz/sevive/arcaeaoffline/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
Expand All @@ -31,13 +35,43 @@ internal fun mainNavControllerNavigateToRoute(
}

@Composable
fun MainScreen(modifier: Modifier = Modifier) {
fun MainScreen(
modifier: Modifier = Modifier,
initialNavigateRoute: String? = null,
) {
val navController = rememberNavController()

val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val currentRouteRoot = currentRoute?.split("/")

// The intent extra survives activity recreation (rotation, process-death restore, etc.),
// so consuming the initial route to prevent unexpected extra navigations.
// A new jump recreates the activity via CLEAR_TOP, which starts with fresh state.
var initialRouteConsumed by rememberSaveable { mutableStateOf(false) }

LaunchedEffect(initialNavigateRoute) {
val route = initialNavigateRoute ?: return@LaunchedEffect
if (initialRouteConsumed) return@LaunchedEffect
val tabRoute = route.substringBefore('/')
if (MainScreen.entries.none { it.route == tabRoute }) return@LaunchedEffect

// Navigate with a fresh tab stack:
// 1. Omit restoreState: prevents restoring a stale nested/detail destination instead of this fresh entry.
// 2. popUpTo(startRoute, inclusive = true): replaces the initial root so pressing Back exits directly
// to the caller activity, while saving the start destination's state for subsequent tab switches.
val startRoute = navController.graph.startDestinationRoute ?: return@LaunchedEffect
navController.navigate(tabRoute) {
popUpTo(startRoute) {
saveState = true
inclusive = true
}
launchSingleTop = true
}

initialRouteConsumed = true
}

val navigationSuiteColors =
NavigationSuiteDefaults.colors(
navigationBarContainerColor = MaterialTheme.colorScheme.inverseOnSurface,
Expand All @@ -51,14 +85,14 @@ fun MainScreen(modifier: Modifier = Modifier) {
icon = { Icon(it.icon(), contentDescription = null) },
label = { Text(stringResource(it.title)) },
alwaysShowLabel = true,
selected = if (currentRouteRoot != null) currentRouteRoot[0] == it.route else false,
selected = currentRouteRoot != null && currentRouteRoot[0] == it.route,
onClick = { mainNavControllerNavigateToRoute(navController, it.route) },
)
}
},
modifier = modifier,
navigationSuiteColors = navigationSuiteColors,
) {
MainNavigationGraph(navController)
MainNavigationGraph(navController, initialNavigateRoute)
}
}
53 changes: 10 additions & 43 deletions app/src/main/java/xyz/sevive/arcaeaoffline/OcrFromShareActivity.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
package xyz.sevive.arcaeaoffline

import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.core.content.IntentCompat
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.PlatformFile
import io.github.vinceglb.filekit.dialogs.init
import io.github.vinceglb.filekit.readBytes
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.opencv.android.OpenCVLoader
import xyz.sevive.arcaeaoffline.helpers.activity.getSourcePackageName
Expand All @@ -26,7 +19,6 @@ import xyz.sevive.arcaeaoffline.ui.theme.ArcaeaOfflineTheme
class OcrFromShareActivity : ComponentActivity() {
private val viewModel by viewModel<OcrFromShareViewModel>()

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Expand All @@ -36,13 +28,10 @@ class OcrFromShareActivity : ComponentActivity() {

setTitle(R.string.title_activity_ocr_from_share)

viewModel.setShareSourceApp(this.getSourcePackageName(), packageManager)
viewModel.setShareSourceApp(getSourcePackageName(), packageManager)
setContent {
val windowSizeClass = calculateWindowSizeClass(this)

ArcaeaOfflineTheme {
OcrFromShareScreen(
windowSizeClass,
onReturnToShareApp = { finishAffinity() },
onStayInApp = {
val intent = Intent(this@OcrFromShareActivity, MainActivity::class.java)
Expand All @@ -59,7 +48,15 @@ class OcrFromShareActivity : ComponentActivity() {
when (intent?.action) {
Intent.ACTION_SEND -> {
if (intent.type?.startsWith("image/") == true) {
handleSingleImageOcr()
// fix 'getParcelableExtra(String!): T?' is deprecated. Deprecated in Java
// https://stackoverflow.com/a/75824124/16484891
// CC BY-SA 4.0
IntentCompat
.getParcelableExtra(
intent,
Intent.EXTRA_STREAM,
Uri::class.java,
)?.let(viewModel::onImageShared)
}
}

Expand All @@ -73,34 +70,4 @@ class OcrFromShareActivity : ComponentActivity() {
else -> {}
}
}

private fun handleSingleImageOcr() {
// fix 'getParcelableExtra(String!): T?' is deprecated. Deprecated in Java
// https://stackoverflow.com/a/75824124/16484891
// CC BY-SA 4.0
val uri =
IntentCompat.getParcelableExtra(
intent,
Intent.EXTRA_STREAM,
Uri::class.java,
) ?: return

// get an input stream from the Uri
// TODO: use input stream
val imageBytes =
runCatching {
runBlocking { PlatformFile(uri).readBytes() }
}.getOrElse {
viewModel.setException(Exception("Error reading image", it))
return
}

val imgBitmap = BitmapFactory.decodeStream(imageBytes.inputStream())
viewModel.setBitmap(imgBitmap)
runBlocking {
launch {
viewModel.startOcr(uri, this@OcrFromShareActivity)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,10 @@ data class CrnnModelStatusDetail(
}
}
}

/**
* Whether the image hashes database needs setup: absent, failed to load, or holding no hashes.
*
* Null counts mean the detail has not loaded yet, not an empty database.
*/
fun ImageHashesDatabaseStatusDetail.needsSetup(): Boolean = absence || exception != null || jacketCount == 0 || partnerIconCount == 0

This file was deleted.

Loading
Loading