New SettingsScreen with export dir preference

This commit is contained in:
Pierre-Yves Nicolas
2025-11-27 17:40:03 +01:00
committed by pynicolas
parent 96b2d5b830
commit 7c9267a866
15 changed files with 371 additions and 37 deletions

View File

@@ -116,6 +116,7 @@ dependencies {
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.protobuf.javalite) implementation(libs.protobuf.javalite)
implementation(libs.litert) implementation(libs.litert)
implementation(libs.litert.support) implementation(libs.litert.support)

View File

@@ -32,6 +32,8 @@ import org.fairscan.app.ui.screens.about.AboutViewModel
import org.fairscan.app.ui.screens.camera.CameraViewModel import org.fairscan.app.ui.screens.camera.CameraViewModel
import org.fairscan.app.ui.screens.export.ExportViewModel import org.fairscan.app.ui.screens.export.ExportViewModel
import org.fairscan.app.ui.screens.home.HomeViewModel import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.screens.settings.SettingsRepository
import org.fairscan.app.ui.screens.settings.SettingsViewModel
import java.io.File import java.io.File
class FairScanApp : Application() { class FairScanApp : Application() {
@@ -58,6 +60,7 @@ class AppContainer(context: Context) {
val logger = FileLogger(logRepository) val logger = FileLogger(logRepository)
val imageSegmentationService = ImageSegmentationService(context, logger) val imageSegmentationService = ImageSegmentationService(context, logger)
val recentDocumentsDataStore = context.recentDocumentsDataStore val recentDocumentsDataStore = context.recentDocumentsDataStore
val settingsRepository = SettingsRepository(context)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
inline fun <reified VM : ViewModel> viewModelFactory( inline fun <reified VM : ViewModel> viewModelFactory(
@@ -73,4 +76,5 @@ class AppContainer(context: Context) {
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) } val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
val exportViewModelFactory = viewModelFactory { ExportViewModel(it) } val exportViewModelFactory = viewModelFactory { ExportViewModel(it) }
val aboutViewModelFactory = viewModelFactory { AboutViewModel(it) } val aboutViewModelFactory = viewModelFactory { AboutViewModel(it) }
val settingsViewModelFactory = viewModelFactory { SettingsViewModel(it) }
} }

View File

@@ -67,6 +67,8 @@ import org.fairscan.app.ui.screens.export.ExportViewModel
import org.fairscan.app.ui.screens.export.PdfGenerationActions import org.fairscan.app.ui.screens.export.PdfGenerationActions
import org.fairscan.app.ui.screens.home.HomeScreen import org.fairscan.app.ui.screens.home.HomeScreen
import org.fairscan.app.ui.screens.home.HomeViewModel import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.screens.settings.SettingsScreen
import org.fairscan.app.ui.screens.settings.SettingsViewModel
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
import org.opencv.android.OpenCVLoader import org.opencv.android.OpenCVLoader
@@ -83,6 +85,8 @@ class MainActivity : ComponentActivity() {
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory } val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
val exportViewModel: ExportViewModel by viewModels { appContainer.exportViewModelFactory } val exportViewModel: ExportViewModel by viewModels { appContainer.exportViewModelFactory }
val aboutViewModel: AboutViewModel by viewModels { appContainer.aboutViewModelFactory } val aboutViewModel: AboutViewModel by viewModels { appContainer.aboutViewModelFactory }
val settingsViewModel: SettingsViewModel
by viewModels { appContainer.settingsViewModelFactory }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
exportViewModel.cleanUpOldPdfs(1000 * 3600) exportViewModel.cleanUpOldPdfs(1000 * 3600)
} }
@@ -158,11 +162,35 @@ class MainActivity : ComponentActivity() {
is Screen.Overlay.Libraries -> { is Screen.Overlay.Libraries -> {
LibrariesScreen(onBack = navigation.back) LibrariesScreen(onBack = navigation.back)
} }
is Screen.Overlay.Settings -> {
SettingsScreenWrapper(settingsViewModel, navigation)
}
} }
} }
} }
} }
@Composable
private fun SettingsScreenWrapper(settingsViewModel: SettingsViewModel, nav: Navigation) {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
) { uri ->
if (uri != null) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, flags)
settingsViewModel.setExportDirUri(uri.toString())
}
}
val settingsUiState by settingsViewModel.uiState.collectAsStateWithLifecycle()
SettingsScreen(
settingsUiState,
onChooseDirectoryClick = { launcher.launch(null) },
onResetExportDirClick = { settingsViewModel.setExportDirUri(null) },
onBack = nav.back
)
}
@Composable @Composable
private fun CollectAboutEvents( private fun CollectAboutEvents(
context: Context, context: Context,
@@ -303,5 +331,6 @@ private fun navigation(viewModel: MainViewModel): Navigation = Navigation(
toExportScreen = { viewModel.navigateTo(Screen.Main.Export) }, toExportScreen = { viewModel.navigateTo(Screen.Main.Export) },
toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) }, toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) },
toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) }, toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) },
toSettingsScreen = { viewModel.navigateTo(Screen.Overlay.Settings) },
back = { viewModel.navigateBack() } back = { viewModel.navigateBack() }
) )

