diff --git a/apps/vbox/client/androidApp/build.gradle.kts b/apps/vbox/client/androidApp/build.gradle.kts index a3f45e7..1cb9b0e 100644 --- a/apps/vbox/client/androidApp/build.gradle.kts +++ b/apps/vbox/client/androidApp/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = namespace minSdk = libs.versions.proj.min.get().toInt() targetSdk = libs.versions.proj.target.get().toInt() - versionCode = 48 - versionName = "1.24.4" + versionCode = 49 + versionName = "1.25.0" } } diff --git a/apps/vbox/client/androidApp/src/main/AndroidManifest.xml b/apps/vbox/client/androidApp/src/main/AndroidManifest.xml index 7d62610..14ffe88 100644 --- a/apps/vbox/client/androidApp/src/main/AndroidManifest.xml +++ b/apps/vbox/client/androidApp/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ android:exported="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:launchMode="singleInstance" android:supportsPictureInPicture="true"> @@ -56,10 +57,6 @@ android:host="box.shadowsparky.ru" android:scheme="https" /> - - - - - val routeFromIntent = obtainRouteOrNull(intent?.data) - val route = if (routeFromIntent == null) { - listOf(Route.default) - } else { - listOf(Route.default, routeFromIntent) - } - koin.get().create(componentContext, route) + koin.get().create(componentContext, listOf(Route.default)) } if (openSavedIfNeeded(intent) || openUrlIfNeeded(intent.data)) { intent = Intent(intent) .setData(null) .setAction(null) } + log("onCreate $intent") val isPiggyActivity = componentName?.className == PigReceiver.PIGGY_CLS setContent { if (isPiggyActivity) { @@ -54,10 +50,9 @@ class MainActivity : ComponentActivity() { edgeToEdge(isSystemInDarkTheme()) } lifecycleScope.launch { - root.onFinish.collect { - finishAndRemoveTask() - } + root.onFinish.collect { finishAndRemoveTask() } } + if (savedInstanceState == null) handleIntent(intent) } private fun edgeToEdge(isDarkTheme: Boolean) { @@ -84,15 +79,42 @@ class MainActivity : ComponentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - if (!openSavedIfNeeded(intent)) { - val uri = intent.data ?: return - val route = obtainRouteOrNull(uri) - if (route != null) { - root.nav(route, true) - } else { - openUrlIfNeeded(uri) - } + log("onNewIntent $intent") + setIntent(intent) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + intent ?: return + if (openSavedIfNeeded(intent)) { + clearIntent(intent) + return } + val uri = intent.data ?: return + val route = obtainRouteOrNull(uri) + if (route != null) { + val currentRoute = root.childStack.value.active.configuration + if (currentRoute != route) { + root.nav(route, true) + } + } else { + openUrlIfNeeded(uri) + } + clearIntent(intent) + } + + private fun clearIntent(intent: Intent) { + intent.data = null + intent.action = null + } + + override fun onDestroy() { + super.onDestroy() + log("onDestroy") + } + + private fun log(msg: String) { + Log.d("MainActivityDebug", msg) } private fun openUrlIfNeeded(uri: Uri?): Boolean { diff --git a/apps/vbox/client/androidApp/src/main/res/xml-v25/shortcuts.xml b/apps/vbox/client/androidApp/src/main/res/xml-v25/shortcuts.xml deleted file mode 100644 index 6e9707b..0000000 --- a/apps/vbox/client/androidApp/src/main/res/xml-v25/shortcuts.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/apps/vbox/client/androidApp/src/main/res/xml/shortcuts.xml b/apps/vbox/client/androidApp/src/main/res/xml/shortcuts.xml deleted file mode 100644 index 167bf66..0000000 --- a/apps/vbox/client/androidApp/src/main/res/xml/shortcuts.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/UrlRoutes.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/UrlRoutes.kt index 60a3d74..f06a703 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/UrlRoutes.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/UrlRoutes.kt @@ -102,6 +102,7 @@ object UrlRoutes { } ), + // "/chat rule( match = { it is Route.Chat }, build = { @@ -110,7 +111,13 @@ object UrlRoutes { emptyMap() ) }, - parse = { _, _ -> Route.Chat() } + parse = { segs, _ -> + if (segs.size == 1 && segs[0] == Seg.CHAT) { + Route.Chat() + } else { + null + } + } ), // "/search" diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/VBoxRootComponent.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/VBoxRootComponent.kt index 61ec15e..54fd8c9 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/VBoxRootComponent.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/VBoxRootComponent.kt @@ -10,7 +10,6 @@ import com.arkivanov.essenty.lifecycle.coroutines.repeatOnLifecycle import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.serialization.KSerializer import org.koin.core.Koin @@ -27,26 +26,29 @@ import ru.shadowsparky.vbox.shared.presentation.nav.Route class VBoxRootComponent( componentContext: ComponentContext, tokenStorage: TokenStorage, - override val initStack: List, + private val initRoute: Route, private val healthCheck: HeathCheck, val imageLoaderProvider: ImageLoaderProvider, updateComponentFactory: UpdateComponentFactory, private val remoteEventListener: RemoteEventListener, + override val initStack: List = listOf(initRoute), override val serializer: KSerializer = Route.serializer(), override val koin: Koin = ru.shadowsparky.koin ) : RootComponent(componentContext), WebNavigationOwner { val updateComponent = updateComponentFactory.create(childContext("UpdateComponentRoot")) private val scope = createComponentScope() - val isLoggedIn = tokenStorage.token.map { - if (it != null) { - runCatching { healthCheck.check() }.getOrNull() ?: return@map false - true - } else { - false - } - } init { + scope.launch { + repeatOnLifecycle { + tokenStorage.token.collect { + if (it != null) { + val invalid = runCatching { healthCheck.check() }.getOrNull() == null + if (invalid) { navReplace(initRoute) } + } + } // i love you + } + } scope.launch { repeatOnLifecycle { tokenStorage.token.flatMapLatest { @@ -103,7 +105,7 @@ class RootFactory( return VBoxRootComponent( context, tokenStorage, - listOf(Route.Loading(next = initStack.toSet())), + Route.Loading(next = initStack.toSet()), heathCheck, imageLoaderProvider, updateComponentFactory, diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt index 5697e42..5e90e0b 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/OverviewScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -292,12 +293,15 @@ private fun OverviewDescription( style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(vertical = 8.dp) ) - Text( - text = details.desc.trim(), - style = MaterialTheme.typography.bodyLarge, - maxLines = if (showDetails.value) Int.MAX_VALUE else 3, - overflow = TextOverflow.Ellipsis - ) + + SelectionContainer { + Text( + text = details.desc.trim(), + style = MaterialTheme.typography.bodyLarge, + maxLines = if (showDetails.value) Int.MAX_VALUE else 3, + overflow = TextOverflow.Ellipsis + ) + } AnimatedVisibility( visible = !showDetails.value, diff --git a/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt b/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt index c64bdcd..b83622f 100644 --- a/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt +++ b/apps/vbox/client/web/src/wasmJsMain/kotlin/ru/shadowsparky/vbox/Main.kt @@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -86,13 +84,6 @@ fun mainInternal() { r.onFinish.collect { r.navReplace(Route.Loading()) } } - val isLoggedIn by r.isLoggedIn.collectAsState(null) - LaunchedEffect(isLoggedIn) { - if (isLoggedIn == false) { - r.navReplace(Route.Loading()) - } - } - Box(Modifier.fillMaxSize().padding(WindowInsets.safeDrawing.asPaddingValues())) { Nav(r) }