diff --git a/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt b/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt
deleted file mode 100644
index 0a7516c6..00000000
--- a/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2024-2025 Christian Nagel and contributors
- *
- * This file is part of ScanBridge.
- *
- * ScanBridge is free software: you can redistribute it and/or modify it under the terms of
- * the GNU General Public License as published by the Free Software Foundation, either
- * version 3 of the License, or (at your option) any later version.
- *
- * ScanBridge is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with eSCLKt.
- * If not, see .
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-package io.github.chrisimx.scanbridge
-
-import android.app.Application
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.testTag
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.toRoute
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
-import io.github.chrisimx.scanbridge.proto.scanningResponseTimeoutOrNull
-import io.github.chrisimx.scanbridge.uicomponents.FullScreenError
-import io.ktor.http.Url
-import kotlin.uuid.Uuid
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import timber.log.Timber
-
-@Serializable
-sealed interface BaseRoute
-
-@Serializable
-@SerialName("StartUpScreenRoute")
-object StartUpScreenRoute : BaseRoute
-
-@Serializable
-@SerialName("ScannerRoute")
-data class ScannerRoute(val scannerName: String, val scannerURL: String, val sessionID: String) : BaseRoute
-
-@Serializable
-@SerialName("CropImageRoute")
-data class CropImageRoute(val scanId: String, val returnRoute: String) : BaseRoute
-
-@Serializable
-@SerialName("ErrorRoute")
-data class ErrorRoute(val error: String) : BaseRoute
-
-fun NavBackStackEntry.toTypedRoute(): BaseRoute? {
- Timber.d("Route changed to: ${destination.route}")
- return when (destination.route) {
- "StartUpScreenRoute" -> StartUpScreenRoute
-
- "CropImageRoute/{scanId}/{pageIdx}/{returnRoute}" -> {
- val scanId = arguments?.getString("scanId") ?: return null
- val returnRouteString = arguments?.getString("returnRoute") ?: return null
- CropImageRoute(scanId, returnRouteString)
- }
-
- "ScannerRoute/{scannerName}/{scannerURL}/{sessionID}" -> {
- val scannerName = arguments?.getString("scannerName") ?: return null
- val scannerURL = arguments?.getString("scannerURL") ?: return null
- val sessionID = arguments?.getString("sessionID") ?: return null
- ScannerRoute(scannerName, scannerURL, sessionID)
- }
-
- "ErrorRoute/{error}" -> {
- val error = arguments?.getString("error") ?: return null
- ErrorRoute(error)
- }
-
- else -> null
- }
-}
-
-@Composable
-fun ScanBridgeNavHost(navController: NavHostController, startDestination: Any) {
- val context = LocalContext.current
- val appSettings = context.appSettingsStore.data
-
- NavHost(
- modifier = Modifier.testTag("root_node"),
- navController = navController,
- startDestination = startDestination
- ) {
- composable { backStackEntry ->
- val errorRoute: ErrorRoute = backStackEntry.toRoute()
- val errorMessage = errorRoute.error
-
- FullScreenError(R.drawable.outline_error_24, errorMessage, true)
- }
- composable {
- StartupScreen(navController)
- }
- composable { backStackEntry ->
- val scannerRoute: CropImageRoute = backStackEntry.toRoute()
- val returnRoute = try {
- Json.decodeFromString(scannerRoute.returnRoute)
- } catch (e: Exception) {
- Timber.e(e, "Failed to decode returnRoute: ${scannerRoute.returnRoute}")
- navController.navigate(StartUpScreenRoute)
- return@composable
- }
- CropScreen(Uuid.parse(scannerRoute.scanId), returnRoute, navController)
- }
- composable { backStackEntry ->
- val scannerRoute: ScannerRoute = backStackEntry.toRoute()
- val appSettingsCurrent = runBlocking { appSettings.first() }
- val debug = appSettingsCurrent.writeDebug
- val certValidationDisabled = appSettingsCurrent.disableCertChecks
- val timeout = appSettingsCurrent.scanningResponseTimeoutOrNull?.value?.toUInt() ?: 25u
- Timber.tag("ScanBridgeNavHost")
- .d(
- "Navigating to scanner ${scannerRoute.scannerName} at ${scannerRoute.scannerURL}. Timeout is $timeout seconds, Debug is $debug. Disabling of cert checks is $certValidationDisabled. Session id is ${scannerRoute.sessionID}"
- )
- ScanningScreen(
- scannerRoute.scannerName,
- Url(scannerRoute.scannerURL),
- navController,
- timeout,
- debug,
- certValidationDisabled,
- Uuid.parse(scannerRoute.sessionID),
- context.applicationContext as Application
- )
- }
- }
-}
diff --git a/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt b/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt
index e764baa4..7120af86 100644
--- a/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt
+++ b/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/ScanBridgeTest.kt
@@ -14,16 +14,12 @@ import androidx.test.espresso.Espresso.pressBack
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import io.github.chrisimx.esclmockserver.EsclMockServer
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
-import io.github.chrisimx.scanbridge.datastore.lastRouteStore
-import io.github.chrisimx.scanbridge.datastore.shownMessagesStore
-import io.github.chrisimx.scanbridge.datastore.updateSettings
-import io.github.chrisimx.scanbridge.proto.copy
-import kotlinx.coroutines.runBlocking
+import io.github.chrisimx.scanbridge.db.ScanBridgeDb
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.koin.mp.KoinPlatform.getKoin
class ScanBridgeTest {
@get:Rule
@@ -37,22 +33,8 @@ class ScanBridgeTest {
@Before
@After
fun cleanupForTest() {
- runBlocking {
- composeTestRule.activity.shownMessagesStore.updateData {
- it.copy {
- thankPlayOne = true
- }
- }
- composeTestRule.activity.lastRouteStore.updateData {
- it.copy {
- clearLastRoute()
- }
- }
- composeTestRule.activity.appSettingsStore.updateSettings {
- clearLastUsedScanSettings()
- autoCleanup = true
- }
- }
+ val scanBridgeDb: ScanBridgeDb = getKoin().get()
+ scanBridgeDb.clearAllTables()
}
@Test
diff --git a/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt b/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt
index 8242c789..9d4bcc24 100644
--- a/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt
+++ b/androidApp/src/androidTest/java/io/github/chrisimx/scanbridge/screenshot/ScanBridgeScreenshotTest.kt
@@ -14,15 +14,11 @@ import androidx.test.rule.GrantPermissionRule
import io.github.chrisimx.esclmockserver.EsclMockServer
import io.github.chrisimx.scanbridge.BuildConfig
import io.github.chrisimx.scanbridge.MainActivity
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
-import io.github.chrisimx.scanbridge.datastore.lastRouteStore
-import io.github.chrisimx.scanbridge.datastore.shownMessagesStore
-import io.github.chrisimx.scanbridge.datastore.updateSettings
-import io.github.chrisimx.scanbridge.proto.copy
-import kotlinx.coroutines.runBlocking
+import io.github.chrisimx.scanbridge.db.ScanBridgeDb
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.koin.mp.KoinPlatform.getKoin
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
@@ -56,22 +52,8 @@ class ScanBridgeScreenshotTest {
@Before
fun cleanupForTest() {
- runBlocking {
- composeTestRule.activity.shownMessagesStore.updateData {
- it.copy {
- thankPlayOne = true
- }
- }
- composeTestRule.activity.lastRouteStore.updateData {
- it.copy {
- clearLastRoute()
- }
- }
- composeTestRule.activity.appSettingsStore.updateSettings {
- clearLastUsedScanSettings()
- autoCleanup = true
- }
- }
+ val scanBridgeDb: ScanBridgeDb = getKoin().get()
+ scanBridgeDb.clearAllTables()
}
@Test
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/AppSettingsScreen.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/AppSettingsScreen.kt
index 7ae82423..4bc14d22 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/AppSettingsScreen.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/AppSettingsScreen.kt
@@ -51,22 +51,21 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
-import androidx.core.net.toUri
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsViewModel
+import io.github.chrisimx.scanbridge.buildinfo.ScanBridgeEdition
import io.github.chrisimx.scanbridge.datastore.*
-import io.github.chrisimx.scanbridge.proto.ScanBridgeSettings
-import io.github.chrisimx.scanbridge.proto.rememberScanSettingsOrNull
import io.github.chrisimx.scanbridge.services.DebugLogService
import io.github.chrisimx.scanbridge.uicomponents.TitledCard
import io.github.chrisimx.scanbridge.uicomponents.dialog.SimpleTextDialog
@@ -75,8 +74,6 @@ import io.github.chrisimx.scanbridge.uicomponents.settings.MoreInformationButton
import io.github.chrisimx.scanbridge.uicomponents.settings.UIntSetting
import io.github.chrisimx.scanbridge.uicomponents.settings.VersionComposable
import java.io.File
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@Composable
@@ -159,7 +156,6 @@ fun DebugOptions(debugLog: Boolean, onInformationRequested: (Int) -> Unit, setWr
},
style = MaterialTheme.typography.bodyMedium
)
- val context = LocalContext.current
Box(
modifier = Modifier
.constrainAs(informationButton) {
@@ -189,19 +185,23 @@ fun DebugOptions(debugLog: Boolean, onInformationRequested: (Int) -> Unit, setWr
@ExperimentalMaterial3Api
@Composable
fun AppSettingsScreen(innerPadding: PaddingValues) {
- val context = LocalContext.current
+ val vm = koinInject()
- val appSettingsStore = context.appSettingsStore
- val appSettings by appSettingsStore.data.collectAsState(
- ScanBridgeSettings.getDefaultInstance()
- )
var information: Int? by remember {
mutableStateOf(null)
}
val setInformationRequested = { it: Int -> information = it }
val scrollState = rememberScrollState()
- val coroutineScope = rememberCoroutineScope()
+
+ val disableCertChecks by vm.disableCertChecks.collectAsState()
+ val rememberScanSettings by vm.rememberScanSettings.collectAsState()
+ val writeDebugLogs by vm.writeDebugLogs.collectAsState()
+
+ val defaultPdfExportChunkSize = vm.getDefaultPdfExportChunkSize()
+ val defaultScanningResponseTimeout = vm.getDefaultScanningResponseTimeout()
+
+ val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
@@ -210,9 +210,15 @@ fun AppSettingsScreen(innerPadding: PaddingValues) {
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally
) {
- VersionComposable()
+ VersionComposable(
+ vm.versionName,
+ vm.versionCode,
+ vm.gitCommitHash,
+ vm.edition,
+ vm.isDebugBuild
+ )
- val isFdroidVariant = BuildConfig.FLAVOR == "fdroid"
+ val isFdroidVariant = vm.edition == ScanBridgeEdition.FDROID
val padding = if (isFdroidVariant) 16.dp else 10.dp
FlowRow(
modifier = Modifier.padding(vertical = padding),
@@ -222,8 +228,7 @@ fun AppSettingsScreen(innerPadding: PaddingValues) {
if (isFdroidVariant) {
Button(
onClick = {
- val intent = Intent(Intent.ACTION_VIEW, "https://github.com/sponsors/Chrisimx".toUri())
- context.startActivity(intent)
+ uriHandler.openUri("https://github.com/sponsors/Chrisimx")
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF6F61),
@@ -233,14 +238,13 @@ fun AppSettingsScreen(innerPadding: PaddingValues) {
Icon(
modifier = Modifier.padding(end = 8.dp),
painter = painterResource(R.drawable.favorite_24px),
- contentDescription = stringResource(R.string.donate)
+ contentDescription = null
)
Text(stringResource(R.string.donate))
}
}
OutlinedIconButton(onClick = {
- val intent = Intent(Intent.ACTION_VIEW, "https://github.com/Chrisimx/ScanBridge".toUri())
- context.startActivity(intent)
+ uriHandler.openUri("https://github.com/Chrisimx/ScanBridge")
}) {
Icon(painterResource(R.drawable.github_mark), contentDescription = stringResource(R.string.source_code))
}
@@ -254,41 +258,37 @@ fun AppSettingsScreen(innerPadding: PaddingValues) {
) {
DisableCertChecksSetting(
setInformationRequested,
- appSettings.disableCertChecks,
- { coroutineScope.launch { appSettingsStore.setDisableCertCheck(it) } }
+ disableCertChecks,
+ vm::setDisableCertValidation
)
CheckboxSetting(
stringResource(R.string.remember_scan_settings),
R.string.remember_scan_settings_desc,
- appSettings.rememberScanSettingsOrNull?.value ?: true,
- { coroutineScope.launch { appSettingsStore.setRememberScanSettings(it) } }
+ rememberScanSettings,
+ vm::setRememberScanSettings
) {
information = it
}
// Timeout setting
UIntSetting(
- {
- appSettingsStore.data.firstOrNull()?.scanningResponseTimeout?.value?.toUInt() ?: 25u
- },
- 25u,
+ { vm.getScanningResponseTimeout() },
+ defaultScanningResponseTimeout,
stringResource(R.string.timeout),
R.string.timeout_info,
setInformationRequested,
- { coroutineScope.launch { appSettingsStore.setTimeoutSetting(it) } }
+ vm::setScanningResponseTimeout
)
// PDF chunk size setting
UIntSetting(
- {
- appSettingsStore.data.firstOrNull()?.chunkSizePdfExport?.value?.toUInt() ?: 50u
- },
- 50u,
+ { vm.getPdfExportChunkSize() },
+ defaultPdfExportChunkSize,
stringResource(R.string.pdf_export_max_pages_per_pdf),
R.string.pdf_export_setting_info,
setInformationRequested,
- { coroutineScope.launch { appSettingsStore.setChunkSize(it) } },
+ vm::setPdfExportChunkSize,
min = 1u,
max = UInt.MAX_VALUE
)
@@ -298,9 +298,9 @@ fun AppSettingsScreen(innerPadding: PaddingValues) {
title = stringResource(R.string.advanced)
) {
DebugOptions(
- appSettings.writeDebug,
+ writeDebugLogs,
setInformationRequested,
- { coroutineScope.launch { appSettingsStore.setWriteDebugLog(it) } }
+ vm::setWriteDebugLogs
)
}
}
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt
index 4ec1e1e6..05d8b5fd 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt
@@ -6,16 +6,16 @@ import AndroidMulticastLockHandler
import AndroidScanBridgeDbBuilderFactory
import android.app.Application
import android.content.Context
-import androidx.datastore.core.DataStore
import coil3.ImageLoader
import coil3.network.ktor3.KtorNetworkFetcherFactory
import io.github.chrisimx.scanbridge.adapters.KoinBasedScanningProtocolManager
import io.github.chrisimx.scanbridge.adapters.RoomBackedCustomScannerRepository
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsRepository
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsViewModel
+import io.github.chrisimx.scanbridge.appsettings.RoomAppSettingsRepository
import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider
import io.github.chrisimx.scanbridge.data.ui.ScanSettingsComposableStateHolder
import io.github.chrisimx.scanbridge.data.ui.ScanningScreenViewModel
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
-import io.github.chrisimx.scanbridge.datastore.shownMessagesStore
import io.github.chrisimx.scanbridge.db.DefaultScanBridgeDbFactory
import io.github.chrisimx.scanbridge.db.ScanBridgeDb
import io.github.chrisimx.scanbridge.db.ScanBridgeDbBuilderFactory
@@ -24,6 +24,7 @@ import io.github.chrisimx.scanbridge.db.migrations.ROOM_MIGRATIONS
import io.github.chrisimx.scanbridge.infrastructure.KmLogScanBridgeLoggerFactory
import io.github.chrisimx.scanbridge.migrations.MigrationExecutor
import io.github.chrisimx.scanbridge.migrations.RoomBackedMigrationExecutor
+import io.github.chrisimx.scanbridge.migrations.ds2room.DATASTORE_TO_ROOM_MIGRATION_DATA_SOURCES
import io.github.chrisimx.scanbridge.migrations.migrationsModule
import io.github.chrisimx.scanbridge.model.HttpClientConfig
import io.github.chrisimx.scanbridge.ports.CustomScannerRepository
@@ -34,11 +35,12 @@ import io.github.chrisimx.scanbridge.ports.MdnsDiscoverService
import io.github.chrisimx.scanbridge.ports.ScanBridgeLoggerFactory
import io.github.chrisimx.scanbridge.ports.ScanningProtocolManager
import io.github.chrisimx.scanbridge.ports.multicast.MulticastLockHandler
-import io.github.chrisimx.scanbridge.proto.ScanBridgeSettings
import io.github.chrisimx.scanbridge.proto.ShownMessages
import io.github.chrisimx.scanbridge.repositories.DatastoreLastRouteRepository
import io.github.chrisimx.scanbridge.repositories.DatastoreShownMessagesRepository
import io.github.chrisimx.scanbridge.repositories.RoomLastRouteRepository
+import io.github.chrisimx.scanbridge.savelastusedscansettings.LastUsedScanSettingsRepository
+import io.github.chrisimx.scanbridge.savelastusedscansettings.RoomLastUsedScanSettingsRepository
import io.github.chrisimx.scanbridge.scan.AndroidStartScanUseCase
import io.github.chrisimx.scanbridge.scannerdiscovery.DiscoveryUsecase
import io.github.chrisimx.scanbridge.scannerdiscovery.ScannerDiscoveryScreenViewModel
@@ -65,9 +67,6 @@ import org.koin.plugin.module.dsl.single
import org.koin.plugin.module.dsl.viewModel
import timber.log.Timber
-fun createAppSettingsDataStore(context: Context) = context.appSettingsStore
-fun createShownMessagesDataStore(context: Context) = context.shownMessagesStore
-
fun createScanBridgeDb(factory: ScanBridgeDbFactory): ScanBridgeDb = factory.createInstance()
fun createScannerIconImageLoader(factory: HttpClientFactory, context: Context): ImageLoader {
@@ -91,17 +90,10 @@ fun createScannerIconImageLoader(factory: HttpClientFactory, context: Context):
.build()
}
val appModule = module {
- single>(named()) {
- create(::createShownMessagesDataStore)
- }
- single>(named()) {
- create(::createAppSettingsDataStore)
- }
single() bind Thread.UncaughtExceptionHandler::class
single() bind LocaleProvider::class
- single {
- FileDebugLogService(get(named()), get())
- } bind DebugLogService::class
+ single() bind AppSettingsRepository::class
+ single() bind DebugLogService::class
single() bind HttpClientFactory::class
single() bind ScanBridgeLoggerFactory::class
single()
@@ -118,6 +110,7 @@ val appModule = module {
factory() bind MdnsDiscoverService::class
single()
single() bind LastRouteRepository::class
+ single() bind LastUsedScanSettingsRepository::class
single(named("legacyDatastoreShownMessages")) {
DatastoreShownMessagesRepository(
get(named()),
@@ -138,7 +131,9 @@ val appModule = module {
single() bind BuildInfoProvider::class
- includes(SCAN_PROTOCOLS, ROOM_MIGRATIONS)
+ viewModel()
+
+ includes(SCAN_PROTOCOLS, ROOM_MIGRATIONS, DATASTORE_TO_ROOM_MIGRATION_DATA_SOURCES)
}
class ScanBridgeApplication : Application() {
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt
index 5a04ac42..88fb10e3 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeNavHost.kt
@@ -28,14 +28,12 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsRepository
import io.github.chrisimx.scanbridge.ports.ScanningProtocolManager
-import io.github.chrisimx.scanbridge.proto.scanningResponseTimeoutOrNull
import io.github.chrisimx.scanbridge.uicomponents.FullScreenError
import io.github.chrisimx.scanbridge.uicomponents.TemporaryFileHandler
import io.github.chrisimx.scanbridge.util.doTempFilesExist
import kotlin.uuid.Uuid
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
@@ -95,8 +93,8 @@ fun NavBackStackEntry.toTypedRoute(): BaseRoute? {
@Composable
fun ScanBridgeNavHost(navController: NavHostController, startDestination: Any) {
val context = LocalContext.current
- val appSettings = context.appSettingsStore.data
val protocolManager = koinInject()
+ val appSettingsRepo = koinInject()
NavHost(
modifier = Modifier.testTag("root_node"),
@@ -129,10 +127,10 @@ fun ScanBridgeNavHost(navController: NavHostController, startDestination: Any) {
}
composable { backStackEntry ->
val scannerRoute: ScannerRoute = backStackEntry.toRoute()
- val appSettingsCurrent = runBlocking { appSettings.first() }
- val debug = appSettingsCurrent.writeDebug
- val certValidationDisabled = appSettingsCurrent.disableCertChecks
- val timeout = appSettingsCurrent.scanningResponseTimeoutOrNull?.value?.toUInt() ?: 25u
+ val appSettingsCurrent = runBlocking { appSettingsRepo.getAppSettings() }
+ val debug = appSettingsCurrent.writeDebugLogs
+ val certValidationDisabled = appSettingsCurrent.disableCertValidation
+ val timeout = appSettingsCurrent.scanningResponseTimeoutInS
val scannerHandle = protocolManager.getScannerHandle(
scannerRoute.protocolId,
scannerRoute.scannerHandleString
@@ -149,7 +147,7 @@ fun ScanBridgeNavHost(navController: NavHostController, startDestination: Any) {
scannerRoute.scannerName,
scannerHandle,
navController,
- timeout,
+ timeout.toUInt(),
debug,
certValidationDisabled,
Uuid.parse(scannerRoute.sessionID)
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/data/ui/ScanningScreenViewModel.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/data/ui/ScanningScreenViewModel.kt
index e9b6b7f3..5a61584a 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/data/ui/ScanningScreenViewModel.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/data/ui/ScanningScreenViewModel.kt
@@ -44,8 +44,10 @@ import io.github.chrisimx.anyscan.ScanSettingsMap
import io.github.chrisimx.anyscan.ScannerConcept
import io.github.chrisimx.anyscan.inches
import io.github.chrisimx.scanbridge.R
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsRepository
import io.github.chrisimx.scanbridge.db.ScanBridgeDb
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
import io.github.chrisimx.scanbridge.db.entities.ScannedPage
import io.github.chrisimx.scanbridge.db.entities.Session
import io.github.chrisimx.scanbridge.db.entities.TempFile
@@ -57,9 +59,8 @@ import io.github.chrisimx.scanbridge.model.toggleRotation
import io.github.chrisimx.scanbridge.ports.InitialScanSettingsProvider
import io.github.chrisimx.scanbridge.ports.ScannerCapabilitiesResult
import io.github.chrisimx.scanbridge.ports.ScannerConnectionSettings
-import io.github.chrisimx.scanbridge.proto.chunkSizePdfExportOrNull
+import io.github.chrisimx.scanbridge.savelastusedscansettings.LastUsedScanSettingsRepository
import io.github.chrisimx.scanbridge.services.ScanJobRepository
-import io.github.chrisimx.scanbridge.stores.DefaultScanSettingsStore
import io.github.chrisimx.scanbridge.usecases.StartScanUseCase
import io.github.chrisimx.scanbridge.util.getEditedImageName
import io.github.chrisimx.scanbridge.util.rotateBy90
@@ -81,7 +82,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -112,6 +112,8 @@ class ScanningScreenViewModel(
val db: ScanBridgeDb,
application: Application,
val scanJobRepo: ScanJobRepository,
+ val appSettingsRepository: AppSettingsRepository,
+ val lastUsedScanSettingsRepo: LastUsedScanSettingsRepository,
val initialScanSettingsProvider: InitialScanSettingsProvider,
val startScanUseCase: StartScanUseCase
) : AndroidViewModel(application) {
@@ -161,11 +163,18 @@ class ScanningScreenViewModel(
init {
session
- .map { it?.currentScanSettings to it?.currentSettingsUIData }
+ .filter { it?.currentScanSettings != null && it.currentSettingsUIData != null }
+ .map {
+ LastUsedScanSettings(
+ lastUsedScanSettings = it!!.currentScanSettings!!,
+ lastUsedScanSettingsEnterable = it.currentSettingsUIData!!
+ )
+ }
.distinctUntilChanged()
- .filter { it.first != null }
- .onEach { (scanSettings, settingsUiState) ->
- DefaultScanSettingsStore.save(application, scanSettings!!, settingsUiState)
+ .onEach { newLastUsedScanSettings ->
+ lastUsedScanSettingsRepo.setLastUsedScanSettings(
+ newLastUsedScanSettings
+ )
}
.launchIn(viewModelScope)
}
@@ -337,12 +346,15 @@ class ScanningScreenViewModel(
}
} else {
// Try to load saved scan settings first, fallback to defaults if none exist
- val savedSettingsPair = DefaultScanSettingsStore.load(application.applicationContext)
- val (savedSettings, savedSettingsUiState) = savedSettingsPair
- val initialSettings = if (savedSettings != null) {
+ val savedSettings = lastUsedScanSettingsRepo.getLastUsedScanSettings()
+
+ val lastUsedScanSettings = savedSettings?.lastUsedScanSettings
+ val lastUsedScanSettingsEnterable = savedSettings?.lastUsedScanSettingsEnterable
+
+ val initialSettings = if (lastUsedScanSettings != null) {
val editor = CommonScanSettingsEditor(
caps,
- savedSettings
+ lastUsedScanSettings
)
editor.build()
} else {
@@ -354,16 +366,20 @@ class ScanningScreenViewModel(
editor.build()
}
- val savedSettingsUiStateWithCaps = savedSettingsUiState?.copy()
-
- sessionDao.insertAll(Session(sessionID, initialSettings, savedSettingsUiState))
+ sessionDao.insertAll(
+ Session(
+ sessionID,
+ initialSettings,
+ lastUsedScanSettingsEnterable
+ )
+ )
_scanningScreenData.scanSettingsVM.value = getKoin().get {
parametersOf(
MutableStateFlow(caps).asStateFlow(),
session.map { it?.currentScanSettings ?: initialSettings }
.stateIn(viewModelScope, SharingStarted.Lazily, initialSettings),
- savedSettingsUiStateWithCaps ?: defaultScanSettingsUIData,
+ lastUsedScanSettingsEnterable ?: defaultScanSettingsUIData,
updateSettings,
viewModelScope
)
@@ -495,10 +511,10 @@ class ScanningScreenViewModel(
var pageCounter = 0
val chunkSize = try {
- application.appSettingsStore.data.first().chunkSizePdfExportOrNull?.value ?: 50
+ appSettingsRepository.getAppSettings().chunkSizeForPDFExport
} catch (exception: Exception) {
Timber.e("doPdfExport couldn't access app settings. Returning default value: $exception")
- 50
+ AppSettings().chunkSizeForPDFExport
}
val chunks = currentScans.chunked(chunkSize)
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/datastore/DataStores.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/datastore/DataStores.kt
index 8ca88fc2..25224149 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/datastore/DataStores.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/datastore/DataStores.kt
@@ -11,6 +11,7 @@ import io.github.chrisimx.scanbridge.proto.LastRoute
import io.github.chrisimx.scanbridge.proto.ScanBridgeSettings
import io.github.chrisimx.scanbridge.proto.ShownMessages
+@Deprecated("Use the corresponding repository instead")
val Context.appSettingsStore: DataStore by dataStore(
fileName = "settings.pb",
serializer = ScanBridgeSettingsSerializer,
@@ -37,6 +38,7 @@ val Context.appSettingsStore: DataStore by dataStore(
}
)
+@Deprecated("Use the corresponding repository instead")
val Context.lastRouteStore: DataStore by dataStore(
fileName = "route_store.pb",
serializer = LastRouteStoreSerializer,
@@ -57,6 +59,7 @@ val Context.lastRouteStore: DataStore by dataStore(
}
)
+@Deprecated("Use the corresponding repository instead")
val Context.shownMessagesStore: DataStore by dataStore(
fileName = "shown_messages.pb",
serializer = ShownMessagesSerializer
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/D2RAppSettingsMigrationDataSourceImpl.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/D2RAppSettingsMigrationDataSourceImpl.kt
new file mode 100644
index 00000000..28435ce7
--- /dev/null
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/D2RAppSettingsMigrationDataSourceImpl.kt
@@ -0,0 +1,45 @@
+package io.github.chrisimx.scanbridge.migrations.ds2room
+
+import android.content.Context
+import io.github.chrisimx.scanbridge.appsettings.D2RAppSettingsMigrationDataSource
+import io.github.chrisimx.scanbridge.datastore.appSettingsStore
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+import io.github.chrisimx.scanbridge.proto.ScanBridgeSettings
+import io.github.chrisimx.scanbridge.proto.chunkSizePdfExportOrNull
+import io.github.chrisimx.scanbridge.proto.rememberScanSettingsOrNull
+import io.github.chrisimx.scanbridge.proto.scanningResponseTimeoutOrNull
+import kotlinx.coroutines.flow.first
+
+class D2RAppSettingsMigrationDataSourceImpl(context: Context) : D2RAppSettingsMigrationDataSource {
+ private val appSettingsStore = context.appSettingsStore
+
+ override suspend fun getAppSettings(): AppSettings {
+ val dataStoreAppSettings = appSettingsStore.data.first()
+
+ return dataStoreAppSettings.toAppSettings()
+ }
+
+ private fun ScanBridgeSettings.toAppSettings(): AppSettings {
+ // The defaults are set in the AppSettings constructor
+ // To use them, we create a new instance of AppSettings with these default params
+ // and then migrate the values from the legacy datastore to the new that actually have a value.
+ var appSettings = AppSettings(
+ writeDebugLogs = this.writeDebug,
+ disableCertValidation = this.disableCertChecks
+ )
+
+ this.scanningResponseTimeoutOrNull?.let {
+ appSettings = appSettings.copy(scanningResponseTimeoutInS = it.value)
+ }
+
+ this.chunkSizePdfExportOrNull?.let {
+ appSettings = appSettings.copy(chunkSizeForPDFExport = it.value)
+ }
+
+ this.rememberScanSettingsOrNull?.let {
+ appSettings = appSettings.copy(rememberScanSettings = it.value)
+ }
+
+ return appSettings
+ }
+}
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/D2RLastUsedScanSettingsMigrationDataSourceImpl.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/D2RLastUsedScanSettingsMigrationDataSourceImpl.kt
new file mode 100644
index 00000000..428e1f01
--- /dev/null
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/D2RLastUsedScanSettingsMigrationDataSourceImpl.kt
@@ -0,0 +1,23 @@
+package io.github.chrisimx.scanbridge.migrations.ds2room
+
+import android.content.Context
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
+import io.github.chrisimx.scanbridge.savelastusedscansettings.D2RLastUsedScanSettingsMigrationSource
+import io.github.chrisimx.scanbridge.stores.DatastoreLegacyLastScanSettingsStore
+
+class D2RLastUsedScanSettingsMigrationDataSourceImpl(val context: Context) : D2RLastUsedScanSettingsMigrationSource {
+
+ @Suppress("DEPRECATION")
+ override suspend fun getLastUsedScanSettings(): LastUsedScanSettings? {
+ val (settings, enterableData) = DatastoreLegacyLastScanSettingsStore.load(context)
+
+ return settings?.let { settings ->
+ enterableData?.let { enterableData ->
+ LastUsedScanSettings(
+ lastUsedScanSettings = settings,
+ lastUsedScanSettingsEnterable = enterableData
+ )
+ }
+ }
+ }
+}
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/Datastore2RoomMigrationDataSources.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/Datastore2RoomMigrationDataSources.kt
new file mode 100644
index 00000000..599fd404
--- /dev/null
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/migrations/ds2room/Datastore2RoomMigrationDataSources.kt
@@ -0,0 +1,12 @@
+package io.github.chrisimx.scanbridge.migrations.ds2room
+
+import io.github.chrisimx.scanbridge.appsettings.D2RAppSettingsMigrationDataSource
+import io.github.chrisimx.scanbridge.savelastusedscansettings.D2RLastUsedScanSettingsMigrationSource
+import org.koin.dsl.bind
+import org.koin.dsl.module
+import org.koin.plugin.module.dsl.single
+
+val DATASTORE_TO_ROOM_MIGRATION_DATA_SOURCES = module {
+ single() bind D2RAppSettingsMigrationDataSource::class
+ single() bind D2RLastUsedScanSettingsMigrationSource::class
+}
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/services/FileDebugLogService.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/services/FileDebugLogService.kt
index 7fdd3017..ba7ed702 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/services/FileDebugLogService.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/services/FileDebugLogService.kt
@@ -2,29 +2,26 @@ package io.github.chrisimx.scanbridge.services
import android.app.Application
import android.net.Uri
-import androidx.datastore.core.DataStore
import io.github.chrisimx.scanbridge.BuildConfig
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsRepository
import io.github.chrisimx.scanbridge.logs.FileLogger
-import io.github.chrisimx.scanbridge.proto.ScanBridgeSettings
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
-class FileDebugLogService(appSettings: DataStore, val application: Application) : DebugLogService {
+class FileDebugLogService(appSettings: AppSettingsRepository, val application: Application) : DebugLogService {
val scope = CoroutineScope(Dispatchers.Main)
private var debugWriter: BufferedWriter? = null
private var tree: Timber.Tree? = null
- private val debugLogActive = appSettings.data.map { it.writeDebug }.distinctUntilChanged()
+ private val debugLogActive = appSettings.getWriteDebugLogsFlow()
init {
debugLogActive.onEach {
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/DefaultScanSettingsStore.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/DatastoreLegacyLastScanSettingsStore.kt
similarity index 96%
rename from androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/DefaultScanSettingsStore.kt
rename to androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/DatastoreLegacyLastScanSettingsStore.kt
index edcf7493..99ac4ce1 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/DefaultScanSettingsStore.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/DatastoreLegacyLastScanSettingsStore.kt
@@ -32,12 +32,13 @@ import io.github.chrisimx.scanbridge.proto.rememberScanSettingsOrNull
import kotlinx.coroutines.flow.first
import timber.log.Timber
-object DefaultScanSettingsStore {
+object DatastoreLegacyLastScanSettingsStore {
private suspend fun isRememberSettingsEnabled(context: Context): Boolean {
val appPreferences = context.appSettingsStore.data.first()
return appPreferences.rememberScanSettingsOrNull?.value ?: true
}
+ @Deprecated("Use LastUsedScanSettingsRepository instead")
suspend fun save(context: Context, scanSettings: CommonScanSettings, uiStateData: ScanSettingsEnterableDataV1?) {
if (!isRememberSettingsEnabled(context)) {
Timber.d("Scan settings persistence is disabled, skipping save")
@@ -61,6 +62,7 @@ object DefaultScanSettingsStore {
}
}
+ @Deprecated("Use LastUsedScanSettingsRepository instead")
suspend fun load(context: Context): Pair {
if (!isRememberSettingsEnabled(context)) {
Timber.d("Scan settings persistence is disabled, returning null")
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/LegacyCustomScannerStore.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/LegacyCustomScannerStore.kt
deleted file mode 100644
index 2a3dd930..00000000
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/LegacyCustomScannerStore.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2024-2025 Christian Nagel and contributors
- *
- * This file is part of ScanBridge.
- *
- * ScanBridge is free software: you can redistribute it and/or modify it under the terms of
- * the GNU General Public License as published by the Free Software Foundation, either
- * version 3 of the License, or (at your option) any later version.
- *
- * ScanBridge is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with eSCLKt.
- * If not, see .
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-package io.github.chrisimx.scanbridge.stores
-
-import android.content.Context
-import androidx.core.content.edit
-import io.github.chrisimx.scanbridge.db.ScanBridgeDb
-import io.github.chrisimx.scanbridge.db.entities.CustomScanner
-import io.ktor.http.Url
-import kotlin.uuid.ExperimentalUuidApi
-import kotlin.uuid.Uuid
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
-import timber.log.Timber
-
-object LegacyCustomScannerStore {
- private const val PREF_NAME = "custom_scanner_store"
-
- @OptIn(ExperimentalUuidApi::class)
- @Deprecated("Was replaced by Room DB access")
- fun load(context: Context): List {
- val sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- val scannerList = mutableListOf()
-
- val count = sharedPreferences.getInt("count", 0)
-
- for (i in 0 until count) {
- val name = sharedPreferences.getString("$i.name", null)
- val url = sharedPreferences.getString("$i.url", null)
- val uuid = sharedPreferences.getString("$i.uuid", null)
-
- if (name == null || url == null || uuid == null) {
- Timber.e("Custom Scanner information that should be in stored in shared preferences is missing!")
- continue
- }
-
- try {
- val scanner = CustomScanner(
- name = name,
- url = Url(url),
- uuid = Uuid.parse(uuid),
- protocolIdentifier = "eSCL"
- )
- scannerList.add(scanner)
- } catch (e: IllegalArgumentException) {
- Timber.e(e, "Invalid URL for custom scanner: $url")
- continue
- }
- }
- return scannerList
- }
-
- @OptIn(ExperimentalUuidApi::class)
- fun clear(context: Context) {
- val sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
- sharedPreferences.edit {
- clear()
- }
- }
-
- // Room migration
- fun ScanBridgeDb.migrateLegacyCustomScanners(context: Context): ScanBridgeDb {
- val db = this
- val legacyData = load(context)
- if (legacyData.isNotEmpty()) {
- CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
- db.customScannerDao().insertAll(*legacyData.toTypedArray())
- clear(context)
- }
- }
- return this
- }
-}
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/LegacySessionsStore.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/LegacySessionsStore.kt
deleted file mode 100644
index 5db4d7fd..00000000
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/stores/LegacySessionsStore.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package io.github.chrisimx.scanbridge.stores
-
-import android.content.Context
-import androidx.room.immediateTransaction
-import androidx.room.useWriterConnection
-import io.github.chrisimx.esclkt.ScannerCapabilities
-import io.github.chrisimx.esclkt.anyscancompat.toCommonAbstraction
-import io.github.chrisimx.scanbridge.ScanSettingsJson
-import io.github.chrisimx.scanbridge.data.model.LegacySessionV2
-import io.github.chrisimx.scanbridge.data.model.LegacySessionV2.Companion.fromString
-import io.github.chrisimx.scanbridge.datastore.appSettingsStore
-import io.github.chrisimx.scanbridge.db.ScanBridgeDb
-import io.github.chrisimx.scanbridge.db.entities.ScannedPage
-import io.github.chrisimx.scanbridge.db.entities.Session
-import io.github.chrisimx.scanbridge.db.entities.TempFile
-import io.github.chrisimx.scanbridge.proto.copy
-import java.io.File
-import java.nio.file.Files
-import kotlin.concurrent.atomics.AtomicBoolean
-import kotlin.concurrent.atomics.ExperimentalAtomicApi
-import kotlin.io.path.Path
-import kotlin.uuid.Uuid
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
-import timber.log.Timber
-
-object LegacySessionsStore {
-
- @OptIn(ExperimentalAtomicApi::class)
- private val migrationStarted = AtomicBoolean(false)
-
- @Deprecated("Replaced with Room database")
- fun loadSession(application: Context, sessionID: String, caps: ScannerCapabilities?): Result {
- Timber.d("Loading session $sessionID")
-
- val path = application.applicationInfo.dataDir + "/files/" + sessionID + ".session"
- val file = File(path)
-
- if (!file.exists()) {
- Timber.d("Could not find session file at $path")
- return Result.success(null)
- }
-
- val sessionFileString = file.readText()
-
- return fromString(sessionFileString, ScanSettingsJson.json, caps)
- }
-
- @OptIn(ExperimentalAtomicApi::class)
- fun ScanBridgeDb.migrateLegacySessions(context: Context): ScanBridgeDb {
- if (!migrationStarted.compareAndSet(expectedValue = false, newValue = true)) {
- return this
- }
- CoroutineScope(Dispatchers.IO).launch {
- try {
- val migrationDone =
- context.appSettingsStore.data.firstOrNull()?.migratedSessionFiles ?: false
-
- if (migrationDone) {
- Timber.i("Migrating legacy sessions already done. Skipping!")
- return@launch
- }
-
- Timber.i("Migrating legacy sessions")
-
- val sessionDir = Path(context.applicationInfo.dataDir, "files")
-
- val db = this@migrateLegacySessions
-
- val sessionIds = sessionDir.toFile()
- .listFiles()
- ?.asSequence()
- ?.filter { it.isFile && it.extension == "session" }
- ?.map { it.nameWithoutExtension }
- ?.filter { it.isNotBlank() }
- ?.toList() ?: emptyList()
-
- sessionIds.forEach { sessionIdString ->
- val legacySession =
- loadSession(context, sessionIdString, null).getOrNull() ?: return@forEach
-
- val sessionId = Uuid.parse(legacySession.sessionID)
-
- db.useWriterConnection {
- it.immediateTransaction {
- db.insertLegacySessionData(sessionId, legacySession)
- }
- }
-
- Files.deleteIfExists(sessionDir.resolve("$sessionIdString.session"))
- }
-
- context.appSettingsStore.updateData {
- it.copy { migratedSessionFiles = true }
- }
- } catch (e: Exception) {
- Timber.e(e, "Legacy migration failed")
- } finally {
- migrationStarted.store(false)
- }
- }
-
- return this
- }
-
- private suspend fun ScanBridgeDb.insertLegacySessionData(sessionId: Uuid, legacySession: LegacySessionV2) {
- sessionDao().insertAll(
- Session(sessionId, legacySession.scanSettings?.toCommonAbstraction(), null)
- )
-
- tmpFileDao().insertAllList(
- legacySession.tmpFiles.map {
- TempFile(ownerSessionId = sessionId, path = it)
- }
- )
-
- scannedPageDao().insertAllList(
- legacySession.scannedPages.mapIndexed { index, page ->
- ScannedPage(
- scanId = Uuid.generateV4(),
- ownerSessionId = sessionId,
- filePath = page.filePath,
- originalScanSettings = page.originalScanSettings.toCommonAbstraction(),
- rotation = page.rotation,
- orderIndex = index
- )
- }
- )
- }
-}
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt
index f48d5bc3..26683e67 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/FoundScannerItem.kt
@@ -122,7 +122,7 @@ fun FoundScannerItem(
if (iconUrl != null) {
AsyncImage(
- model = iconUrl.toString(),
+ model = iconUrl,
contentDescription = stringResource(id = R.string.print_symbol_desc),
imageLoader = imageLoader,
modifier = Modifier
diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/settings/VersionComposable.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/settings/VersionComposable.kt
index 58c7cfdd..3ea16f00 100644
--- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/settings/VersionComposable.kt
+++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/uicomponents/settings/VersionComposable.kt
@@ -30,7 +30,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
@@ -38,15 +37,20 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import io.github.chrisimx.scanbridge.BuildConfig
import io.github.chrisimx.scanbridge.R
+import io.github.chrisimx.scanbridge.buildinfo.ScanBridgeEdition
import io.github.chrisimx.scanbridge.theme.Poppins
import io.github.chrisimx.scanbridge.theme.gradientBrush
@Composable
-fun VersionComposable() {
- val context = LocalContext.current
+fun ScanBridgeEdition.toLocalizedString(): String = when (this) {
+ ScanBridgeEdition.FDROID -> stringResource(R.string.f_droid)
+ ScanBridgeEdition.PLAYSTORE -> stringResource(R.string.google_play)
+ ScanBridgeEdition.IOS -> stringResource(R.string.ios)
+}
+@Composable
+fun VersionComposable(versionName: String, versionCode: Int, gitCommitHash: String, edition: ScanBridgeEdition, debugBuild: Boolean) {
Image(
modifier = Modifier
.size(200.dp)
@@ -67,20 +71,16 @@ fun VersionComposable() {
)
Text(
- "${BuildConfig.VERSION_NAME.removeSuffix("-play")} (${BuildConfig.VERSION_CODE}, ${BuildConfig.GIT_COMMIT_HASH})",
+ "${versionName.removeSuffix("-play")} ($versionCode, $gitCommitHash)",
fontStyle = FontStyle.Normal,
fontFamily = Poppins()
)
val editionNotice = mutableListOf()
- if (BuildConfig.FLAVOR == "fdroid") {
- editionNotice.add(stringResource(R.string.f_droid))
- } else {
- editionNotice.add(stringResource(R.string.google_play))
- }
+ editionNotice.add(edition.toLocalizedString())
- if (BuildConfig.DEBUG) {
+ if (debugBuild) {
editionNotice.add(stringResource(R.string.debug_build))
}
@@ -99,6 +99,12 @@ fun VersionComposablePreview() {
.width(300.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- VersionComposable()
+ VersionComposable(
+ "2.1.0",
+ 2100,
+ "abdfefg",
+ ScanBridgeEdition.FDROID,
+ true
+ )
}
}
diff --git a/composeUI/src/androidMain/res/values/strings.xml b/composeUI/src/androidMain/res/values/strings.xml
index a06733db..9846b4f6 100644
--- a/composeUI/src/androidMain/res/values/strings.xml
+++ b/composeUI/src/androidMain/res/values/strings.xml
@@ -134,4 +134,5 @@
Preview
3D Object
Business Card
+ iOS
\ No newline at end of file
diff --git a/core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/7.json b/core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/7.json
new file mode 100644
index 00000000..34471941
--- /dev/null
+++ b/core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/7.json
@@ -0,0 +1,364 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 7,
+ "identityHash": "392c2a55e575de7cc057617ba14b4b70",
+ "entities": [
+ {
+ "tableName": "customscanners",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `protocolIdentifier` TEXT NOT NULL DEFAULT 'eSCL', PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "protocolIdentifier",
+ "columnName": "protocolIdentifier",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'eSCL'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uuid"
+ ]
+ }
+ },
+ {
+ "tableName": "scannedpages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scanId` TEXT NOT NULL, `ownerSessionId` TEXT NOT NULL, `filePath` TEXT NOT NULL, `originalScanSettings` TEXT NOT NULL, `rotation` TEXT NOT NULL, `orderIndex` INTEGER NOT NULL, `outputName` TEXT, PRIMARY KEY(`scanId`), FOREIGN KEY(`ownerSessionId`) REFERENCES `sessions`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "scanId",
+ "columnName": "scanId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ownerSessionId",
+ "columnName": "ownerSessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "filePath",
+ "columnName": "filePath",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originalScanSettings",
+ "columnName": "originalScanSettings",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rotation",
+ "columnName": "rotation",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderIndex",
+ "columnName": "orderIndex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "outputName",
+ "columnName": "outputName",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "scanId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_scannedpages_ownerSessionId",
+ "unique": false,
+ "columnNames": [
+ "ownerSessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_scannedpages_ownerSessionId` ON `${TABLE_NAME}` (`ownerSessionId`)"
+ },
+ {
+ "name": "index_scannedpages_ownerSessionId_orderIndex",
+ "unique": true,
+ "columnNames": [
+ "ownerSessionId",
+ "orderIndex"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_scannedpages_ownerSessionId_orderIndex` ON `${TABLE_NAME}` (`ownerSessionId`, `orderIndex`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "sessions",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "ownerSessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "sessions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `currentScanSettings` TEXT, `currentSettingsUIData` TEXT DEFAULT null, `currentPage` INTEGER NOT NULL, PRIMARY KEY(`sessionId`))",
+ "fields": [
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currentScanSettings",
+ "columnName": "currentScanSettings",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "currentSettingsUIData",
+ "columnName": "currentSettingsUIData",
+ "affinity": "TEXT",
+ "defaultValue": "null"
+ },
+ {
+ "fieldPath": "currentPage",
+ "columnName": "currentPage",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "sessionId"
+ ]
+ }
+ },
+ {
+ "tableName": "tempfiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tempFileId` TEXT NOT NULL, `ownerSessionId` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`tempFileId`), FOREIGN KEY(`ownerSessionId`) REFERENCES `sessions`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "tempFileId",
+ "columnName": "tempFileId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ownerSessionId",
+ "columnName": "ownerSessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "tempFileId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_tempfiles_ownerSessionId",
+ "unique": false,
+ "columnNames": [
+ "ownerSessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_tempfiles_ownerSessionId` ON `${TABLE_NAME}` (`ownerSessionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "sessions",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "ownerSessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "lastroute",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route` TEXT NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "route",
+ "columnName": "route",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "executedmigrationtoroom",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`migrationId` TEXT NOT NULL, PRIMARY KEY(`migrationId`))",
+ "fields": [
+ {
+ "fieldPath": "migrationId",
+ "columnName": "migrationId",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "migrationId"
+ ]
+ }
+ },
+ {
+ "tableName": "shownstartupmessages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message` TEXT NOT NULL, PRIMARY KEY(`message`))",
+ "fields": [
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "message"
+ ]
+ }
+ },
+ {
+ "tableName": "appsettings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `writeDebugLogs` INTEGER NOT NULL, `disableCertValidation` INTEGER NOT NULL, `scanningResponseTimeoutInS` INTEGER NOT NULL, `chunkSizeForPDFExport` INTEGER NOT NULL, `rememberScanSettings` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "writeDebugLogs",
+ "columnName": "writeDebugLogs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "disableCertValidation",
+ "columnName": "disableCertValidation",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scanningResponseTimeoutInS",
+ "columnName": "scanningResponseTimeoutInS",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chunkSizeForPDFExport",
+ "columnName": "chunkSizeForPDFExport",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rememberScanSettings",
+ "columnName": "rememberScanSettings",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "lastusedscansettings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `lastUsedScanSettings` TEXT NOT NULL, `lastUsedScanSettingsEnterable` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUsedScanSettings",
+ "columnName": "lastUsedScanSettings",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUsedScanSettingsEnterable",
+ "columnName": "lastUsedScanSettingsEnterable",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '392c2a55e575de7cc057617ba14b4b70')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/AppSettingsRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/AppSettingsRepository.kt
new file mode 100644
index 00000000..af2699c1
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/AppSettingsRepository.kt
@@ -0,0 +1,56 @@
+package io.github.chrisimx.scanbridge.appsettings
+
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+interface AppSettingsRepository {
+ suspend fun getAppSettings(): AppSettings
+ fun getAppSettingsFlow(): Flow
+
+ suspend fun setAppSettings(appSettings: AppSettings)
+ suspend fun updateAppSettings(updateOperation: AppSettings.() -> AppSettings)
+ suspend fun resetAppSettings() {
+ setAppSettings(AppSettings())
+ }
+
+ suspend fun setWriteDebugLogs(value: Boolean) = updateAppSettings {
+ copy(writeDebugLogs = value)
+ }
+
+ fun getWriteDebugLogsFlow(): Flow = getAppSettingsFlow().map {
+ it.writeDebugLogs
+ }
+
+ suspend fun setDisableCertValidation(value: Boolean) = updateAppSettings {
+ copy(disableCertValidation = value)
+ }
+
+ fun getDisableCertValidationFlow(): Flow = getAppSettingsFlow().map {
+ it.disableCertValidation
+ }
+
+ suspend fun setScanningResponseTimeout(value: Int) = updateAppSettings {
+ copy(scanningResponseTimeoutInS = value)
+ }
+
+ fun getScanningResponseTimeoutFlow(): Flow = getAppSettingsFlow().map {
+ it.scanningResponseTimeoutInS
+ }
+
+ suspend fun setPdfExportChunkSize(value: Int) = updateAppSettings {
+ copy(chunkSizeForPDFExport = value)
+ }
+
+ fun getPdfExportChunkSizeFlow(): Flow = getAppSettingsFlow().map {
+ it.chunkSizeForPDFExport
+ }
+
+ suspend fun setRememberScanSettings(value: Boolean) = updateAppSettings {
+ copy(rememberScanSettings = value)
+ }
+
+ fun getRememberScanSettingsFlow(): Flow = getAppSettingsFlow().map {
+ it.rememberScanSettings
+ }
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/AppSettingsViewModel.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/AppSettingsViewModel.kt
new file mode 100644
index 00000000..ca3b952b
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/AppSettingsViewModel.kt
@@ -0,0 +1,75 @@
+package io.github.chrisimx.scanbridge.appsettings
+
+import com.rickclephas.kmp.observableviewmodel.ViewModel
+import com.rickclephas.kmp.observableviewmodel.launch
+import com.rickclephas.kmp.observableviewmodel.stateIn
+import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+
+class AppSettingsViewModel(val appSettingsRepository: AppSettingsRepository, buildInfoProvider: BuildInfoProvider) : ViewModel() {
+ val versionName = buildInfoProvider.versionName
+ val versionCode = buildInfoProvider.versionCode
+ val gitCommitHash = buildInfoProvider.commit
+ val edition = buildInfoProvider.edition
+ val isDebugBuild = buildInfoProvider.isDebugBuild
+
+ val disableCertChecks = appSettingsRepository.getDisableCertValidationFlow()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings().disableCertValidation)
+ val rememberScanSettings = appSettingsRepository.getRememberScanSettingsFlow()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings().rememberScanSettings)
+ val scanningResponseTimeout = appSettingsRepository.getScanningResponseTimeoutFlow()
+ .map { it.toUInt() }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings().scanningResponseTimeoutInS.toUInt())
+ val pdfChunkSize = appSettingsRepository.getPdfExportChunkSizeFlow()
+ .map { it.toUInt() }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings().chunkSizeForPDFExport.toUInt())
+ val writeDebugLogs = appSettingsRepository.getWriteDebugLogsFlow()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AppSettings().writeDebugLogs)
+
+ fun setDisableCertValidation(value: Boolean) {
+ viewModelScope.launch {
+ appSettingsRepository.setDisableCertValidation(value)
+ }
+ }
+
+ fun setRememberScanSettings(value: Boolean) {
+ viewModelScope.launch {
+ appSettingsRepository.setRememberScanSettings(value)
+ }
+ }
+
+ fun setScanningResponseTimeout(value: UInt) {
+ if (value > Int.MAX_VALUE.toUInt()) {
+ return
+ }
+
+ viewModelScope.launch {
+ appSettingsRepository.setScanningResponseTimeout(value.toInt())
+ }
+ }
+
+ fun setPdfExportChunkSize(value: UInt) {
+ if (value > Int.MAX_VALUE.toUInt()) {
+ return
+ }
+
+ viewModelScope.launch {
+ appSettingsRepository.setPdfExportChunkSize(value.toInt())
+ }
+ }
+
+ fun setWriteDebugLogs(value: Boolean) {
+ viewModelScope.launch {
+ appSettingsRepository.setWriteDebugLogs(value)
+ }
+ }
+
+ fun getDefaultPdfExportChunkSize(): UInt = AppSettings().chunkSizeForPDFExport.toUInt()
+ fun getDefaultScanningResponseTimeout(): UInt = AppSettings().scanningResponseTimeoutInS.toUInt()
+
+ suspend fun getPdfExportChunkSize(): UInt = appSettingsRepository.getAppSettings().chunkSizeForPDFExport.toUInt()
+
+ suspend fun getScanningResponseTimeout(): UInt = appSettingsRepository.getAppSettings().scanningResponseTimeoutInS.toUInt()
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/D2RAppSettingsMigrationDataSource.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/D2RAppSettingsMigrationDataSource.kt
new file mode 100644
index 00000000..5ec470e4
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/D2RAppSettingsMigrationDataSource.kt
@@ -0,0 +1,7 @@
+package io.github.chrisimx.scanbridge.appsettings
+
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+
+interface D2RAppSettingsMigrationDataSource {
+ suspend fun getAppSettings(): AppSettings
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/RoomAppSettingsRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/RoomAppSettingsRepository.kt
new file mode 100644
index 00000000..014ffa08
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/appsettings/RoomAppSettingsRepository.kt
@@ -0,0 +1,21 @@
+package io.github.chrisimx.scanbridge.appsettings
+
+import io.github.chrisimx.scanbridge.db.ScanBridgeDb
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class RoomAppSettingsRepository(scanBridgeDb: ScanBridgeDb) : AppSettingsRepository {
+ private val appSettingsDao = scanBridgeDb.appSettingsDao()
+
+ override suspend fun getAppSettings(): AppSettings = appSettingsDao.getAppSettings() ?: AppSettings()
+
+ override fun getAppSettingsFlow(): Flow = appSettingsDao.getAppSettingsFlow().map {
+ it ?: AppSettings()
+ }
+
+ override suspend fun setAppSettings(appSettings: AppSettings) = appSettingsDao.setAppSettings(appSettings)
+
+ override suspend fun updateAppSettings(updateOperation: AppSettings.() -> AppSettings) =
+ appSettingsDao.updateAppSettings(updateOperation)
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt
index d016f35f..db4c03b7 100644
--- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt
@@ -4,16 +4,20 @@ import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
+import io.github.chrisimx.scanbridge.db.daos.AppSettingsDao
import io.github.chrisimx.scanbridge.db.daos.CustomScannerDao
import io.github.chrisimx.scanbridge.db.daos.ExecutedMigrationsDao
import io.github.chrisimx.scanbridge.db.daos.LastRouteDao
+import io.github.chrisimx.scanbridge.db.daos.LastUsedScanSettingsDao
import io.github.chrisimx.scanbridge.db.daos.ScannedPageDao
import io.github.chrisimx.scanbridge.db.daos.SessionDao
import io.github.chrisimx.scanbridge.db.daos.ShownStartupMessageDao
import io.github.chrisimx.scanbridge.db.daos.TempFileDao
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
import io.github.chrisimx.scanbridge.db.entities.CustomScanner
import io.github.chrisimx.scanbridge.db.entities.ExecutedMigrationToRoom
import io.github.chrisimx.scanbridge.db.entities.LastRoute
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
import io.github.chrisimx.scanbridge.db.entities.ScannedPage
import io.github.chrisimx.scanbridge.db.entities.Session
import io.github.chrisimx.scanbridge.db.entities.ShownStartupMessage
@@ -30,9 +34,10 @@ import io.github.chrisimx.scanbridge.db.typeconverters.UuidTypeConverter
CustomScanner::class, ScannedPage::class,
Session::class, TempFile::class,
LastRoute::class, ExecutedMigrationToRoom::class,
- ShownStartupMessage::class
+ ShownStartupMessage::class, AppSettings::class,
+ LastUsedScanSettings::class
],
- version = 6,
+ version = 7,
autoMigrations = [
AutoMigration(
from = 1,
@@ -65,4 +70,6 @@ abstract class ScanBridgeDb : RoomDatabase() {
abstract fun lastRouteDao(): LastRouteDao
abstract fun executedMigrationsDao(): ExecutedMigrationsDao
abstract fun shownStartupMessageDao(): ShownStartupMessageDao
+ abstract fun appSettingsDao(): AppSettingsDao
+ abstract fun lastUsedScanSettingsDao(): LastUsedScanSettingsDao
}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt
index fbc043d9..3a97b7dd 100644
--- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt
@@ -2,7 +2,8 @@ package io.github.chrisimx.scanbridge.db
import ROOM_MIGRATION_4_5
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
-import io.github.chrisimx.scanbridge.db.migrations.RoomMigrationV5To6
+import io.github.chrisimx.scanbridge.db.migrations.RoomMigrationVersion5To6
+import io.github.chrisimx.scanbridge.db.migrations.RoomMigrationVersion6To7
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@@ -10,14 +11,17 @@ interface ScanBridgeDbFactory {
fun createInstance(): ScanBridgeDb
}
-class DefaultScanBridgeDbFactory(val builderFactory: ScanBridgeDbBuilderFactory, val migrationV5To6: RoomMigrationV5To6) :
- ScanBridgeDbFactory {
+class DefaultScanBridgeDbFactory(
+ val builderFactory: ScanBridgeDbBuilderFactory,
+ val migrationV5To6: RoomMigrationVersion5To6,
+ val migrationV6To7: RoomMigrationVersion6To7
+) : ScanBridgeDbFactory {
override fun createInstance(): ScanBridgeDb {
val dbBuilder = builderFactory.getBuilder()
return dbBuilder
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
- .addMigrations(ROOM_MIGRATION_4_5, migrationV5To6)
+ .addMigrations(ROOM_MIGRATION_4_5, migrationV5To6, migrationV6To7)
.build()
}
}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/AppSettingsDao.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/AppSettingsDao.kt
new file mode 100644
index 00000000..4e7493ad
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/AppSettingsDao.kt
@@ -0,0 +1,30 @@
+package io.github.chrisimx.scanbridge.db.daos
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import io.github.chrisimx.scanbridge.db.entities.AppSettings
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface AppSettingsDao {
+ @Query("SELECT * FROM appsettings WHERE id = 1")
+ suspend fun getAppSettings(): AppSettings?
+
+ @Query("SELECT * FROM appsettings WHERE id = 1")
+ fun getAppSettingsFlow(): Flow
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun setAppSettings(appSettings: AppSettings)
+
+ @Transaction
+ suspend fun updateAppSettings(updateOperation: AppSettings.() -> AppSettings) {
+ val appSettings = getAppSettings() ?: AppSettings()
+ setAppSettings(appSettings.updateOperation())
+ }
+
+ @Query("DELETE FROM appsettings WHERE id = 1")
+ suspend fun clearAppSettings()
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/LastUsedScanSettingsDao.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/LastUsedScanSettingsDao.kt
new file mode 100644
index 00000000..36d1ed85
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/LastUsedScanSettingsDao.kt
@@ -0,0 +1,19 @@
+package io.github.chrisimx.scanbridge.db.daos
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
+
+@Dao
+interface LastUsedScanSettingsDao {
+ @Query("SELECT * FROM lastusedscansettings WHERE id = 1")
+ suspend fun getLastUsedScanSettings(): LastUsedScanSettings?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun setLastUsedScanSettings(lastUsedScanSettings: LastUsedScanSettings)
+
+ @Query("DELETE FROM lastusedscansettings WHERE id = 1")
+ suspend fun clearLastUsedScanSettings()
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/AppSettings.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/AppSettings.kt
new file mode 100644
index 00000000..b009b70d
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/AppSettings.kt
@@ -0,0 +1,15 @@
+package io.github.chrisimx.scanbridge.db.entities
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "appsettings")
+data class AppSettings(
+ @PrimaryKey
+ val id: Int = 1,
+ val writeDebugLogs: Boolean = false,
+ val disableCertValidation: Boolean = false,
+ val scanningResponseTimeoutInS: Int = 25,
+ val chunkSizeForPDFExport: Int = 50,
+ val rememberScanSettings: Boolean = true
+)
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/LastUsedScanSettings.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/LastUsedScanSettings.kt
new file mode 100644
index 00000000..5ae7938a
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/LastUsedScanSettings.kt
@@ -0,0 +1,14 @@
+package io.github.chrisimx.scanbridge.db.entities
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import io.github.chrisimx.anyscan.CommonScanSettings
+import io.github.chrisimx.scanbridge.model.ScanSettingsEnterableDataV1
+
+@Entity(tableName = "lastusedscansettings")
+data class LastUsedScanSettings(
+ @PrimaryKey
+ val id: Int = 1,
+ val lastUsedScanSettings: CommonScanSettings,
+ val lastUsedScanSettingsEnterable: ScanSettingsEnterableDataV1
+)
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt
index 82491b9f..6d2a37c5 100644
--- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt
@@ -4,5 +4,6 @@ import org.koin.dsl.module
import org.koin.plugin.module.dsl.single
val ROOM_MIGRATIONS = module {
- single()
+ single()
+ single()
}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion5To6.kt
similarity index 93%
rename from core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt
rename to core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion5To6.kt
index 2d492691..0752890a 100644
--- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion5To6.kt
@@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
-class RoomMigrationV5To6(val buildInfoProvider: BuildInfoProvider, val koinScope: Scope, val loggerFactory: ScanBridgeLoggerFactory) :
+class RoomMigrationVersion5To6(val buildInfoProvider: BuildInfoProvider, val koinScope: Scope, val loggerFactory: ScanBridgeLoggerFactory) :
Migration(5, 6) {
private val logger = loggerFactory.withClass(this::class)
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion6To7.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion6To7.kt
new file mode 100644
index 00000000..23e31fb9
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion6To7.kt
@@ -0,0 +1,76 @@
+package io.github.chrisimx.scanbridge.db.migrations
+
+import androidx.room.migration.Migration
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.execSQL
+import io.github.chrisimx.scanbridge.ScanSettingsJson
+import io.github.chrisimx.scanbridge.appsettings.D2RAppSettingsMigrationDataSource
+import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider
+import io.github.chrisimx.scanbridge.model.Platform
+import io.github.chrisimx.scanbridge.ports.ScanBridgeLoggerFactory
+import io.github.chrisimx.scanbridge.savelastusedscansettings.D2RLastUsedScanSettingsMigrationSource
+import kotlinx.coroutines.runBlocking
+import org.koin.core.scope.Scope
+
+class RoomMigrationVersion6To7(val buildInfoProvider: BuildInfoProvider, val koinScope: Scope, val loggerFactory: ScanBridgeLoggerFactory) :
+ Migration(6, 7) {
+
+ private val logger = loggerFactory.withClass(this::class)
+
+ override fun migrate(connection: SQLiteConnection): Unit = runBlocking {
+ val (oldAppSettings, oldLastUsedScanSettings) = if (buildInfoProvider.platform == Platform.ANDROID) {
+ // On Android, we need to get the old AppSettings from the DataStore
+ logger.debug { "Android: Migration of old app settings and last used scan settings from DataStore to Room" }
+ val legacyAppSettingsDatasource = koinScope.get()
+ val legacyLastUsedScanSettingsDatasource = koinScope.get()
+
+ legacyAppSettingsDatasource.getAppSettings() to legacyLastUsedScanSettingsDatasource.getLastUsedScanSettings()
+ } else {
+ logger.debug {
+ "Other Platform (not Android): Migration of old AppSettings and last used scan settings from DataStore to Room not necessary"
+ }
+ null to null
+ }
+
+ connection.execSQL(
+ "CREATE TABLE IF NOT EXISTS `appsettings` (`id` INTEGER NOT NULL, `writeDebugLogs` INTEGER NOT NULL, `disableCertValidation` INTEGER NOT NULL, `scanningResponseTimeoutInS` INTEGER NOT NULL, `chunkSizeForPDFExport` INTEGER NOT NULL, `rememberScanSettings` INTEGER NOT NULL, PRIMARY KEY(`id`))"
+ )
+ connection.execSQL(
+ "CREATE TABLE IF NOT EXISTS `lastusedscansettings` (`id` INTEGER NOT NULL, `lastUsedScanSettings` TEXT NOT NULL, `lastUsedScanSettingsEnterable` TEXT NOT NULL, PRIMARY KEY(`id`))"
+ )
+
+ oldAppSettings?.let {
+ logger.debug { "Inserting old app settings into Room" }
+ val appSettingsInsert = connection.prepare(
+ "INSERT INTO appsettings (id, writeDebugLogs, disableCertValidation, scanningResponseTimeoutInS, chunkSizeForPDFExport, rememberScanSettings) VALUES (?, ?, ?, ?, ?, ?)"
+ )
+ appSettingsInsert.bindInt(1, it.id)
+ appSettingsInsert.bindBoolean(2, it.writeDebugLogs)
+ appSettingsInsert.bindBoolean(3, it.disableCertValidation)
+ appSettingsInsert.bindInt(4, it.scanningResponseTimeoutInS)
+ appSettingsInsert.bindInt(5, it.chunkSizeForPDFExport)
+ appSettingsInsert.bindBoolean(6, it.rememberScanSettings)
+ appSettingsInsert.step()
+ appSettingsInsert.close()
+ }
+
+ oldLastUsedScanSettings?.let {
+ logger.debug { "Inserting old last used scan settings into Room" }
+ val lastUsedScanSettingsInsert = connection.prepare(
+ "INSERT INTO lastusedscansettings (id, lastUsedScanSettings, lastUsedScanSettingsEnterable) VALUES (?, ?, ?)"
+ )
+ val scanSettingsJson = ScanSettingsJson.json
+ lastUsedScanSettingsInsert.bindInt(1, oldLastUsedScanSettings.id)
+ lastUsedScanSettingsInsert.bindText(
+ 2,
+ scanSettingsJson.encodeToString(oldLastUsedScanSettings.lastUsedScanSettings)
+ )
+ lastUsedScanSettingsInsert.bindText(
+ 3,
+ scanSettingsJson.encodeToString(oldLastUsedScanSettings.lastUsedScanSettingsEnterable)
+ )
+ lastUsedScanSettingsInsert.step()
+ lastUsedScanSettingsInsert.close()
+ }
+ }
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/D2RLastUsedScanSettingsMigrationSource.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/D2RLastUsedScanSettingsMigrationSource.kt
new file mode 100644
index 00000000..f363dded
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/D2RLastUsedScanSettingsMigrationSource.kt
@@ -0,0 +1,7 @@
+package io.github.chrisimx.scanbridge.savelastusedscansettings
+
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
+
+interface D2RLastUsedScanSettingsMigrationSource {
+ suspend fun getLastUsedScanSettings(): LastUsedScanSettings?
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/LastUsedScanSettingsRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/LastUsedScanSettingsRepository.kt
new file mode 100644
index 00000000..61856748
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/LastUsedScanSettingsRepository.kt
@@ -0,0 +1,8 @@
+package io.github.chrisimx.scanbridge.savelastusedscansettings
+
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
+
+interface LastUsedScanSettingsRepository {
+ suspend fun getLastUsedScanSettings(): LastUsedScanSettings?
+ suspend fun setLastUsedScanSettings(scanSettings: LastUsedScanSettings?)
+}
diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/RoomLastUsedScanSettingsRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/RoomLastUsedScanSettingsRepository.kt
new file mode 100644
index 00000000..ccfaef47
--- /dev/null
+++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/savelastusedscansettings/RoomLastUsedScanSettingsRepository.kt
@@ -0,0 +1,25 @@
+package io.github.chrisimx.scanbridge.savelastusedscansettings
+
+import io.github.chrisimx.scanbridge.appsettings.AppSettingsRepository
+import io.github.chrisimx.scanbridge.db.ScanBridgeDb
+import io.github.chrisimx.scanbridge.db.entities.LastUsedScanSettings
+
+class RoomLastUsedScanSettingsRepository(scanBridgeDb: ScanBridgeDb, private val appSettingsRepository: AppSettingsRepository) :
+ LastUsedScanSettingsRepository {
+ private val lastUsedScanSettingsDao = scanBridgeDb.lastUsedScanSettingsDao()
+
+ override suspend fun getLastUsedScanSettings(): LastUsedScanSettings? {
+ val currentAppSettings = appSettingsRepository.getAppSettings()
+ if (!currentAppSettings.rememberScanSettings) return null
+
+ return lastUsedScanSettingsDao.getLastUsedScanSettings()
+ }
+
+ override suspend fun setLastUsedScanSettings(scanSettings: LastUsedScanSettings?) {
+ if (scanSettings != null) {
+ lastUsedScanSettingsDao.setLastUsedScanSettings(scanSettings)
+ } else {
+ lastUsedScanSettingsDao.clearLastUsedScanSettings()
+ }
+ }
+}