View File

@@ -24,6 +24,7 @@ sealed class Screen {
sealed class Overlay : Screen() { sealed class Overlay : Screen() {
object About : Overlay() object About : Overlay()
object Libraries : Overlay() object Libraries : Overlay()
object Settings : Overlay()
} }
} }
@@ -34,6 +35,7 @@ data class Navigation(
val toExportScreen: () -> Unit, val toExportScreen: () -> Unit,
val toAboutScreen: () -> Unit, val toAboutScreen: () -> Unit,
val toLibrariesScreen: () -> Unit, val toLibrariesScreen: () -> Unit,
val toSettingsScreen: () -> Unit,
val back: () -> Unit, val back: () -> Unit,
) )

View File

@@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf
import org.fairscan.app.ui.state.DocumentUiModel import org.fairscan.app.ui.state.DocumentUiModel
fun dummyNavigation(): Navigation { fun dummyNavigation(): Navigation {
return Navigation({}, {}, {}, {}, {}, {}, {}) return Navigation({}, {}, {}, {}, {}, {}, {}, {})
} }
fun fakeDocument(): DocumentUiModel { fun fakeDocument(): DocumentUiModel {

View File

@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -85,19 +84,3 @@ fun BackButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
) )
} }
} }
@Composable
fun AboutScreenNavButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
IconButton(
onClick = onClick,
modifier = modifier
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = stringResource(R.string.about),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f))
}
}

View File

