diff --git a/app/src/main/java/eu/kanade/presentation/more/AppMigratingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/AppMigratingScreen.kt new file mode 100644 index 000000000..6d1831d88 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/AppMigratingScreen.kt @@ -0,0 +1,48 @@ +package eu.kanade.presentation.more + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource + +/** + * Shown instead of the app while migrations from the previous version run, so a slow first launch + * after an update doesn't look like the app hanging. + */ +@Composable +fun AppMigratingScreen() { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .safeDrawingPadding() + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + Text( + text = stringResource(MR.strings.app_migrating_title), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(MR.strings.app_migrating_summary), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index b995fa45a..f74d9d043 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -72,6 +72,7 @@ import eu.kanade.presentation.components.AppStateBanners import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor import eu.kanade.presentation.components.IndexingBannerBackgroundColor +import eu.kanade.presentation.more.AppMigratingScreen import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.util.AssistContentScreen @@ -95,6 +96,7 @@ import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim import eu.kanade.tachiyomi.util.system.updaterEnabled import eu.kanade.tachiyomi.util.view.setComposeContent import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop @@ -107,6 +109,7 @@ import mihon.app.di.AppGraph import mihon.app.di.appGraph import mihon.core.metro.metroGraph import mihon.core.migration.Migrator +import mihon.domain.database.repository.DatabaseRepository import mihon.feature.support.SupportUsScreen import mihon.icons.materialsymbols.MaterialSymbols import mihon.icons.materialsymbols.automirroredrounded.OpenInNew @@ -138,6 +141,10 @@ class MainActivity : BaseActivity() { @Inject private lateinit var chapterCache: ChapterCache + @Inject private lateinit var databaseRepository: DatabaseRepository + + private val isAppMigrating = MutableStateFlow(Migrator.isRunning) + @Inject private lateinit var getIncognitoState: GetIncognitoState @Inject private lateinit var extensionManager: ExtensionManager @@ -160,7 +167,10 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) - Migrator.awaitAndRelease() + lifecycleScope.launch { + Migrator.awaitAndRelease() + isAppMigrating.value = false + } // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 if (!isTaskRoot) { @@ -169,6 +179,15 @@ class MainActivity : BaseActivity() { } setComposeContent { + val appMigrating by isAppMigrating.collectAsState() + val databaseMigrating by databaseRepository.isMigrating.collectAsState() + if (appMigrating || databaseMigrating) { + AppMigratingScreen() + // Release the splash screen so the reason for the wait shows + LaunchedEffect(Unit) { ready = true } + return@setComposeContent + } + val context = LocalContext.current var incognito by remember { mutableStateOf(false) } diff --git a/app/src/main/java/mihon/core/migration/Migrator.kt b/app/src/main/java/mihon/core/migration/Migrator.kt index a7c1f4429..9110623bf 100644 --- a/app/src/main/java/mihon/core/migration/Migrator.kt +++ b/app/src/main/java/mihon/core/migration/Migrator.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking object Migrator { @@ -35,7 +34,10 @@ object Migrator { result = null } - fun awaitAndRelease(): Boolean = runBlocking { - await().also { release() } + val isRunning: Boolean + get() = result?.isCompleted == false + + suspend fun awaitAndRelease(): Boolean { + return await().also { release() } } } diff --git a/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt b/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt new file mode 100644 index 000000000..2dbd4e1e7 --- /dev/null +++ b/data/src/main/java/mihon/data/database/DatabaseRepositoryImpl.kt @@ -0,0 +1,16 @@ +package mihon.data.database + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import mihon.domain.database.repository.DatabaseRepository + +@Inject +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DatabaseRepositoryImpl : DatabaseRepository { + + override val isMigrating = MutableStateFlow(false) +} diff --git a/data/src/main/java/tachiyomi/data/DatabaseBindings.kt b/data/src/main/java/tachiyomi/data/DatabaseBindings.kt index c1af0f6f3..df38cef17 100644 --- a/data/src/main/java/tachiyomi/data/DatabaseBindings.kt +++ b/data/src/main/java/tachiyomi/data/DatabaseBindings.kt @@ -7,6 +7,7 @@ import androidx.sqlite.SQLiteConnection import androidx.sqlite.SQLiteDriver import androidx.sqlite.driver.bundled.BundledSQLiteDriver import androidx.sqlite.execSQL +import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration @@ -19,6 +20,7 @@ import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Provides import dev.zacsweers.metro.SingleIn +import mihon.data.database.DatabaseRepositoryImpl @ContributesTo(AppScope::class) @BindingContainer @@ -30,7 +32,7 @@ object DatabaseBindings { */ @Provides @SingleIn(AppScope::class) - fun providesSqlDriver(context: Context): SqlDriver { + fun providesSqlDriver(context: Context, databaseRepository: DatabaseRepositoryImpl): SqlDriver { val isLowRam = context.getSystemService()?.isLowRamDevice == true return AndroidxSqliteDriver( connectionFactory = object : AndroidxSqliteConnectionFactory { @@ -48,6 +50,14 @@ object DatabaseBindings { isForeignKeyConstraintsEnabled = true, concurrencyModel = MultipleReadersSingleWriter(isWal = true, walCount = if (isLowRam) 1 else 4), ), + // Runs before the driver compares the schema version, and onOpen once the schema is ready + onConfigure = { + val version = executePragmaQuery("user_version", { QueryResult.Value(it.apply { next() }.getLong(0)) }) + databaseRepository.isMigrating.value = version in 1.. +} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 9116e336f..f3840acc3 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -188,6 +188,8 @@ A folder must be selected Updating from an older version and not sure what to select? Refer to the storage guide for more information. Storage guide + Finishing the update + Getting things ready after the update. This can take a few minutes, so keep the app open. Install apps permission To install source extensions. Notification permission