New SettingsScreen with export dir preference
This commit is contained in:
committed by
pynicolas
parent
96b2d5b830
commit
7c9267a866
@@ -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)
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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= {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user