@@ -26,18 +26,32 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.fairscan.app.ui.Navigation
@Composable @Composable
fun MyScaffold( fun MyScaffold(
toAboutScreen: () -> Unit, navigation: Navigation,
pageListState: CommonPageListState, pageListState: CommonPageListState,
pageListButton: (@Composable () -> Unit)? = null, pageListButton: (@Composable () -> Unit)? = null,
bottomBar: @Composable () -> Unit, bottomBar: @Composable () -> Unit,
@@ -69,8 +83,8 @@ fun MyScaffold(
.windowInsetsPadding(WindowInsets.safeDrawing) .windowInsetsPadding(WindowInsets.safeDrawing)
) )
} }
AboutScreenNavButton( AppOverflowMenu(
onClick = toAboutScreen, navigation,
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.windowInsetsPadding(WindowInsets.safeDrawing) .windowInsetsPadding(WindowInsets.safeDrawing)
@@ -86,11 +100,11 @@ fun DocumentBar(
pageListButton: (@Composable () -> Unit)? = null, pageListButton: (@Composable () -> Unit)? = null,
) { ) {
val isLandscape = isLandscape(LocalConfiguration.current) val isLandscape = isLandscape(LocalConfiguration.current)
Column ( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.background(MaterialTheme.colorScheme.surfaceContainer) modifier = modifier.background(MaterialTheme.colorScheme.surfaceContainer)
) { ) {
Box ( Box(
if (isLandscape) if (isLandscape)
Modifier Modifier
.weight(1f) .weight(1f)
@@ -144,3 +158,45 @@ fun DocumentBar(
fun isLandscape(configuration: Configuration): Boolean { fun isLandscape(configuration: Configuration): Boolean {
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
} }
@Composable
fun AppOverflowMenu(
navigation: Navigation,
modifier: Modifier = Modifier,
) {
var expanded by remember { mutableStateOf(false) }
Box(
modifier
) {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Menu")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
) {
DropdownMenuItem(
leadingIcon = { Icon(Icons.Default.Settings, contentDescription = null) },
text = { Text("Settings") },
onClick = {
expanded = false
navigation.toSettingsScreen()
}
)
DropdownMenuItem(
leadingIcon = { Icon(Icons.Default.Info, contentDescription = null) },
text = { Text("About") },
onClick = {
expanded = false
navigation.toAboutScreen()
}
)
}
}
}

View File

@@ -95,7 +95,7 @@ fun DocumentScreen(
} }
MyScaffold( MyScaffold(
toAboutScreen = navigation.toAboutScreen, navigation = navigation,
pageListState = CommonPageListState( pageListState = CommonPageListState(
document, document,
onPageClick = { index -> currentPageIndex.intValue = index }, onPageClick = { index -> currentPageIndex.intValue = index },
@@ -103,7 +103,6 @@ fun DocumentScreen(
currentPageIndex = currentPageIndex.intValue, currentPageIndex = currentPageIndex.intValue,
listState = listState, listState = listState,
), ),
onBack = navigation.back,
bottomBar = { bottomBar = {
BottomBar(navigation) BottomBar(navigation)
}, },

View File

@@ -221,9 +221,8 @@ private fun CameraScreenScaffold(
Box { Box {
MyScaffold( MyScaffold(
toAboutScreen = navigation.toAboutScreen, navigation = navigation,
pageListState = pageListState, pageListState = pageListState,
onBack = navigation.back,
bottomBar = { Bar(cameraUiState.pageCount, onFinalizePressed) } bottomBar = { Bar(cameraUiState.pageCount, onFinalizePressed) }
) { modifier -> ) { modifier ->
CameraPreviewBox( CameraPreviewBox(

View File

@@ -69,7 +69,7 @@ import androidx.core.net.toUri
import org.fairscan.app.R import org.fairscan.app.R
import org.fairscan.app.data.GeneratedPdf import org.fairscan.app.data.GeneratedPdf
import org.fairscan.app.ui.Navigation import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.components.AboutScreenNavButton import org.fairscan.app.ui.components.AppOverflowMenu
import org.fairscan.app.ui.components.BackButton import org.fairscan.app.ui.components.BackButton
import org.fairscan.app.ui.components.MainActionButton import org.fairscan.app.ui.components.MainActionButton
import org.fairscan.app.ui.components.NewDocumentDialog import org.fairscan.app.ui.components.NewDocumentDialog
@@ -154,7 +154,7 @@ fun ExportScreen(
title = { Text(stringResource(R.string.export_pdf)) }, title = { Text(stringResource(R.string.export_pdf)) },
navigationIcon = { BackButton(navigation.back) }, navigationIcon = { BackButton(navigation.back) },
actions = { actions = {
AboutScreenNavButton(onClick = navigation.toAboutScreen) AppOverflowMenu(navigation)
} }
) )
} }

View File

@@ -55,13 +55,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.fairscan.app.ui.components.CameraPermissionState
import org.fairscan.app.ui.Navigation
import org.fairscan.app.R import org.fairscan.app.R
import org.fairscan.app.ui.components.rememberCameraPermissionState import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.components.AboutScreenNavButton import org.fairscan.app.ui.components.AppOverflowMenu
import org.fairscan.app.ui.components.CameraPermissionState
import org.fairscan.app.ui.components.formatDate import org.fairscan.app.ui.components.formatDate
import org.fairscan.app.ui.components.pageCountText import org.fairscan.app.ui.components.pageCountText
import org.fairscan.app.ui.components.rememberCameraPermissionState
import org.fairscan.app.ui.dummyNavigation import org.fairscan.app.ui.dummyNavigation
import org.fairscan.app.ui.fakeDocument import org.fairscan.app.ui.fakeDocument
import org.fairscan.app.ui.state.DocumentUiModel import org.fairscan.app.ui.state.DocumentUiModel
@@ -82,9 +82,7 @@ fun HomeScreen(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.app_name)) }, title = { Text(stringResource(R.string.app_name)) },
actions = { actions = { AppOverflowMenu(navigation) }
AboutScreenNavButton(onClick = navigation.toAboutScreen)
}
) )
}, },
) { padding -> ) { padding ->

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program 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.
* This program 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
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.fairscan.app.ui.screens.settings
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "fairscan_settings")
class SettingsRepository(private val context: Context) {
private val EXPORT_DIR_URI = stringPreferencesKey("export_dir_uri")
val exportDirUri: Flow<String?> =
context.dataStore.data.map { prefs ->
prefs[EXPORT_DIR_URI]
}
suspend fun setExportDirUri(uri: String?) {
context.dataStore.edit { prefs ->
if (uri == null) {
prefs.remove(EXPORT_DIR_URI)
} else {
prefs[EXPORT_DIR_URI] = uri
}
}
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program 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.
* This program 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
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.fairscan.app.ui.screens.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import org.fairscan.app.ui.components.BackButton
import org.fairscan.app.ui.theme.FairScanTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
uiState: SettingsUiState,
onChooseDirectoryClick: () -> Unit,
onResetExportDirClick: () -> Unit,
onBack: () -> Unit,
) {
BackHandler { onBack() }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Settings") },
navigationIcon = { BackButton(onBack) },
)
}
) { paddingValues ->
SettingsContent(uiState, onChooseDirectoryClick, onResetExportDirClick, modifier = Modifier.padding(paddingValues))
}
}
@Composable
private fun SettingsContent(
uiState: SettingsUiState,
onChooseDirectoryClick: () -> Unit,
onResetExportDirClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val folderName = remember(uiState.exportDirUri) {
extractFolderName(uiState.exportDirUri)
}
Column(
modifier
.fillMaxSize()
.padding(20.dp)
) {
DirectorySettingItem(
label = "Export directory",
folderName = folderName,
onClick = onChooseDirectoryClick
)
Spacer(Modifier.height(12.dp))
if (uiState.exportDirUri != null) {
OutlinedButton(
onClick = onResetExportDirClick,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
) {
Text("Reset to default")
}
}
}
}
@Composable
fun DirectorySettingItem(
label: String,
folderName: String,
onClick: () -> Unit,
) {
Column {
Text(
text = label,
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(8.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = MaterialTheme.shapes.medium
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = folderName,
style = MaterialTheme.typography.bodyLarge
)
Icon(
Icons.Default.Folder,
contentDescription = "Change directory",
)
}
}
}
}
private fun extractFolderName(uriString: String?): String {
if (uriString == null) return "Downloads (default)"
return runCatching {
val uri = uriString.toUri()
uri.lastPathSegment?.substringAfter(':')?.substringAfter('/') ?: uriString
}.getOrElse { uriString }
}
@Preview
@Composable
fun SettingsScreenPreviewWithoutDir() {
SettingsScreenPreview(SettingsUiState(null))
}
@Preview
@Composable
fun SettingsScreenPreviewWithDir() {
SettingsScreenPreview(SettingsUiState("content://root/dir"))
}
@Composable
fun SettingsScreenPreview(uiState: SettingsUiState) {
FairScanTheme {
SettingsScreen(uiState, onChooseDirectoryClick = {}, onResetExportDirClick = {}, onBack= {})
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program 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.
* This program 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
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.fairscan.app.ui.screens.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.fairscan.app.AppContainer
data class SettingsUiState(
val exportDirUri: String? = null
)
class SettingsViewModel(container: AppContainer) : ViewModel() {
private val repo = container.settingsRepository
val uiState: StateFlow<SettingsUiState> =
repo.exportDirUri
.map { uri -> SettingsUiState(exportDirUri = uri) }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
SettingsUiState()
)
fun setExportDirUri(uri: String?) {
viewModelScope.launch {
repo.setExportDirUri(uri)
}
}
}

View File

@@ -10,7 +10,7 @@ lifecycleRuntimeKtx = "2.9.3"
activityCompose = "1.10.1" activityCompose = "1.10.1"
composeBom = "2025.08.01" composeBom = "2025.08.01"
camerax = "1.4.2" camerax = "1.4.2"
datastore = "1.1.7" datastore = "1.2.0"
litert = "1.4.0" litert = "1.4.0"
opencv = "4.12.0" opencv = "4.12.0"
assertj = "3.27.4" assertj = "3.27.4"
@@ -46,6 +46,7 @@ androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2",
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
androidx-datastore = { group = "androidx.datastore", name = "datastore" , version.ref = "datastore" } androidx-datastore = { group = "androidx.datastore", name = "datastore" , version.ref = "datastore" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences" , version.ref = "datastore" }
protobuf-javalite = { group = "com.google.protobuf", name="protobuf-javalite", version.ref = "protobufJavaLite"} protobuf-javalite = { group = "com.google.protobuf", name="protobuf-javalite", version.ref = "protobufJavaLite"}
litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "litert" } litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "litert" }
litert-support = { group = "com.google.ai.edge.litert", name = "litert-support", version.ref = "litert" } litert-support = { group = "com.google.ai.edge.litert", name = "litert-support", version.ref = "litert" }